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, + ) + }) + }) + }) })