From 918b6d3bf093bffffb119810b3d21103dc88b2fa Mon Sep 17 00:00:00 2001 From: Francesco Pantano Date: Tue, 6 Jun 2023 10:25:54 +0200 Subject: [PATCH] Improve basic functional test As a follow up of PR#239, this patch is supposed to improve the envTest coverage, providing more tests and the basic data structures required to simplify the logic. Signed-off-by: Francesco Pantano --- Makefile | 2 +- go.mod | 3 +- go.sum | 2 + tests/functional/base_test.go | 134 ++++++++- tests/functional/glance_controller_test.go | 301 +++++++++++++++++++-- tests/functional/glance_test_data.go | 125 +++++++++ tests/functional/suite_test.go | 59 +++- 7 files changed, 590 insertions(+), 36 deletions(-) create mode 100644 tests/functional/glance_test_data.go diff --git a/Makefile b/Makefile index e0da8fd9..4f1ae71c 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ golangci-lint: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.51.2 $(LOCALBIN)/golangci-lint run --fix -PROCS?=$(shell expr $(shell nproc --ignore 2) / 2) +PROCS?=$(shell expr $(shell nproc --ignore 2) / 4) PROC_CMD = --procs ${PROCS} .PHONY: test diff --git a/go.mod b/go.mod index 2b1812bf..f4486b00 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/openstack-k8s-operators/lib-common/modules/common v0.1.0 github.com/openstack-k8s-operators/lib-common/modules/database v0.1.0 github.com/openstack-k8s-operators/lib-common/modules/storage v0.1.0 - github.com/openstack-k8s-operators/lib-common/modules/test v0.1.1 github.com/openstack-k8s-operators/mariadb-operator/api v0.1.0 k8s.io/api v0.26.7 k8s.io/apimachinery v0.26.7 @@ -24,11 +23,13 @@ require ( require ( github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 github.com/openstack-k8s-operators/cinder-operator/api v0.1.0 + github.com/openstack-k8s-operators/lib-common/modules/test v0.1.1 ) require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/tools v0.9.3 // indirect ) diff --git a/go.sum b/go.sum index 9accbd72..afd76bac 100644 --- a/go.sum +++ b/go.sum @@ -349,6 +349,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 125685bb..3fcd619e 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -14,13 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package functional_test +package functional import ( + k8s_errors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" @@ -34,12 +36,25 @@ func GetGlance(name types.NamespacedName) *glancev1.Glance { return instance } +func GetGlanceAPI(name types.NamespacedName) *glancev1.GlanceAPI { + instance := &glancev1.GlanceAPI{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + func GlanceConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetGlance(name) return instance.Status.Conditions } -func CreateGlance(name types.NamespacedName) client.Object { +func GlanceAPIConditionGetter(name types.NamespacedName) condition.Conditions { + instance := GetGlanceAPI(name) + return instance.Status.Conditions +} + +func CreateDefaultGlance(name types.NamespacedName) client.Object { raw := map[string]interface{}{ "apiVersion": "glance.openstack.org/v1beta1", "kind": "Glance", @@ -49,8 +64,121 @@ func CreateGlance(name types.NamespacedName) client.Object { }, "spec": map[string]interface{}{ "databaseInstance": "openstack", - "storageRequest": "10G", + "storageRequest": glanceTest.GlancePVCSize, }, } return th.CreateUnstructured(raw) } + +func GetGlanceEmptySpec() map[string]interface{} { + return map[string]interface{}{ + "secret": SecretName, + "spec": map[string]interface{}{ + "databaseInstance": "openstack", + "storageRequest": glanceTest.GlancePVCSize, + }, + } +} + +func GetGlanceDefaultSpec() map[string]interface{} { + return map[string]interface{}{ + "databaseInstance": "openstack", + "databaseUser": glanceTest.GlanceDatabaseUser, + "serviceUser": glanceName.Name, + "secret": SecretName, + "glanceAPIInternal": GetGlanceAPIDefaultSpec(GlanceAPITypeInternal), + "glanceAPIExternal": GetGlanceAPIDefaultSpec(GlanceAPITypeExternal), + "storageRequest": glanceTest.GlancePVCSize, + } +} + +func GetGlanceDefaultSpecWithQuota() map[string]interface{} { + return map[string]interface{}{ + "databaseInstance": "openstack", + "databaseUser": glanceTest.GlanceDatabaseUser, + "serviceUser": glanceName.Name, + "secret": SecretName, + "glanceAPIInternal": GetGlanceAPIDefaultSpec(GlanceAPITypeInternal), + "glanceAPIExternal": GetGlanceAPIDefaultSpec(GlanceAPITypeExternal), + "storageRequest": glanceTest.GlancePVCSize, + "quotas": glanceTest.GlanceQuotas, + } +} + +func GetGlanceAPIDefaultSpec(apiType APIType) map[string]interface{} { + return map[string]interface{}{ + "replicas": 1, + } +} + +func CreateGlance(name types.NamespacedName, spec map[string]interface{}) client.Object { + + raw := map[string]interface{}{ + "apiVersion": "glance.openstack.org/v1beta1", + "kind": "Glance", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + return th.CreateUnstructured(raw) +} + +func CreateGlanceAPI(name types.NamespacedName, spec map[string]interface{}) client.Object { + raw := map[string]interface{}{ + "apiVersion": "glance.openstack.org/v1beta1", + "kind": "GlanceAPI", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + return th.CreateUnstructured(raw) +} + +func CreateGlanceSecret(namespace string, name string) *corev1.Secret { + return th.CreateSecret( + types.NamespacedName{Namespace: namespace, Name: name}, + map[string][]byte{ + "GlancePassword": []byte(glanceTest.GlancePassword), + "GlanceDatabasePassword": []byte(glanceTest.GlancePassword), + }, + ) +} + +func GetDefaultGlanceSpec() map[string]interface{} { + return map[string]interface{}{ + "databaseInstance": "openstack", + "secret": SecretName, + "glanceAPIInternal": GetDefaultGlanceAPISpec(), + "glanceAPIExternal": GetDefaultGlanceAPISpec(), + } +} + +func GetDefaultGlanceAPISpec() map[string]interface{} { + return map[string]interface{}{ + "secret": SecretName, + "replicas": 1, + "containerImage": "test://glance", + "serviceAccount": glanceTest.GlanceSA.Name, + "databaseInstance": "openstack", + } +} + +func GlanceAPINotExists(name types.NamespacedName) { + Consistently(func(g Gomega) { + instance := &glancev1.GlanceAPI{} + err := k8sClient.Get(ctx, name, instance) + g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue()) + }, timeout, interval).Should(Succeed()) +} + +func GlanceAPIExists(name types.NamespacedName) { + Consistently(func(g Gomega) { + instance := &glancev1.GlanceAPI{} + err := k8sClient.Get(ctx, name, instance) + g.Expect(k8s_errors.IsNotFound(err)).To(BeFalse()) + }, timeout, interval).Should(Succeed()) +} diff --git a/tests/functional/glance_controller_test.go b/tests/functional/glance_controller_test.go index 6ae0c842..7a36166a 100644 --- a/tests/functional/glance_controller_test.go +++ b/tests/functional/glance_controller_test.go @@ -14,44 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -package functional_test +package functional import ( - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" -) - -var ( - namespace string - glanceName types.NamespacedName + util "github.com/openstack-k8s-operators/lib-common/modules/common/util" ) var _ = Describe("Glance controller", func() { When("Glance is created", func() { BeforeEach(func() { - namespace = uuid.New().String() - th.CreateNamespace(namespace) - DeferCleanup(th.DeleteNamespace, namespace) - - glanceName = types.NamespacedName{Namespace: namespace, Name: "glance"} - DeferCleanup(th.DeleteInstance, CreateGlance(glanceName)) - + DeferCleanup(th.DeleteInstance, CreateDefaultGlance(glanceName)) }) It("initializes the status fields", func() { Eventually(func(g Gomega) { glance := GetGlance(glanceName) g.Expect(glance.Status.Conditions).To(HaveLen(11)) - + g.Expect(glance.Status.DatabaseHostname).To(Equal("")) + g.Expect(glance.Status.APIEndpoints).To(BeEmpty()) + g.Expect(glance.Status.GlanceAPIExternalReadyCount).To(Equal(int32(0))) + g.Expect(glance.Status.GlanceAPIInternalReadyCount).To(Equal(int32(0))) }, timeout, interval).Should(Succeed()) }) @@ -66,6 +56,32 @@ var _ = Describe("Glance controller", func() { ) }) + It("initializes Spec fields", func() { + Glance := GetGlance(glanceTest.Instance) + Expect(Glance.Spec.DatabaseInstance).Should(Equal("openstack")) + Expect(Glance.Spec.DatabaseUser).Should(Equal(glanceTest.GlanceDatabaseUser)) + Expect(Glance.Spec.ServiceUser).Should(Equal(glanceTest.GlanceServiceUser)) + // No Keystone Quota is present, check the default is 0 + Expect(Glance.Spec.Quotas.ImageCountUpload).To(Equal(int(0))) + Expect(Glance.Spec.Quotas.ImageSizeTotal).To(Equal(int(0))) + Expect(Glance.Spec.Quotas.ImageCountTotal).To(Equal(int(0))) + Expect(Glance.Spec.Quotas.ImageStageTotal).To(Equal(int(0))) + }) + + It("should have a finalizer", func() { + // the reconciler loop adds the finalizer so we have to wait for + // it to run + Eventually(func() []string { + return GetGlance(glanceTest.Instance).Finalizers + }, timeout, interval).Should(ContainElement("Glance")) + }) + + It("should not create a config map", func() { + Eventually(func() []corev1.ConfigMap { + return th.ListConfigMaps(glanceTest.GlanceConfigMapData.Name).Items + }, timeout, interval).Should(BeEmpty()) + }) + It("creates service account, role and rolebindig", func() { th.ExpectCondition( glanceName, @@ -73,7 +89,7 @@ var _ = Describe("Glance controller", func() { condition.ServiceAccountReadyCondition, corev1.ConditionTrue, ) - sa := th.GetServiceAccount(types.NamespacedName{Namespace: namespace, Name: "glance-" + glanceName.Name}) + sa := th.GetServiceAccount(glanceTest.GlanceSA) th.ExpectCondition( glanceName, @@ -81,7 +97,7 @@ var _ = Describe("Glance controller", func() { condition.RoleReadyCondition, corev1.ConditionTrue, ) - role := th.GetRole(types.NamespacedName{Namespace: namespace, Name: "glance-" + glanceName.Name + "-role"}) + role := th.GetRole(glanceTest.GlanceRole) Expect(role.Rules).To(HaveLen(2)) Expect(role.Rules[0].Resources).To(Equal([]string{"securitycontextconstraints"})) Expect(role.Rules[1].Resources).To(Equal([]string{"pods"})) @@ -92,7 +108,7 @@ var _ = Describe("Glance controller", func() { condition.RoleBindingReadyCondition, corev1.ConditionTrue, ) - binding := th.GetRoleBinding(types.NamespacedName{Namespace: namespace, Name: "glance-" + glanceName.Name + "-rolebinding"}) + binding := th.GetRoleBinding(glanceTest.GlanceRoleBinding) Expect(binding.RoleRef.Name).To(Equal(role.Name)) Expect(binding.Subjects).To(HaveLen(1)) Expect(binding.Subjects[0].Name).To(Equal(sa.Name)) @@ -104,6 +120,251 @@ var _ = Describe("Glance controller", func() { Expect(glance.Spec.GlanceAPIInternal.ContainerImage).To(Equal(glancev1.GlanceAPIContainerImage)) Expect(glance.Spec.GlanceAPIExternal.ContainerImage).To(Equal(glancev1.GlanceAPIContainerImage)) }) + }) + When("Glance DB is created", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, GetGlanceDefaultSpec())) + DeferCleanup( + th.DeleteDBService, + th.CreateDBService( + glanceName.Namespace, + GetGlance(glanceTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + }) + It("Should set DBReady Condition and set DatabaseHostname Status when DB is Created", func() { + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobSuccess(glanceTest.GlanceDBSync) + Glance := GetGlance(glanceTest.Instance) + Expect(Glance.Status.DatabaseHostname).To(Equal("hostname-for-openstack")) + th.ExpectCondition( + glanceName, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBReadyCondition, + corev1.ConditionTrue, + ) + th.ExpectCondition( + glanceName, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBSyncReadyCondition, + corev1.ConditionFalse, + ) + }) + It("Should fail if db-sync job fails when DB is Created", func() { + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobFailure(glanceTest.GlanceDBSync) + th.ExpectCondition( + glanceTest.Instance, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBReadyCondition, + corev1.ConditionTrue, + ) + th.ExpectCondition( + glanceTest.Instance, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBSyncReadyCondition, + corev1.ConditionFalse, + ) + }) + It("Does not create GlanceAPI", func() { + GlanceAPINotExists(glanceTest.GlanceInternal) + GlanceAPINotExists(glanceTest.GlanceInternal) + }) + }) + When("Glance DB is created and db-sync Job succeeded", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, GetGlanceDefaultSpec())) + DeferCleanup( + th.DeleteDBService, + th.CreateDBService( + glanceName.Namespace, + GetGlance(glanceTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobSuccess(glanceTest.GlanceDBSync) + keystoneAPI := th.CreateKeystoneAPI(glanceTest.Instance.Namespace) + DeferCleanup(th.DeleteKeystoneAPI, keystoneAPI) + th.SimulateKeystoneServiceReady(glanceTest.Instance) + }) + It("Glance DB is Ready and db-sync reports ReadyCondition", func() { + th.ExpectCondition( + glanceTest.Instance, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBReadyCondition, + corev1.ConditionTrue, + ) + th.ExpectCondition( + glanceTest.Instance, + ConditionGetterFunc(GlanceConditionGetter), + condition.DBSyncReadyCondition, + corev1.ConditionTrue, + ) + }) + It("GlanceAPI CRs are created", func() { + GlanceAPIExists(glanceTest.GlanceInternal) + GlanceAPIExists(glanceTest.GlanceExternal) + }) + }) + When("Glance CR is created without container images defined", func() { + BeforeEach(func() { + // GlanceEmptySpec is used to provide a standard Glance CR where no + // field is customized + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, GetGlanceEmptySpec())) + }) + It("has the expected container image defaults", func() { + glanceDefault := GetGlance(glanceTest.Instance) + Expect(glanceDefault.Spec.GlanceAPIInternal.ContainerImage).To(Equal(util.GetEnvVar("GLANCE_API_IMAGE_URL_DEFAULT", glancev1.GlanceAPIContainerImage))) + Expect(glanceDefault.Spec.GlanceAPIInternal.ContainerImage).To(Equal(util.GetEnvVar("GLANCE_API_IMAGE_URL_DEFAULT", glancev1.GlanceAPIContainerImage))) + }) + }) + When("All the Resources are ready", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, GetGlanceDefaultSpec())) + // Get Default GlanceAPI (internal/external) + DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.Instance, GetDefaultGlanceAPISpec())) + DeferCleanup( + th.DeleteDBService, + th.CreateDBService( + glanceTest.Instance.Namespace, + GetGlance(glanceName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + DeferCleanup(th.DeleteKeystoneAPI, th.CreateKeystoneAPI(glanceTest.Instance.Namespace)) + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobSuccess(glanceTest.GlanceDBSync) + th.SimulateKeystoneServiceReady(glanceTest.Instance) + th.SimulateKeystoneEndpointReady(glanceTest.GlanceInternalRoute) + }) + It("Creates glanceAPI", func() { + GlanceAPIExists(glanceTest.GlanceInternal) + GlanceAPIExists(glanceTest.GlanceExternal) + }) + It("Assert Services are created", func() { + th.AssertServiceExists(glanceTest.GlancePublicRoute) + th.AssertServiceExists(glanceTest.GlanceInternalRoute) + }) + It("Assert Routes are created", func() { + th.AssertRouteExists(glanceTest.GlancePublicRoute) + }) + }) + When("Glance CR is deleted", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, GetGlanceDefaultSpec())) + // Get Default GlanceAPI (internal/external) + DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.Instance, GetDefaultGlanceAPISpec())) + DeferCleanup( + th.DeleteDBService, + th.CreateDBService( + glanceTest.Instance.Namespace, + GetGlance(glanceName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + DeferCleanup(th.DeleteKeystoneAPI, th.CreateKeystoneAPI(glanceTest.Instance.Namespace)) + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobSuccess(glanceTest.GlanceDBSync) + th.SimulateKeystoneServiceReady(glanceTest.Instance) + th.SimulateKeystoneEndpointReady(glanceTest.GlanceInternalRoute) + }) + It("removes the finalizers from the Glance DB", func() { + mDB := th.GetMariaDBDatabase(glanceTest.Instance) + Expect(mDB.Finalizers).To(ContainElement("Glance")) + th.DeleteInstance(GetGlance(glanceTest.Instance)) + + }) + It("removes the ConfigMaps", func() { + Eventually(func() corev1.ConfigMap { + return *th.GetConfigMap(glanceTest.GlanceConfigMapData) + }, timeout, interval).ShouldNot(BeNil()) + Eventually(func() corev1.ConfigMap { + return *th.GetConfigMap(glanceTest.GlanceConfigMapScripts) + }, timeout, interval).ShouldNot(BeNil()) + Eventually(func() []corev1.ConfigMap { + return th.ListConfigMaps(glanceTest.GlanceConfigMapData.Name).Items + }, timeout, interval).Should(BeEmpty()) + Eventually(func() []corev1.ConfigMap { + return th.ListConfigMaps(glanceTest.GlanceConfigMapScripts.Name).Items + }, timeout, interval).Should(BeEmpty()) + }) + }) + When("Glance CR instance is built w/ NAD", func() { + BeforeEach(func() { + nad := th.CreateNetworkAttachmentDefinition(glanceTest.InternalAPINAD) + DeferCleanup(th.DeleteInstance, nad) + var externalEndpoints []interface{} + externalEndpoints = append( + externalEndpoints, map[string]interface{}{ + "endpoint": "internal", + "ipAddressPool": "osp-internalapi", + "loadBalancerIPs": []string{"10.1.0.1", "10.1.0.2"}, + }, + ) + rawSpec := map[string]interface{}{ + "storageRequest": glanceTest.GlancePVCSize, + "secret": SecretName, + "databaseInstance": "openstack", + "glanceAPIInternal": map[string]interface{}{ + "containerImage": glancev1.GlanceAPIContainerImage, + "networkAttachments": []string{"internalapi"}, + "externalEndpoints": externalEndpoints, + }, + "glanceAPIExternal": map[string]interface{}{ + "containerImage": glancev1.GlanceAPIContainerImage, + "networkAttachments": []string{"internalapi"}, + }, + } + DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, rawSpec)) + DeferCleanup( + th.DeleteDBService, + th.CreateDBService( + glanceTest.Instance.Namespace, + GetGlance(glanceName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + th.SimulateMariaDBDatabaseCompleted(glanceTest.Instance) + th.SimulateJobSuccess(glanceTest.GlanceDBSync) + keystoneAPI := th.CreateKeystoneAPI(glanceTest.Instance.Namespace) + DeferCleanup(th.DeleteKeystoneAPI, keystoneAPI) + keystoneAPIName := th.GetKeystoneAPI(keystoneAPI) + keystoneAPIName.Status.APIEndpoints["internal"] = "http://keystone-internal-openstack.testing" + Eventually(func(g Gomega) { + g.Expect(k8sClient.Status().Update(ctx, keystoneAPIName.DeepCopy())).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + th.SimulateKeystoneServiceReady(glanceTest.Instance) + }) + It("Check the resulting endpoints of the generated sub-CRs", func() { + th.SimulateDeploymentReadyWithPods( + glanceTest.GlanceInternalAPI, + map[string][]string{glanceName.Namespace + "/internalapi": {"10.0.0.1"}}, + ) + th.SimulateDeploymentReadyWithPods( + glanceTest.GlanceExternalAPI, + map[string][]string{glanceName.Namespace + "/internalapi": {"10.0.0.1"}}, + ) + // Retrieve the generated resources + glance := GetGlance(glanceTest.Instance) + internalAPI := GetGlanceAPI(glanceTest.GlanceInternal) + externalAPI := GetGlanceAPI(glanceTest.GlanceInternal) + // Check GlanceAPI NADs + Expect(internalAPI.Spec.NetworkAttachments).To(Equal(glance.Spec.GlanceAPIInternal.NetworkAttachments)) + Expect(internalAPI.Spec.ExternalEndpoints).To(Equal(glance.Spec.GlanceAPIInternal.ExternalEndpoints)) + Expect(externalAPI.Spec.NetworkAttachments).To(Equal(glance.Spec.GlanceAPIInternal.NetworkAttachments)) + }) }) }) diff --git a/tests/functional/glance_test_data.go b/tests/functional/glance_test_data.go new file mode 100644 index 00000000..23d8272c --- /dev/null +++ b/tests/functional/glance_test_data.go @@ -0,0 +1,125 @@ +/* +Copyright 2023. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package functional implements the envTest coverage for glance-operator +package functional + +import ( + "fmt" + "k8s.io/apimachinery/pkg/types" +) + +// GlanceTestData is the data structure used to provide input data to envTest +type APIType string + +const ( + GlanceAPITypeInternal APIType = "internal" + GlanceAPITypeExternal APIType = "external" +) + +type GlanceTestData struct { + GlanceDatabaseUser string + GlancePassword string + GlanceServiceUser string + GlancePVCSize string + GlanceQuotas map[string]interface{} + Instance types.NamespacedName + GlanceInternal types.NamespacedName + GlanceExternal types.NamespacedName + GlanceRole types.NamespacedName + GlanceRoleBinding types.NamespacedName + GlanceSA types.NamespacedName + GlanceDBSync types.NamespacedName + GlancePublicRoute types.NamespacedName + GlanceInternalRoute types.NamespacedName + GlanceConfigMapData types.NamespacedName + GlanceConfigMapScripts types.NamespacedName + GlanceInternalAPI types.NamespacedName + GlanceExternalAPI types.NamespacedName + InternalAPINAD types.NamespacedName +} + +// GetGlanceTestData is a function that initialize the GlanceTestData +// used in the test +func GetGlanceTestData(glanceName types.NamespacedName) GlanceTestData { + + m := glanceName + return GlanceTestData{ + Instance: m, + + GlanceDBSync: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-db-sync", glanceName.Name), + }, + GlanceInternalAPI: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-internal-api", glanceName.Name), + }, + GlanceExternalAPI: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-external-api", glanceName.Name), + }, + GlanceInternal: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-internal", glanceName.Name), + }, + GlanceExternal: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-external", glanceName.Name), + }, + GlanceRole: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("glance-%s-role", glanceName.Name), + }, + GlanceRoleBinding: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("glance-%s-rolebinding", glanceName.Name), + }, + GlanceSA: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("glance-%s", glanceName.Name), + }, + GlanceConfigMapData: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-%s", glanceName.Name, "config-data"), + }, + GlanceConfigMapScripts: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-%s", glanceName.Name, "scripts"), + }, + GlancePublicRoute: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-public", glanceName.Name), + }, + // Also used to identify GlanceKeystoneService + GlanceInternalRoute: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: fmt.Sprintf("%s-internal", glanceName.Name), + }, + GlanceQuotas: map[string]interface{}{ + "imageSizeTotal": 1000, + "imageStageTotal": 1000, + "imageCountUpload": 100, + "imageCountTotal": 100, + }, + InternalAPINAD: types.NamespacedName{ + Namespace: glanceName.Namespace, + Name: "internalapi", + }, + GlanceDatabaseUser: "glance", + // Password used for both db and service + GlancePassword: "12345678", + GlanceServiceUser: "glance", + GlancePVCSize: "10G", + } +} diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index 234e5d32..e4894c63 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package functional_test +package functional import ( "context" "crypto/tls" "fmt" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/types" "net" "path/filepath" "testing" @@ -29,8 +31,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" - "github.com/openstack-k8s-operators/lib-common/modules/test" + . "github.com/openstack-k8s-operators/lib-common/modules/test/helpers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" @@ -41,11 +42,13 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" routev1 "github.com/openshift/api/route/v1" glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1" "github.com/openstack-k8s-operators/glance-operator/controllers" rabbitmqv1beta1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/test" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" //+kubebuilder:scaffold:imports ) @@ -56,17 +59,21 @@ import ( const ( timeout = 10 * time.Second // have maximum 100 retries before the timeout hits - interval = timeout / 100 + interval = timeout / 100 + SecretName = "test-osp-secret" ) var ( - cfg *rest.Config - k8sClient client.Client - testEnv *envtest.Environment - th *TestHelper - ctx context.Context - cancel context.CancelFunc - logger logr.Logger + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + th *TestHelper + ctx context.Context + cancel context.CancelFunc + logger logr.Logger + namespace string + glanceName types.NamespacedName + glanceTest GlanceTestData ) func TestAPIs(t *testing.T) { @@ -85,6 +92,9 @@ var _ = BeforeSuite(func() { keystoneCRDs, err := test.GetCRDDirFromModule( "github.com/openstack-k8s-operators/keystone-operator/api", gomod, "bases") Expect(err).ShouldNot(HaveOccurred()) + networkv1CRD, err := test.GetCRDDirFromModule( + "github.com/k8snetworkplumbingwg/network-attachment-definition-client", "../../go.mod", "artifacts/networks-crd.yaml") + Expect(err).ShouldNot(HaveOccurred()) mariadbCRDs, err := test.GetCRDDirFromModule( "github.com/openstack-k8s-operators/mariadb-operator/api", gomod, "bases") Expect(err).ShouldNot(HaveOccurred()) @@ -99,6 +109,11 @@ var _ = BeforeSuite(func() { keystoneCRDs, routev1CRDs, }, + CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + networkv1CRD, + }, + }, ErrorIfCRDPathMissing: true, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "config", "webhook")}, @@ -124,6 +139,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = routev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = networkv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -202,3 +219,23 @@ var _ = AfterSuite(func() { err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) }) + +var _ = BeforeEach(func() { + // NOTE(gibi): We need to create a unique namespace for each test run + // as namespaces cannot be deleted in a locally running envtest. See + // https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation + namespace = uuid.New().String() + th.CreateNamespace(namespace) + // We still request the delete of the Namespace to properly cleanup if + // we run the test in an existing cluster. + glanceName = types.NamespacedName{ + Namespace: namespace, + Name: "glance", + } + + glanceTest = GetGlanceTestData(glanceName) + + DeferCleanup(th.DeleteNamespace, namespace) + //Let's create the osp-secret in advance (in common to all the test cases) + DeferCleanup(k8sClient.Delete, ctx, CreateGlanceSecret(glanceName.Namespace, SecretName)) +})