diff --git a/docs/testing/reference.md b/docs/testing/reference.md index 9fd5b93b..04980395 100644 --- a/docs/testing/reference.md +++ b/docs/testing/reference.md @@ -73,6 +73,7 @@ delete | list of object references | A list of objects to delete, if they do n index | int | Override the test step's index. commands | list of [Commands](#commands) | Commands to run prior at the beginning of the test step. kubeconfig | string | The Kubeconfig file to use to run the included steps(s). +kubeconfigLoading | string | Specifies the mode for loading Kubeconfig and making a cluster connection: `Eager` (when loading the test definition) or `Lazy` (right before executing the step, makes it possible to generate the Kubeconfig in a preceding step). Defaults to `Eager`. unitTest | bool | Indicates if the step is a unit test, safe to run without a real Kubernetes cluster. diff --git a/pkg/apis/testharness/v1beta1/test_types.go b/pkg/apis/testharness/v1beta1/test_types.go index 675fd299..45aef4db 100644 --- a/pkg/apis/testharness/v1beta1/test_types.go +++ b/pkg/apis/testharness/v1beta1/test_types.go @@ -6,6 +6,9 @@ import ( "k8s.io/client-go/rest" ) +const KubeconfigLoadingEager = "Eager" +const KubeconfigLoadingLazy = "Lazy" + // Create embedded struct to implement custom DeepCopyInto method type RestConfig struct { RC *rest.Config @@ -125,6 +128,11 @@ type TestStep struct { // Kubeconfig to use when applying and asserting for this step. Kubeconfig string `json:"kubeconfig,omitempty"` + + // Specifies the mode for loading Kubeconfig: Eager/Lazy. Defaults to Eager. + // +kubebuilder:default=Eager + // +kubebuilder:validation:Enum=Eager;Lazy + KubeconfigLoading string `json:"kubeconfigLoading,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/test/case.go b/pkg/test/case.go index 2d264260..d8a2b450 100644 --- a/pkg/test/case.go +++ b/pkg/test/case.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/tools/clientcmd" + "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1" "github.com/kudobuilder/kuttl/pkg/report" testutils "github.com/kudobuilder/kuttl/pkg/test/utils" ) @@ -333,11 +334,11 @@ func (t *Case) Run(test *testing.T, ts *report.Testsuite) { clients := map[string]client.Client{"": cl} for _, testStep := range t.Steps { - if clients[testStep.Kubeconfig] != nil { + if clients[testStep.Kubeconfig] != nil || testStep.KubeconfigLoading == v1beta1.KubeconfigLoadingLazy { continue } - cl, err := newClient(testStep.Kubeconfig)(false) + cl, err = newClient(testStep.Kubeconfig)(false) if err != nil { setupReport.Failure = report.NewFailure(err.Error(), nil) ts.AddTestcase(setupReport) @@ -347,9 +348,11 @@ func (t *Case) Run(test *testing.T, ts *report.Testsuite) { clients[testStep.Kubeconfig] = cl } - for _, c := range clients { - if err := t.CreateNamespace(test, c, ns); err != nil { - setupReport.Failure = report.NewFailure(err.Error(), nil) + for kc, c := range clients { + if err = t.CreateNamespace(test, c, ns); k8serrors.IsAlreadyExists(err) { + t.Logger.Logf("namespace %q already exists, using kubeconfig %q", ns.Name, kc) + } else if err != nil { + setupReport.Failure = report.NewFailure("failed to create test namespace", []error{err}) ts.AddTestcase(setupReport) test.Fatal(err) } @@ -370,7 +373,25 @@ func (t *Case) Run(test *testing.T, ts *report.Testsuite) { tc.Assertions += len(testStep.Asserts) tc.Assertions += len(testStep.Errors) - errs := testStep.Run(test, ns.Name) + errs := []error{} + + // Set-up client/namespace for lazy-loaded Kubeconfig + if testStep.KubeconfigLoading == v1beta1.KubeconfigLoadingLazy { + cl, err = testStep.Client(false) + if err != nil { + errs = append(errs, fmt.Errorf("failed to lazy-load kubeconfig: %w", err)) + } else if err = t.CreateNamespace(test, cl, ns); k8serrors.IsAlreadyExists(err) { + t.Logger.Logf("namespace %q already exists", ns.Name) + } else if err != nil { + errs = append(errs, fmt.Errorf("failed to create test namespace: %w", err)) + } + } + + // Run test case only if no setup errors are encountered + if len(errs) == 0 { + errs = append(errs, testStep.Run(test, ns.Name)...) + } + if len(errs) > 0 { caseErr := fmt.Errorf("failed in step %s", testStep.String()) tc.Failure = report.NewFailure(caseErr.Error(), errs) diff --git a/pkg/test/step.go b/pkg/test/step.go index 8d79c920..522df6c5 100644 --- a/pkg/test/step.go +++ b/pkg/test/step.go @@ -51,9 +51,10 @@ type Step struct { Timeout int - Kubeconfig string - Client func(forceNew bool) (client.Client, error) - DiscoveryClient func() (discovery.DiscoveryInterface, error) + Kubeconfig string + KubeconfigLoading string + Client func(forceNew bool) (client.Client, error) + DiscoveryClient func() (discovery.DiscoveryInterface, error) Logger testutils.Logger } @@ -555,6 +556,13 @@ func (s *Step) LoadYAML(file string) error { exKubeconfig := env.Expand(s.Step.Kubeconfig) s.Kubeconfig = cleanPath(exKubeconfig, s.Dir) } + + switch s.Step.KubeconfigLoading { + case "", harness.KubeconfigLoadingEager, harness.KubeconfigLoadingLazy: + s.KubeconfigLoading = s.Step.KubeconfigLoading + default: + return fmt.Errorf("attribute 'kubeconfigLoading' has invalid value %q", s.Step.KubeconfigLoading) + } } else { applies = append(applies, obj) }