diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index eee8f06e..9b479b76 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -22,17 +22,18 @@ import ( . "github.com/onsi/gomega" //revive:disable:dot-imports + topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" + ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1" ironic_pkg "github.com/openstack-k8s-operators/ironic-operator/pkg/ironic" ironic_inspector_pkg "github.com/openstack-k8s-operators/ironic-operator/pkg/ironicinspector" + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1" - condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" ) const ( @@ -655,9 +656,11 @@ func CreateUnstructured(rawObj map[string]interface{}) *unstructured.Unstructure return unstructuredObj } -// GetSampleTopologySpec - A sample (and opinionated) Topology Spec used to -// test Ironic components -func GetSampleTopologySpec(svcSelector string) map[string]interface{} { +// GetSampleTopologySpec - An opinionated Topology Spec sample used to +// test Service components. It returns both the user input representation +// in the form of map[string]string, and the Golang expected representation +// used in the test asserts. +func GetSampleTopologySpec(label string) (map[string]interface{}, []corev1.TopologySpreadConstraint) { // Build the topology Spec topologySpec := map[string]interface{}{ "topologySpreadConstraints": []map[string]interface{}{ @@ -667,13 +670,26 @@ func GetSampleTopologySpec(svcSelector string) map[string]interface{} { "whenUnsatisfiable": "ScheduleAnyway", "labelSelector": map[string]interface{}{ "matchLabels": map[string]interface{}{ - "service": svcSelector, + "component": label, }, }, }, }, } - return topologySpec + // Build the topologyObj representation + topologySpecObj := []corev1.TopologySpreadConstraint{ + { + MaxSkew: 1, + TopologyKey: corev1.LabelHostname, + WhenUnsatisfiable: corev1.ScheduleAnyway, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "component": label, + }, + }, + }, + } + return topologySpec, topologySpecObj } // CreateTopology - Creates a Topology CR based on the spec passed as input @@ -689,3 +705,12 @@ func CreateTopology(topology types.NamespacedName, spec map[string]interface{}) } return th.CreateUnstructured(raw) } + +// GetTopology - Returns the referenced Topology +func GetTopology(name types.NamespacedName) *topologyv1.Topology { + instance := &topologyv1.Topology{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} diff --git a/tests/functional/ironic_controller_test.go b/tests/functional/ironic_controller_test.go index c53e1105..368cc0cd 100644 --- a/tests/functional/ironic_controller_test.go +++ b/tests/functional/ironic_controller_test.go @@ -28,6 +28,7 @@ import ( . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" mariadb_test "github.com/openstack-k8s-operators/mariadb-operator/api/test/helpers" @@ -227,11 +228,21 @@ var _ = Describe("Ironic controller", func() { }) When("Ironic is created with topologyref", func() { + var topologyRef, topologyRefAlt *topologyv1.TopoRef BeforeEach(func() { - // Build the topology Spec - topologySpec := GetSampleTopologySpec("ironic") + // Define the two topology references used in this test + topologyRef = &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[0].Name, + Namespace: ironicNames.IronicTopologies[0].Namespace, + } + topologyRefAlt = &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[1].Name, + Namespace: ironicNames.IronicTopologies[1].Namespace, + } // Create Test Topologies for _, t := range ironicNames.IronicTopologies { + // Build the topology Spec + topologySpec, _ := GetSampleTopologySpec(t.Name) CreateTopology(t, topologySpec) } DeferCleanup( @@ -254,7 +265,7 @@ var _ = Describe("Ironic controller", func() { keystone.CreateKeystoneAPI(ironicNames.Namespace)) spec := GetDefaultIronicSpec() spec["topologyRef"] = map[string]interface{}{ - "name": ironicNames.IronicTopologies[0].Name, + "name": topologyRef.Name, } DeferCleanup( th.DeleteInstance, @@ -286,46 +297,96 @@ var _ = Describe("Ironic controller", func() { It("sets topology in CR status", func() { Eventually(func(g Gomega) { + tp := GetTopology(types.NamespacedName{ + Name: topologyRef.Name, + Namespace: topologyRef.Namespace, + }) + finalizers := tp.GetFinalizers() + g.Expect(finalizers).To(HaveLen(4)) ironicAPI := GetIronicAPI(ironicNames.APIName) g.Expect(ironicAPI.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicAPI.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[0].Name)) + g.Expect(ironicAPI.Status.LastAppliedTopology).To(Equal(topologyRef)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicapi-%s", ironicAPI.Name))) + ironicConductor := GetIronicConductor(ironicNames.ConductorName) g.Expect(ironicConductor.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicConductor.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[0].Name)) + g.Expect(ironicConductor.Status.LastAppliedTopology).To(Equal(topologyRef)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicconductor-%s", ironicConductor.Name))) + ironicInspector := GetIronicInspector(ironicNames.InspectorName) g.Expect(ironicInspector.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicInspector.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[0].Name)) + g.Expect(ironicInspector.Status.LastAppliedTopology).To(Equal(topologyRef)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicinspector-%s", ironicInspector.Name))) }, timeout, interval).Should(Succeed()) }) - It("sets nodeSelector in resource specs", func() { + It("sets topology in resource specs", func() { Eventually(func(g Gomega) { + _, expectedTopologySpecObj := GetSampleTopologySpec(topologyRef.Name) g.Expect(th.GetDeployment(ironicNames.IronicName).Spec.Template.Spec.TopologySpreadConstraints).ToNot(BeNil()) + g.Expect(th.GetDeployment(ironicNames.IronicName).Spec.Template.Spec.TopologySpreadConstraints).To(Equal(expectedTopologySpecObj)) g.Expect(th.GetDeployment(ironicNames.IronicName).Spec.Template.Spec.Affinity).To(BeNil()) + g.Expect(th.GetStatefulSet(ironicNames.InspectorName).Spec.Template.Spec.TopologySpreadConstraints).ToNot(BeNil()) + g.Expect(th.GetStatefulSet(ironicNames.InspectorName).Spec.Template.Spec.TopologySpreadConstraints).To(Equal(expectedTopologySpecObj)) g.Expect(th.GetStatefulSet(ironicNames.InspectorName).Spec.Template.Spec.Affinity).To(BeNil()) + + g.Expect(th.GetStatefulSet(ironicNames.ConductorName).Spec.Template.Spec.TopologySpreadConstraints).To(Equal(expectedTopologySpecObj)) g.Expect(th.GetStatefulSet(ironicNames.ConductorName).Spec.Template.Spec.TopologySpreadConstraints).ToNot(BeNil()) g.Expect(th.GetStatefulSet(ironicNames.ConductorName).Spec.Template.Spec.Affinity).To(BeNil()) + g.Expect(th.GetDeployment(ironicNames.INAName).Spec.Template.Spec.TopologySpreadConstraints).ToNot(BeNil()) + g.Expect(th.GetDeployment(ironicNames.INAName).Spec.Template.Spec.TopologySpreadConstraints).To(Equal(expectedTopologySpecObj)) g.Expect(th.GetDeployment(ironicNames.INAName).Spec.Template.Spec.Affinity).To(BeNil()) }, timeout, interval).Should(Succeed()) }) It("updates topology when the reference changes", func() { + expectedTopology := &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[1].Name, + Namespace: ironicNames.IronicTopologies[1].Namespace, + } + var finalizers []string Eventually(func(g Gomega) { ironic := GetIronic(ironicNames.IronicName) - ironic.Spec.TopologyRef.Name = ironicNames.IronicTopologies[1].Name + ironic.Spec.TopologyRef.Name = topologyRefAlt.Name g.Expect(k8sClient.Update(ctx, ironic)).To(Succeed()) }, timeout, interval).Should(Succeed()) Eventually(func(g Gomega) { + tp := GetTopology(types.NamespacedName{ + Name: expectedTopology.Name, + Namespace: expectedTopology.Namespace, + }) + finalizers = tp.GetFinalizers() + g.Expect(finalizers).To(HaveLen(4)) + ironicAPI := GetIronicAPI(ironicNames.APIName) g.Expect(ironicAPI.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicAPI.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[1].Name)) + g.Expect(ironicAPI.Status.LastAppliedTopology).To(Equal(topologyRefAlt)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicapi-%s", ironicAPI.Name))) + ironicConductor := GetIronicConductor(ironicNames.ConductorName) g.Expect(ironicConductor.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicConductor.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[1].Name)) + g.Expect(ironicConductor.Status.LastAppliedTopology).To(Equal(topologyRefAlt)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicconductor-%s", ironicConductor.Name))) + ironicInspector := GetIronicInspector(ironicNames.InspectorName) g.Expect(ironicInspector.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicInspector.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[1].Name)) + g.Expect(ironicInspector.Status.LastAppliedTopology).To(Equal(topologyRefAlt)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicinspector-%s", ironicInspector.Name))) + + // Get the previous topology and verify there are no finalizers + // anymore + tp = GetTopology(types.NamespacedName{ + Name: topologyRef.Name, + Namespace: topologyRef.Namespace, + }) + g.Expect(tp.GetFinalizers()).To(BeEmpty()) }, timeout, interval).Should(Succeed()) }) It("overrides topology when the reference changes", func() { @@ -347,15 +408,57 @@ var _ = Describe("Ironic controller", func() { }, timeout, interval).Should(Succeed()) Eventually(func(g Gomega) { + expectedTopology := &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[1].Name, + Namespace: ironicNames.IronicTopologies[1].Namespace, + } + tp := GetTopology(types.NamespacedName{ + Name: expectedTopology.Name, + Namespace: expectedTopology.Namespace, + }) + g.Expect(tp.GetFinalizers()).To(HaveLen(1)) + finalizers := tp.GetFinalizers() ironicAPI := GetIronicAPI(ironicNames.APIName) g.Expect(ironicAPI.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicAPI.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[1].Name)) + g.Expect(ironicAPI.Status.LastAppliedTopology).To(Equal(expectedTopology)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicapi-%s", ironicAPI.Name))) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + expectedTopology := &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[2].Name, + Namespace: ironicNames.IronicTopologies[2].Namespace, + } + tp := GetTopology(types.NamespacedName{ + Name: expectedTopology.Name, + Namespace: expectedTopology.Namespace, + }) + g.Expect(tp.GetFinalizers()).To(HaveLen(1)) + finalizers := tp.GetFinalizers() ironicConductor := GetIronicConductor(ironicNames.ConductorName) g.Expect(ironicConductor.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicConductor.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[2].Name)) + g.Expect(ironicConductor.Status.LastAppliedTopology).To(Equal(expectedTopology)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicconductor-%s", ironicConductor.Name))) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + expectedTopology := &topologyv1.TopoRef{ + Name: ironicNames.IronicTopologies[3].Name, + Namespace: ironicNames.IronicTopologies[3].Namespace, + } + tp := GetTopology(types.NamespacedName{ + Name: expectedTopology.Name, + Namespace: expectedTopology.Namespace, + }) + g.Expect(tp.GetFinalizers()).To(HaveLen(1)) + finalizers := tp.GetFinalizers() ironicInspector := GetIronicInspector(ironicNames.InspectorName) g.Expect(ironicInspector.Status.LastAppliedTopology).ToNot(BeNil()) - g.Expect(ironicInspector.Status.LastAppliedTopology.Name).To(Equal(ironicNames.IronicTopologies[3].Name)) + g.Expect(ironicInspector.Status.LastAppliedTopology).To(Equal(expectedTopology)) + g.Expect(finalizers).To(ContainElement( + fmt.Sprintf("openstack.org/ironicinspector-%s", ironicInspector.Name))) }, timeout, interval).Should(Succeed()) }) It("removes topologyRef from the spec", func() { @@ -385,6 +488,17 @@ var _ = Describe("Ironic controller", func() { g.Expect(th.GetDeployment(ironicNames.INAName).Spec.Template.Spec.TopologySpreadConstraints).To(BeNil()) g.Expect(th.GetDeployment(ironicNames.INAName).Spec.Template.Spec.Affinity).ToNot(BeNil()) }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + for _, topology := range ironicNames.IronicTopologies { + // Get the current topology and verify there are no finalizers + tp := GetTopology(types.NamespacedName{ + Name: topology.Name, + Namespace: topology.Namespace, + }) + g.Expect(tp.GetFinalizers()).To(BeEmpty()) + } + }, timeout, interval).Should(Succeed()) }) }) When("Ironic is created with nodeSelector", func() {