From ec4d671c4092c10a325a4400127939ec13f49556 Mon Sep 17 00:00:00 2001 From: Katarina Strenkova Date: Tue, 6 Oct 2026 15:55:04 +0200 Subject: [PATCH] Add envtests for workflow feature This PR adds Tempest, Tobiko and AnsibleTest envtests for workflow feature. It covers workflow step resource creation, spec override precedence, networkAttachments handling, inheritance fallback when workflow steps omit values, and resource creation in case of multiple steps. --- .../functional/ansibletest_controller_test.go | 129 ++++++++++++ test/functional/base_test.go | 83 ++++++++ test/functional/tempest_controller_test.go | 188 ++++++++++++++++++ test/functional/tobiko_controller_test.go | 174 ++++++++++++++++ 4 files changed, 574 insertions(+) diff --git a/test/functional/ansibletest_controller_test.go b/test/functional/ansibletest_controller_test.go index 0c6cbca6..300a2b21 100644 --- a/test/functional/ansibletest_controller_test.go +++ b/test/functional/ansibletest_controller_test.go @@ -25,6 +25,7 @@ import ( . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("AnsibleTest controller", func() { @@ -230,4 +231,132 @@ var _ = Describe("AnsibleTest controller", func() { }) + Context("workflow", func() { + When("is created", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateAnsibleTest(ansibleTestName, GetDefaultAnsibleTestWorkflowSpec())) + }) + + It("creates PVC with workflow step name", func() { + pvc := GetTestOperatorPVC(namespace, ansibleTestName.Name) + Expect(pvc.Name).To(ContainSubstring(ansibleTestName.Name + "-0-")) + }) + + It("creates pod with workflow step name", func() { + pod := GetTestOperatorPod(namespace, ansibleTestName.Name) + spec := GetDefaultAnsibleTestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + stepName := workflow[0]["stepName"].(string) + Expect(pod.Name).To(Equal(ansibleTestName.Name + "-s00-" + stepName)) + }) + }) + + When("overrides spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateAnsibleTest(ansibleTestName, GetDefaultAnsibleTestWorkflowSpec())) + }) + + It("workflow values take precedence", func() { + pod := GetTestOperatorPod(namespace, ansibleTestName.Name) + spec := GetDefaultAnsibleTestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + expectedRepo := workflow[0]["ansibleGitRepo"].(string) + expectedPlaybook := workflow[0]["ansiblePlaybookPath"].(string) + + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_GIT_REPO")).To(Equal(expectedRepo)) + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_GIT_REPO")).NotTo(Equal(spec["ansibleGitRepo"].(string))) + + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_PLAYBOOK")).To(Equal(expectedPlaybook)) + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_PLAYBOOK")).NotTo(Equal(spec["ansiblePlaybookPath"].(string))) + }) + }) + + When("inherits from spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + spec := GetDefaultAnsibleTestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + delete(workflow[0], "ansibleGitRepo") + delete(workflow[0], "ansiblePlaybookPath") + + DeferCleanup(th.DeleteInstance, CreateAnsibleTest(ansibleTestName, spec)) + }) + + It("uses spec-level values when workflow step omits them", func() { + pod := GetTestOperatorPod(namespace, ansibleTestName.Name) + spec := GetDefaultAnsibleTestWorkflowSpec() + expectedRepo := spec["ansibleGitRepo"].(string) + expectedPlaybook := spec["ansiblePlaybookPath"].(string) + + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_GIT_REPO")).To(Equal(expectedRepo)) + Expect(GetPodEnvVar(pod, "POD_ANSIBLE_PLAYBOOK")).To(Equal(expectedPlaybook)) + }) + }) + + When("with multiple workflow steps", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateAnsibleTest(ansibleTestName, GetDefaultAnsibleTestWorkflowSpec())) + }) + + It("creates second pod after first pod succeeds", func() { + firstPod := GetTestOperatorPod(namespace, ansibleTestName.Name) + Expect(firstPod.Name).To(Equal(ansibleTestName.Name + "-s00-first-step")) + + firstPod.Status.Phase = corev1.PodSucceeded + Expect(k8sClient.Status().Update(ctx, firstPod)).Should(Succeed()) + + Eventually(func(g Gomega) { + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + "instanceName": ansibleTestName.Name, + "operator": "test-operator", + "workflowStep": "1", + }, + } + g.Expect(k8sClient.List(ctx, podList, listOpts...)).Should(Succeed()) + g.Expect(podList.Items).To(HaveLen(1)) + secondPod := podList.Items[0] + g.Expect(secondPod.Name).To(Equal(ansibleTestName.Name + "-s01-second-step")) + + spec := GetDefaultAnsibleTestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + expectedRepo := workflow[1]["ansibleGitRepo"].(string) + expectedPlaybook := workflow[1]["ansiblePlaybookPath"].(string) + + g.Expect(GetPodEnvVar(&secondPod, "POD_ANSIBLE_GIT_REPO")).To(Equal(expectedRepo)) + g.Expect(GetPodEnvVar(&secondPod, "POD_ANSIBLE_PLAYBOOK")).To(Equal(expectedPlaybook)) + }, timeout, interval).Should(Succeed()) + }) + }) + }) + }) diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 654eb1db..b3167815 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -213,6 +213,17 @@ func ExpectPodNotHasVolumeMount(pod *corev1.Pod, volName string) { } } +func GetPodEnvVar(pod *corev1.Pod, envName string) string { + for _, container := range pod.Spec.Containers { + for _, env := range container.Env { + if env.Name == envName { + return env.Value + } + } + } + return "" +} + func GetDefaultConfigMapExtraMount() ExtraMount { return ExtraMount{ VolName: ExtraConfigVolName, @@ -296,6 +307,28 @@ func GetDefaultAnsibleTestSpec() map[string]any { } } +func GetDefaultAnsibleTestWorkflowSpec() map[string]any { + return map[string]any{ + "storageClass": DefaultStorageClass, + "ansibleGitRepo": "https://github.com/example/test-repo", + "ansiblePlaybookPath": "tests/playbook.yaml", + "workflow": []map[string]any{ + { + "stepName": "first-step", + "storageClass": DefaultStorageClass, + "ansibleGitRepo": "https://github.com/example/workflow-repo", + "ansiblePlaybookPath": "tests/workflow-playbook.yaml", + }, + { + "stepName": "second-step", + "storageClass": DefaultStorageClass, + "ansibleGitRepo": "https://github.com/example/second-repo", + "ansiblePlaybookPath": "tests/second-playbook.yaml", + }, + }, + } +} + func AnsibleTestConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetAnsibleTest(name) return instance.Status.Conditions @@ -369,6 +402,37 @@ func GetDefaultTempestSpec() map[string]any { } } +func GetDefaultTempestWorkflowSpec() map[string]any { + return map[string]any{ + "storageClass": DefaultStorageClass, + "tempestRun": map[string]any{ + "includeList": "tempest.api.identity.v3.*", + }, + "tempestconfRun": map[string]any{ + "networkID": "spec-network-id", + }, + "workflow": []map[string]any{ + { + "stepName": "first-step", + "storageClass": DefaultStorageClass, + "tempestRun": map[string]any{ + "includeList": "tempest.api.compute.*", + }, + "tempestconfRun": map[string]any{ + "networkID": "workflow-network-id", + }, + }, + { + "stepName": "second-step", + "storageClass": DefaultStorageClass, + "tempestRun": map[string]any{ + "includeList": "tempest.api.network.*", + }, + }, + }, + } +} + func TempestConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetTempest(name) return instance.Status.Conditions @@ -403,6 +467,25 @@ func GetDefaultTobikoSpec() map[string]any { } } +func GetDefaultTobikoWorkflowSpec() map[string]any { + return map[string]any{ + "storageClass": DefaultStorageClass, + "testenv": "sanity", + "workflow": []map[string]any{ + { + "stepName": "first-step", + "storageClass": DefaultStorageClass, + "testenv": "compute", + }, + { + "stepName": "second-step", + "storageClass": DefaultStorageClass, + "testenv": "network", + }, + }, + } +} + func TobikoConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetTobiko(name) return instance.Status.Conditions diff --git a/test/functional/tempest_controller_test.go b/test/functional/tempest_controller_test.go index db467c57..bfd89281 100644 --- a/test/functional/tempest_controller_test.go +++ b/test/functional/tempest_controller_test.go @@ -28,6 +28,7 @@ import ( . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("Tempest controller", func() { @@ -164,6 +165,7 @@ var _ = Describe("Tempest controller", func() { It("should add network annotation to pod", func() { pod := GetTestOperatorPod(namespace, tempestName.Name) Expect(pod.Annotations).To(HaveKey("k8s.v1.cni.cncf.io/networks")) + Expect(pod.Annotations["k8s.v1.cni.cncf.io/networks"]).To(ContainSubstring(networkAttachmentName)) }) }) @@ -293,4 +295,190 @@ var _ = Describe("Tempest controller", func() { }) }) }) + + Context("workflow", func() { + When("is created", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, GetDefaultTempestWorkflowSpec())) + }) + + It("creates resources with workflow step name", func() { + customDataCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-custom-data-s0", tempestName.Name), + }) + Expect(customDataCM.Data).To(HaveKey("include.txt")) + + envVarsCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-env-vars-s0", tempestName.Name), + }) + Expect(envVarsCM.Data).NotTo(BeEmpty()) + }) + + It("creates PVC with workflow step name", func() { + pvc := GetTestOperatorPVC(namespace, tempestName.Name) + Expect(pvc.Name).To(ContainSubstring(tempestName.Name + "-0-")) + }) + + It("creates pod with workflow step name", func() { + pod := GetTestOperatorPod(namespace, tempestName.Name) + spec := GetDefaultTempestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + stepName := workflow[0]["stepName"].(string) + Expect(pod.Name).To(Equal(tempestName.Name + "-s00-" + stepName)) + }) + }) + + When("overrides spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, GetDefaultTempestWorkflowSpec())) + }) + + It("workflow tempestRun values take precedence", func() { + customDataCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-custom-data-s0", tempestName.Name), + }) + Expect(customDataCM.Data["include.txt"]).To(ContainSubstring("tempest.api.compute.*")) + Expect(customDataCM.Data["include.txt"]).NotTo(ContainSubstring("tempest.api.identity.v3.*")) + }) + + It("workflow tempestconfRun values take precedence", func() { + envVarsCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-env-vars-s0", tempestName.Name), + }) + Expect(envVarsCM.Data["TEMPESTCONF_NETWORK_ID"]).To(Equal("workflow-network-id")) + Expect(envVarsCM.Data["TEMPESTCONF_NETWORK_ID"]).NotTo(Equal("spec-network-id")) + }) + }) + + When("inherits from spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + spec := GetDefaultTempestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + delete(workflow[0], "tempestRun") + delete(workflow[0], "tempestconfRun") + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, spec)) + }) + + It("uses spec-level values when workflow step omits them", func() { + customDataCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-custom-data-s0", tempestName.Name), + }) + Expect(customDataCM.Data["include.txt"]).To(ContainSubstring("tempest.api.identity.v3.*")) + + envVarsCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-env-vars-s0", tempestName.Name), + }) + Expect(envVarsCM.Data["TEMPESTCONF_NETWORK_ID"]).To(Equal("spec-network-id")) + }) + }) + + When("with multiple workflow steps", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, GetDefaultTempestWorkflowSpec())) + }) + + It("creates second pod after first pod succeeds", func() { + firstPod := GetTestOperatorPod(namespace, tempestName.Name) + Expect(firstPod.Name).To(Equal(tempestName.Name + "-s00-first-step")) + + firstPod.Status.Phase = corev1.PodSucceeded + Expect(k8sClient.Status().Update(ctx, firstPod)).Should(Succeed()) + + Eventually(func(g Gomega) { + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + "instanceName": tempestName.Name, + "operator": "test-operator", + "workflowStep": "1", + }, + } + g.Expect(k8sClient.List(ctx, podList, listOpts...)).Should(Succeed()) + g.Expect(podList.Items).To(HaveLen(1)) + g.Expect(podList.Items[0].Name).To(Equal(tempestName.Name + "-s01-second-step")) + + customDataCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-custom-data-s1", tempestName.Name), + }) + g.Expect(customDataCM.Data["include.txt"]).To(ContainSubstring("tempest.api.network.*")) + + envVarsCM := th.GetConfigMap(types.NamespacedName{ + Namespace: namespace, + Name: fmt.Sprintf("%s-env-vars-s1", tempestName.Name), + }) + g.Expect(envVarsCM.Data["TEMPESTCONF_NETWORK_ID"]).To(Equal("spec-network-id")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("with networkAttachments", func() { + var networkAttachmentName = "ctlplane" + + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + nad := th.CreateNetworkAttachmentDefinition(types.NamespacedName{ + Namespace: namespace, + Name: networkAttachmentName, + }) + DeferCleanup(th.DeleteInstance, nad) + + spec := GetDefaultTempestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + workflow[0]["networkAttachments"] = []string{networkAttachmentName} + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, spec)) + }) + + It("adds network annotation to workflow pod", func() { + pod := GetTestOperatorPod(namespace, tempestName.Name) + Expect(pod.Annotations).To(HaveKey("k8s.v1.cni.cncf.io/networks")) + Expect(pod.Annotations["k8s.v1.cni.cncf.io/networks"]).To(ContainSubstring(networkAttachmentName)) + }) + }) + + When("with non-existent networkAttachments", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + spec := GetDefaultTempestWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + workflow[0]["networkAttachments"] = []string{"non-existent-nad"} + DeferCleanup(th.DeleteInstance, CreateTempest(tempestName, spec)) + }) + + It("should set NetworkAttachmentsReady to false", func() { + th.ExpectCondition( + tempestName, + ConditionGetterFunc(TempestConditionGetter), + condition.NetworkAttachmentsReadyCondition, + corev1.ConditionFalse, + ) + }) + }) + }) }) diff --git a/test/functional/tobiko_controller_test.go b/test/functional/tobiko_controller_test.go index 6138b01e..ed53c453 100644 --- a/test/functional/tobiko_controller_test.go +++ b/test/functional/tobiko_controller_test.go @@ -25,6 +25,7 @@ import ( . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("Tobiko controller", func() { @@ -148,6 +149,7 @@ var _ = Describe("Tobiko controller", func() { It("should add network annotation to pod", func() { pod := GetTestOperatorPod(namespace, tobikoName.Name) Expect(pod.Annotations).To(HaveKey("k8s.v1.cni.cncf.io/networks")) + Expect(pod.Annotations["k8s.v1.cni.cncf.io/networks"]).To(ContainSubstring(networkAttachmentName)) }) }) @@ -280,4 +282,176 @@ var _ = Describe("Tobiko controller", func() { }) }) }) + + Context("workflow", func() { + When("is created", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, GetDefaultTobikoWorkflowSpec())) + }) + + It("creates PVC with workflow step name", func() { + pvc := GetTestOperatorPVC(namespace, tobikoName.Name) + Expect(pvc.Name).To(ContainSubstring(tobikoName.Name + "-0-")) + }) + + It("creates pod with workflow step name", func() { + pod := GetTestOperatorPod(namespace, tobikoName.Name) + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + stepName := workflow[0]["stepName"].(string) + Expect(pod.Name).To(Equal(tobikoName.Name + "-s00-" + stepName)) + }) + }) + + When("overrides spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, GetDefaultTobikoWorkflowSpec())) + }) + + It("workflow testenv values take precedence", func() { + pod := GetTestOperatorPod(namespace, tobikoName.Name) + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + expectedTestenv := workflow[0]["testenv"].(string) + + Expect(GetPodEnvVar(pod, "TOBIKO_TESTENV")).To(Equal(expectedTestenv)) + Expect(GetPodEnvVar(pod, "TOBIKO_TESTENV")).NotTo(Equal(spec["testenv"].(string))) + }) + }) + + When("inherits from spec defaults", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + delete(workflow[0], "testenv") + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, spec)) + }) + + It("uses spec-level values when workflow step omits them", func() { + pod := GetTestOperatorPod(namespace, tobikoName.Name) + spec := GetDefaultTobikoWorkflowSpec() + expectedTestenv := spec["testenv"].(string) + + Expect(GetPodEnvVar(pod, "TOBIKO_TESTENV")).To(Equal(expectedTestenv)) + }) + }) + + When("with multiple workflow steps", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, GetDefaultTobikoWorkflowSpec())) + }) + + It("creates second pod after first pod succeeds", func() { + firstPod := GetTestOperatorPod(namespace, tobikoName.Name) + Expect(firstPod.Name).To(Equal(tobikoName.Name + "-s00-first-step")) + + firstPod.Status.Phase = corev1.PodSucceeded + Expect(k8sClient.Status().Update(ctx, firstPod)).Should(Succeed()) + + Eventually(func(g Gomega) { + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + "instanceName": tobikoName.Name, + "operator": "test-operator", + "workflowStep": "1", + }, + } + g.Expect(k8sClient.List(ctx, podList, listOpts...)).Should(Succeed()) + g.Expect(podList.Items).To(HaveLen(1)) + secondPod := podList.Items[0] + g.Expect(secondPod.Name).To(Equal(tobikoName.Name + "-s01-second-step")) + + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + expectedTestenv := workflow[1]["testenv"].(string) + g.Expect(GetPodEnvVar(&secondPod, "TOBIKO_TESTENV")).To(Equal(expectedTestenv)) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("with networkAttachments", func() { + var networkAttachmentName = "ctlplane" + + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + nad := th.CreateNetworkAttachmentDefinition(types.NamespacedName{ + Namespace: namespace, + Name: networkAttachmentName, + }) + DeferCleanup(th.DeleteInstance, nad) + + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + workflow[0]["networkAttachments"] = []string{networkAttachmentName} + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, spec)) + }) + + It("adds network annotation to workflow pod", func() { + pod := GetTestOperatorPod(namespace, tobikoName.Name) + Expect(pod.Annotations).To(HaveKey("k8s.v1.cni.cncf.io/networks")) + Expect(pod.Annotations["k8s.v1.cni.cncf.io/networks"]).To(ContainSubstring(networkAttachmentName)) + }) + }) + + When("with non-existent networkAttachments", func() { + BeforeEach(func() { + openstackConfigMap, openstackSecret := CreateCommonOpenstackResources(namespace) + Expect(k8sClient.Create(ctx, openstackConfigMap)).Should(Succeed()) + Expect(k8sClient.Create(ctx, openstackSecret)).Should(Succeed()) + + testOperatorConfigMap := CreateTestOperatorConfigMap(namespace) + Expect(k8sClient.Create(ctx, testOperatorConfigMap)).Should(Succeed()) + + spec := GetDefaultTobikoWorkflowSpec() + workflow := spec["workflow"].([]map[string]any) + workflow[0]["networkAttachments"] = []string{"non-existent-nad"} + DeferCleanup(th.DeleteInstance, CreateTobiko(tobikoName, spec)) + }) + + It("should set NetworkAttachmentsReady to false", func() { + th.ExpectCondition( + tobikoName, + ConditionGetterFunc(TobikoConditionGetter), + condition.NetworkAttachmentsReadyCondition, + corev1.ConditionFalse, + ) + }) + }) + }) })