1
16
17
18
19
20
21
22 package framework
23
24 import (
25 "context"
26 "fmt"
27 "math/rand"
28 "os"
29 "path"
30 "reflect"
31 "strings"
32 "time"
33
34 "k8s.io/apimachinery/pkg/runtime"
35
36 v1 "k8s.io/api/core/v1"
37 apierrors "k8s.io/apimachinery/pkg/api/errors"
38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
39 "k8s.io/apimachinery/pkg/labels"
40 "k8s.io/apimachinery/pkg/runtime/schema"
41 "k8s.io/apimachinery/pkg/util/wait"
42 v1svc "k8s.io/client-go/applyconfigurations/core/v1"
43 "k8s.io/client-go/discovery"
44 cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
45 "k8s.io/client-go/dynamic"
46 clientset "k8s.io/client-go/kubernetes"
47 "k8s.io/client-go/kubernetes/scheme"
48 "k8s.io/client-go/rest"
49 "k8s.io/client-go/restmapper"
50 scaleclient "k8s.io/client-go/scale"
51 admissionapi "k8s.io/pod-security-admission/api"
52
53 "github.com/onsi/ginkgo/v2"
54 )
55
56 const (
57
58 DefaultNamespaceDeletionTimeout = 5 * time.Minute
59 defaultServiceAccountName = "default"
60 )
61
62 var (
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 NewFrameworkExtensions []func(f *Framework)
88 )
89
90
91
92
93
94
95
96
97
98 type Framework struct {
99 BaseName string
100
101
102
103
104 UniqueName string
105
106 clientConfig *rest.Config
107 ClientSet clientset.Interface
108 KubemarkExternalClusterClientSet clientset.Interface
109
110 DynamicClient dynamic.Interface
111
112 ScalesGetter scaleclient.ScalesGetter
113
114 SkipNamespaceCreation bool
115 SkipSecretCreation bool
116 Namespace *v1.Namespace
117 namespacesToDelete []*v1.Namespace
118 NamespaceDeletionTimeout time.Duration
119 NamespacePodSecurityEnforceLevel admissionapi.Level
120 NamespacePodSecurityWarnLevel admissionapi.Level
121 NamespacePodSecurityAuditLevel admissionapi.Level
122 NamespacePodSecurityLevel admissionapi.Level
123
124
125 flakeReport *FlakeReport
126
127
128 Options Options
129
130
131
132 TestSummaries []TestDataSummary
133
134
135 Timeouts *TimeoutContext
136
137
138
139 DumpAllNamespaceInfo DumpAllNamespaceInfoAction
140 }
141
142
143
144 type DumpAllNamespaceInfoAction func(ctx context.Context, f *Framework, namespace string)
145
146
147 type TestDataSummary interface {
148 SummaryKind() string
149 PrintHumanReadable() string
150 PrintJSON() string
151 }
152
153
154 type Options struct {
155 ClientQPS float32
156 ClientBurst int
157 GroupVersion *schema.GroupVersion
158 }
159
160
161
162
163 func NewFrameworkWithCustomTimeouts(baseName string, timeouts *TimeoutContext) *Framework {
164 f := NewDefaultFramework(baseName)
165 in := reflect.ValueOf(timeouts).Elem()
166 out := reflect.ValueOf(f.Timeouts).Elem()
167 for i := 0; i < in.NumField(); i++ {
168 value := in.Field(i)
169 if !value.IsZero() {
170 out.Field(i).Set(value)
171 }
172 }
173 return f
174 }
175
176
177
178
179
180 func NewDefaultFramework(baseName string) *Framework {
181 options := Options{
182 ClientQPS: 20,
183 ClientBurst: 50,
184 }
185 return NewFramework(baseName, options, nil)
186 }
187
188
189 func NewFramework(baseName string, options Options, client clientset.Interface) *Framework {
190 f := &Framework{
191 BaseName: baseName,
192 Options: options,
193 ClientSet: client,
194 Timeouts: NewTimeoutContext(),
195 }
196
197
198
199
200 ginkgo.BeforeEach(f.BeforeEach, AnnotatedLocation("set up framework"))
201 for _, extension := range NewFrameworkExtensions {
202 extension(f)
203 }
204
205 return f
206 }
207
208
209 func (f *Framework) BeforeEach(ctx context.Context) {
210
211
212
213
214
215 ginkgo.DeferCleanup(f.AfterEach, AnnotatedLocation("tear down framework"))
216
217
218 ginkgo.DeferCleanup(f.dumpNamespaceInfo, AnnotatedLocation("dump namespaces"))
219
220 ginkgo.By("Creating a kubernetes client")
221 config, err := LoadConfig()
222 ExpectNoError(err)
223
224 config.QPS = f.Options.ClientQPS
225 config.Burst = f.Options.ClientBurst
226 if f.Options.GroupVersion != nil {
227 config.GroupVersion = f.Options.GroupVersion
228 }
229 if TestContext.KubeAPIContentType != "" {
230 config.ContentType = TestContext.KubeAPIContentType
231 }
232 f.clientConfig = rest.CopyConfig(config)
233 f.ClientSet, err = clientset.NewForConfig(config)
234 ExpectNoError(err)
235 f.DynamicClient, err = dynamic.NewForConfig(config)
236 ExpectNoError(err)
237
238
239
240 if config.GroupVersion == nil {
241 config.GroupVersion = &schema.GroupVersion{}
242 }
243 if config.NegotiatedSerializer == nil {
244 config.NegotiatedSerializer = scheme.Codecs
245 }
246 restClient, err := rest.RESTClientFor(config)
247 ExpectNoError(err)
248 discoClient, err := discovery.NewDiscoveryClientForConfig(config)
249 ExpectNoError(err)
250 cachedDiscoClient := cacheddiscovery.NewMemCacheClient(discoClient)
251 restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoClient)
252 restMapper.Reset()
253 resolver := scaleclient.NewDiscoveryScaleKindResolver(cachedDiscoClient)
254 f.ScalesGetter = scaleclient.New(restClient, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
255
256 TestContext.CloudConfig.Provider.FrameworkBeforeEach(f)
257
258 if !f.SkipNamespaceCreation {
259 ginkgo.By(fmt.Sprintf("Building a namespace api object, basename %s", f.BaseName))
260 namespace, err := f.CreateNamespace(ctx, f.BaseName, map[string]string{
261 "e2e-framework": f.BaseName,
262 })
263 ExpectNoError(err)
264
265 f.Namespace = namespace
266
267 if TestContext.VerifyServiceAccount {
268 ginkgo.By("Waiting for a default service account to be provisioned in namespace")
269 err = WaitForDefaultServiceAccountInNamespace(ctx, f.ClientSet, namespace.Name)
270 ExpectNoError(err)
271 ginkgo.By("Waiting for kube-root-ca.crt to be provisioned in namespace")
272 err = WaitForKubeRootCAInNamespace(ctx, f.ClientSet, namespace.Name)
273 ExpectNoError(err)
274 } else {
275 Logf("Skipping waiting for service account")
276 }
277
278 f.UniqueName = f.Namespace.GetName()
279 } else {
280
281 f.UniqueName = fmt.Sprintf("%s-%08x", f.BaseName, rand.Int31())
282 }
283
284 f.flakeReport = NewFlakeReport()
285 }
286
287 func (f *Framework) dumpNamespaceInfo(ctx context.Context) {
288 if !ginkgo.CurrentSpecReport().Failed() {
289 return
290 }
291 if !TestContext.DumpLogsOnFailure {
292 return
293 }
294 if f.DumpAllNamespaceInfo == nil {
295 return
296 }
297 ginkgo.By("dump namespace information after failure", func() {
298 if !f.SkipNamespaceCreation {
299 for _, ns := range f.namespacesToDelete {
300 f.DumpAllNamespaceInfo(ctx, f, ns.Name)
301 }
302 }
303 })
304 }
305
306
307 func printSummaries(summaries []TestDataSummary, testBaseName string) {
308 now := time.Now()
309 for i := range summaries {
310 Logf("Printing summary: %v", summaries[i].SummaryKind())
311 switch TestContext.OutputPrintType {
312 case "hr":
313 if TestContext.ReportDir == "" {
314 Logf(summaries[i].PrintHumanReadable())
315 } else {
316
317 filePath := path.Join(TestContext.ReportDir, summaries[i].SummaryKind()+"_"+testBaseName+"_"+now.Format(time.RFC3339)+".txt")
318 if err := os.WriteFile(filePath, []byte(summaries[i].PrintHumanReadable()), 0644); err != nil {
319 Logf("Failed to write file %v with test performance data: %v", filePath, err)
320 }
321 }
322 case "json":
323 fallthrough
324 default:
325 if TestContext.OutputPrintType != "json" {
326 Logf("Unknown output type: %v. Printing JSON", TestContext.OutputPrintType)
327 }
328 if TestContext.ReportDir == "" {
329 Logf("%v JSON\n%v", summaries[i].SummaryKind(), summaries[i].PrintJSON())
330 Logf("Finished")
331 } else {
332
333 filePath := path.Join(TestContext.ReportDir, summaries[i].SummaryKind()+"_"+testBaseName+"_"+now.Format(time.RFC3339)+".json")
334 Logf("Writing to %s", filePath)
335 if err := os.WriteFile(filePath, []byte(summaries[i].PrintJSON()), 0644); err != nil {
336 Logf("Failed to write file %v with test performance data: %v", filePath, err)
337 }
338 }
339 }
340 }
341 }
342
343
344 func (f *Framework) AfterEach(ctx context.Context) {
345
346
347 if f.ClientSet == nil {
348 Failf("The framework ClientSet must not be nil at this point")
349 }
350
351
352
353 defer func() {
354 nsDeletionErrors := map[string]error{}
355
356
357
358 if TestContext.DeleteNamespace && (TestContext.DeleteNamespaceOnFailure || !ginkgo.CurrentSpecReport().Failed()) {
359 for _, ns := range f.namespacesToDelete {
360 ginkgo.By(fmt.Sprintf("Destroying namespace %q for this suite.", ns.Name))
361 if err := f.ClientSet.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{}); err != nil {
362 if !apierrors.IsNotFound(err) {
363 nsDeletionErrors[ns.Name] = err
364
365
366 if !ginkgo.CurrentSpecReport().Failed() && TestContext.DumpLogsOnFailure && f.DumpAllNamespaceInfo != nil {
367 f.DumpAllNamespaceInfo(ctx, f, ns.Name)
368 }
369 } else {
370 Logf("Namespace %v was already deleted", ns.Name)
371 }
372 }
373 }
374 } else {
375 if !TestContext.DeleteNamespace {
376 Logf("Found DeleteNamespace=false, skipping namespace deletion!")
377 } else {
378 Logf("Found DeleteNamespaceOnFailure=false and current test failed, skipping namespace deletion!")
379 }
380 }
381
382
383
384
385 f.Namespace = nil
386 f.clientConfig = nil
387 f.ClientSet = nil
388 f.namespacesToDelete = nil
389
390
391 if len(nsDeletionErrors) != 0 {
392 messages := []string{}
393 for namespaceKey, namespaceErr := range nsDeletionErrors {
394 messages = append(messages, fmt.Sprintf("Couldn't delete ns: %q: %s (%#v)", namespaceKey, namespaceErr, namespaceErr))
395 }
396 Failf(strings.Join(messages, ","))
397 }
398 }()
399
400 TestContext.CloudConfig.Provider.FrameworkAfterEach(f)
401
402
403 if f.flakeReport != nil && f.flakeReport.GetFlakeCount() > 0 {
404 f.TestSummaries = append(f.TestSummaries, f.flakeReport)
405 f.flakeReport = nil
406 }
407
408 printSummaries(f.TestSummaries, f.BaseName)
409 }
410
411
412
413
414 func (f *Framework) DeleteNamespace(ctx context.Context, name string) {
415 defer func() {
416 err := f.ClientSet.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{})
417 if err != nil && !apierrors.IsNotFound(err) {
418 Logf("error deleting namespace %s: %v", name, err)
419 return
420 }
421 err = WaitForNamespacesDeleted(ctx, f.ClientSet, []string{name}, DefaultNamespaceDeletionTimeout)
422 if err != nil {
423 Logf("error deleting namespace %s: %v", name, err)
424 return
425 }
426
427 for i, ns := range f.namespacesToDelete {
428 if ns == nil {
429 continue
430 }
431 if ns.Name == name {
432 f.namespacesToDelete = append(f.namespacesToDelete[:i], f.namespacesToDelete[i+1:]...)
433 }
434 }
435 }()
436
437 if !f.SkipNamespaceCreation && ginkgo.CurrentSpecReport().Failed() && TestContext.DumpLogsOnFailure && f.DumpAllNamespaceInfo != nil {
438 f.DumpAllNamespaceInfo(ctx, f, name)
439 }
440
441 }
442
443
444 func (f *Framework) CreateNamespace(ctx context.Context, baseName string, labels map[string]string) (*v1.Namespace, error) {
445 createTestingNS := TestContext.CreateTestingNS
446 if createTestingNS == nil {
447 createTestingNS = CreateTestingNS
448 }
449
450 if labels == nil {
451 labels = make(map[string]string)
452 } else {
453 labelsCopy := make(map[string]string)
454 for k, v := range labels {
455 labelsCopy[k] = v
456 }
457 labels = labelsCopy
458 }
459
460 labels[admissionapi.EnforceLevelLabel] = firstNonEmptyPSaLevelOrRestricted(f.NamespacePodSecurityEnforceLevel, f.NamespacePodSecurityLevel)
461 labels[admissionapi.WarnLevelLabel] = firstNonEmptyPSaLevelOrRestricted(f.NamespacePodSecurityWarnLevel, f.NamespacePodSecurityLevel)
462 labels[admissionapi.AuditLevelLabel] = firstNonEmptyPSaLevelOrRestricted(f.NamespacePodSecurityAuditLevel, f.NamespacePodSecurityLevel)
463
464 ns, err := createTestingNS(ctx, baseName, f.ClientSet, labels)
465
466
467 f.AddNamespacesToDelete(ns)
468
469 if TestContext.E2EDockerConfigFile != "" && !f.SkipSecretCreation {
470
471
472 secret, err := f.createSecretFromDockerConfig(ctx, ns.Name)
473 if err != nil {
474 return ns, fmt.Errorf("failed to create secret from docker config file: %v", err)
475 }
476
477 serviceAccountClient := f.ClientSet.CoreV1().ServiceAccounts(ns.Name)
478 serviceAccountConfig := v1svc.ServiceAccount(defaultServiceAccountName, ns.Name)
479 serviceAccountConfig.ImagePullSecrets = append(serviceAccountConfig.ImagePullSecrets, v1svc.LocalObjectReferenceApplyConfiguration{Name: &secret.Name})
480
481 svc, err := serviceAccountClient.Apply(ctx, serviceAccountConfig, metav1.ApplyOptions{FieldManager: "e2e-framework"})
482 if err != nil {
483 return ns, fmt.Errorf("failed to patch imagePullSecret [%s] to service account [%s]: %v", secret.Name, svc.Name, err)
484 }
485
486 }
487
488 return ns, err
489 }
490
491 func firstNonEmptyPSaLevelOrRestricted(levelConfig ...admissionapi.Level) string {
492 for _, l := range levelConfig {
493 if len(l) > 0 {
494 return string(l)
495 }
496 }
497 return string(admissionapi.LevelRestricted)
498 }
499
500
501
502 func (f *Framework) createSecretFromDockerConfig(ctx context.Context, namespace string) (*v1.Secret, error) {
503 contents, err := os.ReadFile(TestContext.E2EDockerConfigFile)
504 if err != nil {
505 return nil, fmt.Errorf("error reading docker config file: %v", err)
506 }
507
508 secretObject := &v1.Secret{
509 Data: map[string][]byte{v1.DockerConfigJsonKey: contents},
510 Type: v1.SecretTypeDockerConfigJson,
511 }
512 secretObject.GenerateName = "registry-cred"
513 Logf("create image pull secret %s", secretObject.Name)
514
515 secret, err := f.ClientSet.CoreV1().Secrets(namespace).Create(ctx, secretObject, metav1.CreateOptions{})
516
517 return secret, err
518 }
519
520
521
522 func (f *Framework) RecordFlakeIfError(err error, optionalDescription ...interface{}) {
523 f.flakeReport.RecordFlakeIfError(err, optionalDescription...)
524 }
525
526
527
528 func (f *Framework) AddNamespacesToDelete(namespaces ...*v1.Namespace) {
529 for _, ns := range namespaces {
530 if ns == nil {
531 continue
532 }
533 f.namespacesToDelete = append(f.namespacesToDelete, ns)
534
535 }
536 }
537
538
539 func (f *Framework) ClientConfig() *rest.Config {
540 ret := rest.CopyConfig(f.clientConfig)
541
542 ret.ContentType = runtime.ContentTypeJSON
543 ret.AcceptContentTypes = runtime.ContentTypeJSON
544 return ret
545 }
546
547
548 type KubeUser struct {
549 Name string `yaml:"name"`
550 User struct {
551 Username string `yaml:"username"`
552 Password string `yaml:"password" datapolicy:"password"`
553 Token string `yaml:"token" datapolicy:"token"`
554 } `yaml:"user"`
555 }
556
557
558 type KubeCluster struct {
559 Name string `yaml:"name"`
560 Cluster struct {
561 CertificateAuthorityData string `yaml:"certificate-authority-data"`
562 Server string `yaml:"server"`
563 } `yaml:"cluster"`
564 }
565
566
567 type KubeConfig struct {
568 Contexts []struct {
569 Name string `yaml:"name"`
570 Context struct {
571 Cluster string `yaml:"cluster"`
572 User string
573 } `yaml:"context"`
574 } `yaml:"contexts"`
575
576 Clusters []KubeCluster `yaml:"clusters"`
577
578 Users []KubeUser `yaml:"users"`
579 }
580
581
582 func (kc *KubeConfig) FindUser(name string) *KubeUser {
583 for _, user := range kc.Users {
584 if user.Name == name {
585 return &user
586 }
587 }
588 return nil
589 }
590
591
592 func (kc *KubeConfig) FindCluster(name string) *KubeCluster {
593 for _, cluster := range kc.Clusters {
594 if cluster.Name == name {
595 return &cluster
596 }
597 }
598 return nil
599 }
600
601
602
603
604 type PodStateVerification struct {
605
606 Selectors map[string]string
607
608
609 ValidPhases []v1.PodPhase
610
611
612
613
614
615
616 Verify func(v1.Pod) (bool, error)
617
618
619 PodName string
620 }
621
622
623 type ClusterVerification struct {
624 client clientset.Interface
625 namespace *v1.Namespace
626 podState PodStateVerification
627 }
628
629
630 func (f *Framework) NewClusterVerification(namespace *v1.Namespace, filter PodStateVerification) *ClusterVerification {
631 return &ClusterVerification{
632 f.ClientSet,
633 namespace,
634 filter,
635 }
636 }
637
638 func passesPodNameFilter(pod v1.Pod, name string) bool {
639 return name == "" || strings.Contains(pod.Name, name)
640 }
641
642 func passesVerifyFilter(pod v1.Pod, verify func(p v1.Pod) (bool, error)) (bool, error) {
643 if verify == nil {
644 return true, nil
645 }
646
647 verified, err := verify(pod)
648
649 if err != nil {
650 return false, err
651 }
652 return verified, nil
653 }
654
655 func passesPhasesFilter(pod v1.Pod, validPhases []v1.PodPhase) bool {
656 passesPhaseFilter := false
657 for _, phase := range validPhases {
658 if pod.Status.Phase == phase {
659 passesPhaseFilter = true
660 }
661 }
662 return passesPhaseFilter
663 }
664
665
666 func filterLabels(ctx context.Context, selectors map[string]string, cli clientset.Interface, ns string) (*v1.PodList, error) {
667 var err error
668 var selector labels.Selector
669 var pl *v1.PodList
670
671
672 if len(selectors) > 0 {
673 selector = labels.SelectorFromSet(labels.Set(selectors))
674 options := metav1.ListOptions{LabelSelector: selector.String()}
675 pl, err = cli.CoreV1().Pods(ns).List(ctx, options)
676 } else {
677 pl, err = cli.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{})
678 }
679 return pl, err
680 }
681
682
683
684
685 func (p *PodStateVerification) filter(ctx context.Context, c clientset.Interface, namespace *v1.Namespace) ([]v1.Pod, error) {
686 if len(p.ValidPhases) == 0 || namespace == nil {
687 panic(fmt.Errorf("Need to specify a valid pod phases (%v) and namespace (%v). ", p.ValidPhases, namespace))
688 }
689
690 ns := namespace.Name
691 pl, err := filterLabels(ctx, p.Selectors, c, ns)
692 Logf("Selector matched %v pods for %v", len(pl.Items), p.Selectors)
693 if len(pl.Items) == 0 || err != nil {
694 return pl.Items, err
695 }
696
697 unfilteredPods := pl.Items
698 filteredPods := []v1.Pod{}
699 ReturnPodsSoFar:
700
701 for _, pod := range unfilteredPods {
702 if !(passesPhasesFilter(pod, p.ValidPhases) && passesPodNameFilter(pod, p.PodName)) {
703 continue
704 }
705 passesVerify, err := passesVerifyFilter(pod, p.Verify)
706 if err != nil {
707 Logf("Error detected on %v : %v !", pod.Name, err)
708 break ReturnPodsSoFar
709 }
710 if passesVerify {
711 filteredPods = append(filteredPods, pod)
712 }
713 }
714 return filteredPods, err
715 }
716
717
718
719 func (cl *ClusterVerification) WaitFor(ctx context.Context, atLeast int, timeout time.Duration) ([]v1.Pod, error) {
720 pods := []v1.Pod{}
721 var returnedErr error
722
723 err := wait.PollWithContext(ctx, 1*time.Second, timeout, func(ctx context.Context) (bool, error) {
724 pods, returnedErr = cl.podState.filter(ctx, cl.client, cl.namespace)
725
726
727 if returnedErr != nil {
728 Logf("Cutting polling short: We got an error from the pod filtering layer.")
729
730
731 return false, returnedErr
732 }
733 Logf("Found %v / %v", len(pods), atLeast)
734
735
736 if len(pods) >= atLeast {
737 return true, nil
738 }
739
740 return false, nil
741 })
742 Logf("WaitFor completed with timeout %v. Pods found = %v out of %v", timeout, len(pods), atLeast)
743 return pods, err
744 }
745
746
747 func (cl *ClusterVerification) WaitForOrFail(ctx context.Context, atLeast int, timeout time.Duration) {
748 pods, err := cl.WaitFor(ctx, atLeast, timeout)
749 if err != nil || len(pods) < atLeast {
750 Failf("Verified %v of %v pods , error : %v", len(pods), atLeast, err)
751 }
752 }
753
754
755
756
757
758
759 func (cl *ClusterVerification) ForEach(ctx context.Context, podFunc func(v1.Pod)) error {
760 pods, err := cl.podState.filter(ctx, cl.client, cl.namespace)
761 if err == nil {
762 if len(pods) == 0 {
763 Failf("No pods matched the filter.")
764 }
765 Logf("ForEach: Found %v pods from the filter. Now looping through them.", len(pods))
766 for _, p := range pods {
767 podFunc(p)
768 }
769 } else {
770 Logf("ForEach: Something went wrong when filtering pods to execute against: %v", err)
771 }
772
773 return err
774 }
775
View as plain text