From 57ff62af1bf10a399973eded6c10456d2c3c2b9c Mon Sep 17 00:00:00 2001 From: Marcin Owsiany Date: Thu, 11 Aug 2022 06:41:46 +0200 Subject: [PATCH] WiP --- pkg/file/files.go | 4 +-- pkg/file/files_test.go | 5 +-- pkg/kuttlctl/cmd/test.go | 2 +- pkg/test/assert.go | 4 +-- pkg/test/case.go | 59 ++++++++++++++++--------------- pkg/test/case_integration_test.go | 1 + pkg/test/step.go | 18 +++++----- pkg/test/step_integration_test.go | 24 ++++++------- pkg/test/utils/kubernetes.go | 36 +++++++++++++++++-- pkg/test/utils/kubernetes_test.go | 4 +-- 10 files changed, 96 insertions(+), 61 deletions(-) diff --git a/pkg/file/files.go b/pkg/file/files.go index df94ad30..2cfbe4ad 100644 --- a/pkg/file/files.go +++ b/pkg/file/files.go @@ -13,11 +13,11 @@ import ( ) // from a list of paths, returns an array of runtime objects -func ToObjects(paths []string) ([]client.Object, error) { +func ToObjects(paths []string, templatingContext testutils.TemplatingContext) ([]client.Object, error) { apply := []client.Object{} for _, path := range paths { - objs, err := testutils.LoadYAMLFromFile(path) + objs, err := testutils.LoadYAMLFromFile(path, templatingContext) if err != nil { return nil, fmt.Errorf("file %q load yaml error: %w", path, err) } diff --git a/pkg/file/files_test.go b/pkg/file/files_test.go index bf7d96b3..f7668420 100644 --- a/pkg/file/files_test.go +++ b/pkg/file/files_test.go @@ -1,6 +1,7 @@ package file import ( + "github.com/kudobuilder/kuttl/pkg/test/utils" "testing" "github.com/stretchr/testify/assert" @@ -51,13 +52,13 @@ func TestFromPath(t *testing.T) { func TestToRuntimeObjects(t *testing.T) { files := []string{"testdata/path/test1.yaml"} - objs, err := ToObjects(files) + objs, err := ToObjects(files, utils.TemplatingContext{}) assert.NoError(t, err) assert.Equal(t, 1, len(objs)) assert.Equal(t, "Pod", objs[0].GetObjectKind().GroupVersionKind().Kind) files = append(files, "testdata/path/test2.yaml") - _, err = ToObjects(files) + _, err = ToObjects(files, utils.TemplatingContext{}) assert.Error(t, err, "file \"testdata/path/test2.yaml\" load yaml error") } diff --git a/pkg/kuttlctl/cmd/test.go b/pkg/kuttlctl/cmd/test.go index a97b949a..e2275131 100644 --- a/pkg/kuttlctl/cmd/test.go +++ b/pkg/kuttlctl/cmd/test.go @@ -88,7 +88,7 @@ For more detailed documentation, visit: https://kuttl.dev`, // Load the configuration YAML into options. if configPath != "" { - objects, err := testutils.LoadYAMLFromFile(configPath) + objects, err := testutils.LoadYAMLFromFile(configPath, testutils.TemplatingContext{}) if err != nil { return err } diff --git a/pkg/test/assert.go b/pkg/test/assert.go index 91a03c4e..3578ce68 100644 --- a/pkg/test/assert.go +++ b/pkg/test/assert.go @@ -18,7 +18,7 @@ func Assert(namespace string, timeout int, assertFiles ...string) error { var objects []client.Object for _, file := range assertFiles { - o, err := ObjectsFromPath(file, "") + o, err := ObjectsFromPath(file, "", testutils.GetTemplatingContext(namespace)) if err != nil { return err } @@ -64,7 +64,7 @@ func Errors(namespace string, timeout int, errorFiles ...string) error { var objects []client.Object for _, file := range errorFiles { - o, err := ObjectsFromPath(file, "") + o, err := ObjectsFromPath(file, "", testutils.GetTemplatingContext(namespace)) if err != nil { return err } diff --git a/pkg/test/case.go b/pkg/test/case.go index 567f27c5..69c05cab 100644 --- a/pkg/test/case.go +++ b/pkg/test/case.go @@ -28,7 +28,7 @@ import ( ) // testStepRegex contains one capturing group to determine the index of a step file. -var testStepRegex = regexp.MustCompile(`^(\d+)-(?:[^\.]+)(?:\.yaml)?$`) +var testStepRegex = regexp.MustCompile(`^(\d+)-(?:[^\.]+)(?:\.gotmpl)?(?:\.yaml)?$`) // Case contains all of the test steps and the Kubernetes client and other global configuration // for a test. @@ -42,6 +42,7 @@ type Case struct { Client func(forceNew bool) (client.Client, error) DiscoveryClient func() (discovery.DiscoveryInterface, error) + ns *namespace Logger testutils.Logger // Suppress is used to suppress logs @@ -54,13 +55,13 @@ type namespace struct { } // DeleteNamespace deletes a namespace in Kubernetes after we are done using it. -func (t *Case) DeleteNamespace(cl client.Client, ns *namespace) error { - if !ns.AutoCreated { - t.Logger.Log("Skipping deletion of user-supplied namespace:", ns.Name) +func (t *Case) DeleteNamespace(cl client.Client) error { + if !t.ns.AutoCreated { + t.Logger.Log("Skipping deletion of user-supplied namespace:", t.ns.Name) return nil } - t.Logger.Log("Deleting namespace:", ns.Name) + t.Logger.Log("Deleting namespace:", t.ns.Name) ctx := context.Background() if t.Timeout > 0 { @@ -71,7 +72,7 @@ func (t *Case) DeleteNamespace(cl client.Client, ns *namespace) error { return cl.Delete(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: ns.Name, + Name: t.ns.Name, }, TypeMeta: metav1.TypeMeta{ Kind: "Namespace", @@ -80,12 +81,12 @@ func (t *Case) DeleteNamespace(cl client.Client, ns *namespace) error { } // CreateNamespace creates a namespace in Kubernetes to use for a test. -func (t *Case) CreateNamespace(cl client.Client, ns *namespace) error { - if !ns.AutoCreated { - t.Logger.Log("Skipping creation of user-supplied namespace:", ns.Name) +func (t *Case) CreateNamespace(cl client.Client) error { + if !t.ns.AutoCreated { + t.Logger.Log("Skipping creation of user-supplied namespace:", t.ns.Name) return nil } - t.Logger.Log("Creating namespace:", ns.Name) + t.Logger.Log("Creating namespace:", t.ns.Name) ctx := context.Background() if t.Timeout > 0 { @@ -96,7 +97,7 @@ func (t *Case) CreateNamespace(cl client.Client, ns *namespace) error { return cl.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: ns.Name, + Name: t.ns.Name, }, TypeMeta: metav1.TypeMeta{ Kind: "Namespace", @@ -292,8 +293,6 @@ func shortString(obj *corev1.ObjectReference) string { // Run runs a test case including all of its steps. func (t *Case) Run(test *testing.T, tc *report.Testcase) { - ns := t.determineNamespace() - cl, err := t.Client(false) if err != nil { tc.Failure = report.NewFailure(err.Error(), nil) @@ -317,7 +316,7 @@ func (t *Case) Run(test *testing.T, tc *report.Testcase) { } for _, c := range clients { - if err := t.CreateNamespace(c, ns); err != nil { + if err := t.CreateNamespace(c); err != nil { tc.Failure = report.NewFailure(err.Error(), nil) test.Fatal(err) } @@ -326,7 +325,7 @@ func (t *Case) Run(test *testing.T, tc *report.Testcase) { if !t.SkipDelete { defer func() { for _, c := range clients { - if err := t.DeleteNamespace(c, ns); err != nil { + if err := t.DeleteNamespace(c); err != nil { test.Error(err) } } @@ -348,13 +347,13 @@ func (t *Case) Run(test *testing.T, tc *report.Testcase) { if !t.SkipDelete { defer func() { - if err := testStep.Clean(ns.Name); err != nil { + if err := testStep.Clean(t.ns.Name); err != nil { test.Error(err) } }() } - if errs := testStep.Run(ns.Name); len(errs) > 0 { + if errs := testStep.Run(t.ns.Name); len(errs) > 0 { caseErr := fmt.Errorf("failed in step %s", testStep.String()) tc.Failure = report.NewFailure(caseErr.Error(), errs) @@ -369,22 +368,22 @@ func (t *Case) Run(test *testing.T, tc *report.Testcase) { if funk.Contains(t.Suppress, "events") { t.Logger.Logf("skipping kubernetes event logging") } else { - t.CollectEvents(ns.Name) + t.CollectEvents(t.ns.Name) } } -func (t *Case) determineNamespace() *namespace { - ns := &namespace{ - Name: t.PreferredNamespace, - AutoCreated: false, - } - // no preferred ns, means we auto-create with petnames +func (t *Case) determineNamespace() { if t.PreferredNamespace == "" { - ns.Name = fmt.Sprintf("kuttl-test-%s", petname.Generate(2, "-")) - ns.AutoCreated = true + t.ns = &namespace{ + Name: fmt.Sprintf("kuttl-test-%s", petname.Generate(2, "-")), + AutoCreated: true, + } + } else { + t.ns = &namespace{ + Name: t.PreferredNamespace, + AutoCreated: false, + } } - // if we have a preferred namespace, we do NOT auto-create - return ns } // CollectTestStepFiles collects a map of test steps and their associated files @@ -446,6 +445,8 @@ func getIndexFromFile(fileName string) (int64, error) { // LoadTestSteps loads all of the test steps for a test case. func (t *Case) LoadTestSteps() error { + t.determineNamespace() + testStepFiles, err := t.CollectTestStepFiles() if err != nil { return err @@ -464,7 +465,7 @@ func (t *Case) LoadTestSteps() error { } for _, file := range files { - if err := testStep.LoadYAML(file); err != nil { + if err := testStep.LoadYAML(file, testutils.GetTemplatingContext(t.ns.Name)); err != nil { return err } } diff --git a/pkg/test/case_integration_test.go b/pkg/test/case_integration_test.go index 16e97413..fcc96bb1 100644 --- a/pkg/test/case_integration_test.go +++ b/pkg/test/case_integration_test.go @@ -100,6 +100,7 @@ func TestMultiClusterCase(t *testing.T) { return testenv.DiscoveryClient, nil }, } + c.determineNamespace() c.Run(t, &report.Testcase{}) } diff --git a/pkg/test/step.go b/pkg/test/step.go index 29857d05..286ba130 100644 --- a/pkg/test/step.go +++ b/pkg/test/step.go @@ -26,7 +26,7 @@ import ( // fileNameRegex contains two capturing groups to determine whether a file has special // meaning (ex. assert) or contains an appliable object, and extra name elements. -var fileNameRegex = regexp.MustCompile(`^(?:\d+-)?([^-\.]+)(-[^\.]+)?(?:\.yaml)?$`) +var fileNameRegex = regexp.MustCompile(`^(?:\d+-)?([^-\.]+)(-[^\.]+)?(?:\.gotmpl)?(?:\.yaml)?$`) // A Step contains the name of the test step, its index in the test, // and all of the test step's settings (including objects to apply and assert on). @@ -476,8 +476,8 @@ func (s *Step) String() string { // * If the YAML file is called "errors", then it contains objects that, // if seen, mark a test immediately failed. // * All other YAML files are considered resources to create. -func (s *Step) LoadYAML(file string) error { - objects, err := testutils.LoadYAMLFromFile(file) +func (s *Step) LoadYAML(file string, templatingContext testutils.TemplatingContext) error { + objects, err := testutils.LoadYAMLFromFile(file, templatingContext) if err != nil { return fmt.Errorf("loading %s: %s", file, err) } @@ -530,7 +530,7 @@ func (s *Step) LoadYAML(file string) error { // process configured step applies for _, applyPath := range s.Step.Apply { exApply := env.Expand(applyPath) - apply, err := ObjectsFromPath(exApply, s.Dir) + apply, err := ObjectsFromPath(exApply, s.Dir, templatingContext) if err != nil { return fmt.Errorf("step %q apply path %s: %w", s.Name, exApply, err) } @@ -539,7 +539,7 @@ func (s *Step) LoadYAML(file string) error { // process configured step asserts for _, assertPath := range s.Step.Assert { exAssert := env.Expand(assertPath) - assert, err := ObjectsFromPath(exAssert, s.Dir) + assert, err := ObjectsFromPath(exAssert, s.Dir, templatingContext) if err != nil { return fmt.Errorf("step %q assert path %s: %w", s.Name, exAssert, err) } @@ -548,7 +548,7 @@ func (s *Step) LoadYAML(file string) error { // process configured errors for _, errorPath := range s.Step.Error { exError := env.Expand(errorPath) - errObjs, err := ObjectsFromPath(exError, s.Dir) + errObjs, err := ObjectsFromPath(exError, s.Dir, templatingContext) if err != nil { return fmt.Errorf("step %q error path %s: %w", s.Name, exError, err) } @@ -566,7 +566,7 @@ func (s *Step) LoadYAML(file string) error { func (s *Step) populateObjectsByFileName(fileName string, objects []client.Object) error { matches := fileNameRegex.FindStringSubmatch(fileName) if len(matches) < 2 { - return fmt.Errorf("%s does not match file name regexp: %s", fileName, testStepRegex.String()) + return fmt.Errorf("%s does not match file name regexp: %s", fileName, fileNameRegex.String()) } switch fname := strings.ToLower(matches[1]); fname { @@ -590,7 +590,7 @@ func (s *Step) populateObjectsByFileName(fileName string, objects []client.Objec } // ObjectsFromPath returns an array of runtime.Objects for files / urls provided -func ObjectsFromPath(path, dir string) ([]client.Object, error) { +func ObjectsFromPath(path, dir string, templatingContext testutils.TemplatingContext) ([]client.Object, error) { if http.IsURL(path) { apply, err := http.ToObjects(path) if err != nil { @@ -605,7 +605,7 @@ func ObjectsFromPath(path, dir string) ([]client.Object, error) { if err != nil { return nil, fmt.Errorf("failed to find YAML files in %s: %w", cPath, err) } - apply, err := kfile.ToObjects(paths) + apply, err := kfile.ToObjects(paths, templatingContext) if err != nil { return nil, err } diff --git a/pkg/test/step_integration_test.go b/pkg/test/step_integration_test.go index 90fccc5e..a495f66e 100644 --- a/pkg/test/step_integration_test.go +++ b/pkg/test/step_integration_test.go @@ -335,7 +335,7 @@ func TestCheckedTypeAssertions(t *testing.T) { t.Run(test.name, func(t *testing.T) { step := Step{} path := fmt.Sprintf("step_integration_test_data/error_detect/00-%s.yaml", test.name) - assert.EqualError(t, step.LoadYAML(path), + assert.EqualError(t, step.LoadYAML(path, utils.GetTemplatingContext("")), fmt.Sprintf("failed to load %s object from %s: it contains an object of type *unstructured.Unstructured", test.typeName, path)) }) @@ -350,7 +350,7 @@ func TestApplyExpansion(t *testing.T) { step := Step{Dir: "step_integration_test_data/assert_expand/"} path := "step_integration_test_data/assert_expand/00-step1.yaml" - err := step.LoadYAML(path) + err := step.LoadYAML(path, utils.GetTemplatingContext("")) assert.NoError(t, err) assert.Equal(t, 1, len(step.Apply)) @@ -362,12 +362,12 @@ func TestOverriddenKubeconfigPathResolution(t *testing.T) { os.Unsetenv("SUBPATH") }() stepRelativePath := &Step{Dir: "step_integration_test_data/kubeconfig_path_resolution/"} - err := stepRelativePath.LoadYAML("step_integration_test_data/kubeconfig_path_resolution/00-step1.yaml") + err := stepRelativePath.LoadYAML("step_integration_test_data/kubeconfig_path_resolution/00-step1.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) assert.Equal(t, "step_integration_test_data/kubeconfig_path_resolution/kubeconfig-test.yaml", stepRelativePath.Kubeconfig) stepAbsPath := &Step{Dir: "step_integration_test_data/kubeconfig_path_resolution/"} - err = stepAbsPath.LoadYAML("step_integration_test_data/kubeconfig_path_resolution/00-step2.yaml") + err = stepAbsPath.LoadYAML("step_integration_test_data/kubeconfig_path_resolution/00-step2.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) assert.Equal(t, "/absolute/kubeconfig-test.yaml", stepAbsPath.Kubeconfig) } @@ -381,9 +381,9 @@ func TestTwoTestStepping(t *testing.T) { } // 2 apply files in 1 step - err := step.LoadYAML("step_integration_test_data/two_step/00-step1.yaml") + err := step.LoadYAML("step_integration_test_data/two_step/00-step1.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) - err = step.LoadYAML("step_integration_test_data/two_step/00-step2.yaml") + err = step.LoadYAML("step_integration_test_data/two_step/00-step2.yaml", utils.GetTemplatingContext("")) assert.Error(t, err, "more than 1 TestStep not allowed in step \"twostepping\"") // 2 teststeps in 1 file in 1 step @@ -392,7 +392,7 @@ func TestTwoTestStepping(t *testing.T) { Index: 0, Apply: apply, } - err = step.LoadYAML("step_integration_test_data/two_step/01-step1.yaml") + err = step.LoadYAML("step_integration_test_data/two_step/01-step1.yaml", utils.GetTemplatingContext("")) assert.Error(t, err, "more than 1 TestStep not allowed in step \"twostepping\"") } @@ -452,7 +452,7 @@ func TestAssertCommandsValidCommandRunsOk(t *testing.T) { } // Load test that has an echo command, so it should run ok, and don't return any errors - err := step.LoadYAML("step_integration_test_data/assert_commands/valid_command/00-assert.yaml") + err := step.LoadYAML("step_integration_test_data/assert_commands/valid_command/00-assert.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) errors := step.Run("irrelevant") @@ -470,7 +470,7 @@ func TestAssertCommandsMultipleCommandRunsOk(t *testing.T) { } // Load test that has an echo command, so it should run ok, and don't return any errors - err := step.LoadYAML("step_integration_test_data/assert_commands/multiple_commands/00-assert.yaml") + err := step.LoadYAML("step_integration_test_data/assert_commands/multiple_commands/00-assert.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) errors := step.Run("irrelevant") @@ -488,7 +488,7 @@ func TestAssertCommandsMissingCommandFails(t *testing.T) { } // Load test that has an command that is not present (thiscommanddoesnotexist), so it should return an error - err := step.LoadYAML("step_integration_test_data/assert_commands/command_does_not_exist/00-assert.yaml") + err := step.LoadYAML("step_integration_test_data/assert_commands/command_does_not_exist/00-assert.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) errors := step.Run("irrelevant") @@ -506,7 +506,7 @@ func TestAssertCommandsFailingCommandFails(t *testing.T) { } // Load test that has an command that is present but will allways fail (false), so we should get back the error. - err := step.LoadYAML("step_integration_test_data/assert_commands/failing_comand/00-assert.yaml") + err := step.LoadYAML("step_integration_test_data/assert_commands/failing_comand/00-assert.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) errors := step.Run("irrelevant") @@ -525,7 +525,7 @@ func TestAssertCommandsShouldTimeout(t *testing.T) { // Load test that has an command that sleeps for 5 seconds, while the timeout for the step is 1, // so we should get back the error, and the test should run in less slightly more than 1 seconds. - err := step.LoadYAML("step_integration_test_data/assert_commands/timingout_command/00-assert.yaml") + err := step.LoadYAML("step_integration_test_data/assert_commands/timingout_command/00-assert.yaml", utils.GetTemplatingContext("")) assert.NoError(t, err) start := time.Now() diff --git a/pkg/test/utils/kubernetes.go b/pkg/test/utils/kubernetes.go index a6f8483d..71233c41 100644 --- a/pkg/test/utils/kubernetes.go +++ b/pkg/test/utils/kubernetes.go @@ -17,6 +17,7 @@ import ( "strings" "sync" "testing" + "text/template" "time" "github.com/google/shlex" @@ -484,8 +485,25 @@ func MarshalObjectJSON(o runtime.Object, w io.Writer) error { return json.NewSerializer(json.DefaultMetaFactory, nil, nil, false).Encode(copied, w) } +type TemplatingContext struct { + Namespace string + Env map[string]string +} + // LoadYAMLFromFile loads all objects from a YAML file. -func LoadYAMLFromFile(path string) ([]client.Object, error) { +func LoadYAMLFromFile(path string, templatingContext TemplatingContext) ([]client.Object, error) { + if strings.HasSuffix(path, ".gotmpl.yaml") { + tpl, err := template.ParseFiles(path) + if err != nil { + return nil, fmt.Errorf("failed to parse file %q as go template: %w", path, err) + } + b := strings.Builder{} + err = tpl.Execute(&b, templatingContext) + if err != nil { + return nil, fmt.Errorf("failed to execute file %q as go template: %w", path, err) + } + return LoadYAML(path, strings.NewReader(b.String())) + } opened, err := os.Open(path) if err != nil { return nil, err @@ -574,7 +592,7 @@ func InstallManifests(ctx context.Context, c client.Client, dClient discovery.Di return nil } - objs, err := LoadYAMLFromFile(path) + objs, err := LoadYAMLFromFile(path, TemplatingContext{}) if err != nil { return err } @@ -1233,3 +1251,17 @@ func InClusterConfig() (bool, error) { } return false, err } + +func GetTemplatingContext(namespace string) TemplatingContext { + env := make(map[string]string, len(os.Environ())) + for _, setting := range os.Environ() { + if name, val, found := strings.Cut(setting, "="); found { + env[name] = val + } + } + + return TemplatingContext{ + Namespace: namespace, + Env: env, + } +} diff --git a/pkg/test/utils/kubernetes_test.go b/pkg/test/utils/kubernetes_test.go index 7ae05791..816132da 100644 --- a/pkg/test/utils/kubernetes_test.go +++ b/pkg/test/utils/kubernetes_test.go @@ -200,7 +200,7 @@ spec: t.Fatal(err) } - objs, err := LoadYAMLFromFile(tmpfile.Name()) + objs, err := LoadYAMLFromFile(tmpfile.Name(), TemplatingContext{}) assert.Nil(t, err) assert.Equal(t, &unstructured.Unstructured{ @@ -269,7 +269,7 @@ metadata: t.Fatal(err) } - objs, err := LoadYAMLFromFile(tmpfile.Name()) + objs, err := LoadYAMLFromFile(tmpfile.Name(), TemplatingContext{}) assert.Nil(t, err) crd := NewResource("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "", "")