diff --git a/apis/bases/core.openstack.org_openstackcontrolplanes.yaml b/apis/bases/core.openstack.org_openstackcontrolplanes.yaml index a15aeff86..1d5890939 100644 --- a/apis/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/apis/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -9855,6 +9855,8 @@ spec: additionalProperties: type: string type: object + notificationsBusInstance: + type: string nova: properties: apiOverride: diff --git a/apis/core/v1beta1/openstackcontrolplane_types.go b/apis/core/v1beta1/openstackcontrolplane_types.go index c4431f1d8..0016d78a4 100644 --- a/apis/core/v1beta1/openstackcontrolplane_types.go +++ b/apis/core/v1beta1/openstackcontrolplane_types.go @@ -127,6 +127,13 @@ type OpenStackControlPlaneSpec struct { // Rabbitmq - Parameters related to the Rabbitmq service Rabbitmq RabbitmqSection `json:"rabbitmq,omitempty"` + // +kubebuilder:validation:Optional + // NotificationsBusInstance - the name of RabbitMQ Cluster CR to select a Messaging + // Bus Service instance used by all services that produce or consume notifications. + // Avoid colocating it with RabbitMQ services used for PRC. + // That instance will be pushed down for services, unless overriden in templates. + NotificationsBusInstance *string `json:"notificationsBusInstance,omitempty"` + // +kubebuilder:validation:Optional // +operator-sdk:csv:customresourcedefinitions:type=spec // Memcached - Parameters related to the Memcached service diff --git a/apis/core/v1beta1/openstackcontrolplane_webhook.go b/apis/core/v1beta1/openstackcontrolplane_webhook.go index 9eb632679..fed1abca5 100644 --- a/apis/core/v1beta1/openstackcontrolplane_webhook.go +++ b/apis/core/v1beta1/openstackcontrolplane_webhook.go @@ -132,6 +132,10 @@ func (r *OpenStackControlPlane) ValidateCreate() (admission.Warnings, error) { allErrs = append(allErrs, err) } + if err := r.ValidateNotificationsBusInstance(basePath); err != nil { + allErrs = append(allErrs, err) + } + if len(allErrs) != 0 { return allWarn, apierrors.NewInvalid( schema.GroupKind{Group: "core.openstack.org", Kind: "OpenStackControlPlane"}, @@ -161,6 +165,10 @@ func (r *OpenStackControlPlane) ValidateUpdate(old runtime.Object) (admission.Wa allErrs = append(allErrs, err) } + if err := r.ValidateNotificationsBusInstance(basePath); err != nil { + allErrs = append(allErrs, err) + } + if len(allErrs) != 0 { return nil, apierrors.NewInvalid( schema.GroupKind{Group: "core.openstack.org", Kind: "OpenStackControlPlane"}, @@ -1138,3 +1146,28 @@ func (r *OpenStackControlPlane) ValidateTopology(basePath *field.Path) *field.Er } return nil } + +// ValidateNotificationsBusInstance - returns an error if the notificationsBusInstance +// parameter is not valid. +// - nil or empty string must be raised as an error +// - when notificationsBusInstance does not point to an existing RabbitMQ instance +func (r *OpenStackControlPlane) ValidateNotificationsBusInstance(basePath *field.Path) *field.Error { + notificationsField := basePath.Child("notificationsBusInstance") + // no notificationsBusInstance field set, nothing to validate here + if r.Spec.NotificationsBusInstance == nil { + return nil + } + // When NotificationsBusInstance is set, fail if it is an empty string + if *r.Spec.NotificationsBusInstance == "" { + return field.Invalid(notificationsField, *r.Spec.NotificationsBusInstance, "notificationsBusInstance is not a valid string") + } + // NotificationsBusInstance is set and must be equal to an existing + // deployed rabbitmq instance, otherwise we should fail because it + // does not represent a valid string + for k := range(*r.Spec.Rabbitmq.Templates) { + if *r.Spec.NotificationsBusInstance == k { + return nil + } + } + return field.Invalid(notificationsField, *r.Spec.NotificationsBusInstance, "notificationsBusInstance must match an existing RabbitMQ instance name") +} diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index 1c6212861..fc8b549ed 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -1178,6 +1178,11 @@ func (in *OpenStackControlPlaneSpec) DeepCopyInto(out *OpenStackControlPlaneSpec in.Cinder.DeepCopyInto(&out.Cinder) in.Galera.DeepCopyInto(&out.Galera) in.Rabbitmq.DeepCopyInto(&out.Rabbitmq) + if in.NotificationsBusInstance != nil { + in, out := &in.NotificationsBusInstance, &out.NotificationsBusInstance + *out = new(string) + **out = **in + } in.Memcached.DeepCopyInto(&out.Memcached) in.Ovn.DeepCopyInto(&out.Ovn) in.Neutron.DeepCopyInto(&out.Neutron) diff --git a/bindata/crds/crds.yaml b/bindata/crds/crds.yaml index 7634f9c66..9e0507764 100644 --- a/bindata/crds/crds.yaml +++ b/bindata/crds/crds.yaml @@ -10021,6 +10021,8 @@ spec: additionalProperties: type: string type: object + notificationsBusInstance: + type: string nova: properties: apiOverride: diff --git a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml index a15aeff86..1d5890939 100644 --- a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -9855,6 +9855,8 @@ spec: additionalProperties: type: string type: object + notificationsBusInstance: + type: string nova: properties: apiOverride: diff --git a/pkg/openstack/cinder.go b/pkg/openstack/cinder.go index 10f70c7b8..873cfdd5b 100644 --- a/pkg/openstack/cinder.go +++ b/pkg/openstack/cinder.go @@ -130,6 +130,12 @@ func ReconcileCinder(ctx context.Context, instance *corev1beta1.OpenStackControl instance.Spec.Cinder.Template.TopologyRef = instance.Spec.TopologyRef } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Cinder.Template.NotificationsBusInstance == nil { + instance.Spec.Cinder.Template.NotificationsBusInstance = instance.Spec.NotificationsBusInstance + } + Log.Info("Reconciling Cinder", "Cinder.Namespace", instance.Namespace, "Cinder.Name", cinderName) op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), cinder, func() error { instance.Spec.Cinder.Template.CinderSpecBase.DeepCopyInto(&cinder.Spec.CinderSpecBase) diff --git a/pkg/openstack/glance.go b/pkg/openstack/glance.go index c4f39014d..50e5f6a9e 100644 --- a/pkg/openstack/glance.go +++ b/pkg/openstack/glance.go @@ -74,6 +74,12 @@ func ReconcileGlance(ctx context.Context, instance *corev1beta1.OpenStackControl instance.Spec.Glance.Template.TopologyRef = instance.Spec.TopologyRef } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Glance.Template.NotificationBusInstance == nil { + instance.Spec.Glance.Template.NotificationBusInstance = instance.Spec.NotificationsBusInstance + } + // When component services got created check if there is the need to create a route if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: glanceName, Namespace: instance.Namespace}, glance); err != nil { if !k8s_errors.IsNotFound(err) { diff --git a/pkg/openstack/manila.go b/pkg/openstack/manila.go index 5e8d98334..7c0f90471 100644 --- a/pkg/openstack/manila.go +++ b/pkg/openstack/manila.go @@ -118,6 +118,12 @@ func ReconcileManila(ctx context.Context, instance *corev1beta1.OpenStackControl instance.Spec.Manila.Template.TopologyRef = instance.Spec.TopologyRef } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Manila.Template.NotificationsBusInstance == nil { + instance.Spec.Manila.Template.NotificationsBusInstance = instance.Spec.NotificationsBusInstance + } + Log.Info("Reconciling Manila", "Manila.Namespace", instance.Namespace, "Manila.Name", "manila") op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), manila, func() error { instance.Spec.Manila.Template.ManilaSpecBase.DeepCopyInto(&manila.Spec.ManilaSpecBase) diff --git a/pkg/openstack/neutron.go b/pkg/openstack/neutron.go index ddf2a0298..82d37127a 100644 --- a/pkg/openstack/neutron.go +++ b/pkg/openstack/neutron.go @@ -155,6 +155,12 @@ func ReconcileNeutron(ctx context.Context, instance *corev1beta1.OpenStackContro instance.Spec.Neutron.Template.TopologyRef = instance.Spec.TopologyRef } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Neutron.Template.NotificationsBusInstance == nil { + instance.Spec.Neutron.Template.NotificationsBusInstance = instance.Spec.NotificationsBusInstance + } + Log.Info("Reconciling NeutronAPI", "NeutronAPI.Namespace", instance.Namespace, "NeutronAPI.Name", "neutron") op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), neutronAPI, func() error { instance.Spec.Neutron.Template.DeepCopyInto(&neutronAPI.Spec.NeutronAPISpecCore) diff --git a/pkg/openstack/nova.go b/pkg/openstack/nova.go index cf287f163..d13dd1ee3 100644 --- a/pkg/openstack/nova.go +++ b/pkg/openstack/nova.go @@ -72,6 +72,12 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl instance.Spec.Nova.Template.NodeSelector = &instance.Spec.NodeSelector } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Nova.Template.NotificationsBusInstance == nil { + instance.Spec.Nova.Template.NotificationsBusInstance = instance.Spec.NotificationsBusInstance + } + // When there's no Topology referenced in the Service Template, inject the // top-level one // NOTE: This does not check the Service subCRs: by default the generated diff --git a/pkg/openstack/watcher.go b/pkg/openstack/watcher.go index 9861e35e8..b7fe7773f 100644 --- a/pkg/openstack/watcher.go +++ b/pkg/openstack/watcher.go @@ -114,6 +114,12 @@ func ReconcileWatcher(ctx context.Context, instance *corev1beta1.OpenStackContro instance.Spec.Watcher.Template.TopologyRef = instance.Spec.TopologyRef } + // When no NotificationsBusInstance is referenced in the subCR (override) + // try to inject the top-level one if defined + if instance.Spec.Watcher.Template.NotificationsBusInstance == nil { + instance.Spec.Watcher.Template.NotificationsBusInstance = instance.Spec.NotificationsBusInstance + } + helper.GetLogger().Info("Reconciling Watcher", "Watcher.Namespace", instance.Namespace, "Watcher.Name", "watcher") op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), watcher, func() error { instance.Spec.Watcher.Template.DeepCopyInto(&watcher.Spec.WatcherSpecCore) diff --git a/tests/functional/ctlplane/base_test.go b/tests/functional/ctlplane/base_test.go index f0c7bac6f..0c3b4fa88 100644 --- a/tests/functional/ctlplane/base_test.go +++ b/tests/functional/ctlplane/base_test.go @@ -37,6 +37,7 @@ import ( manilav1 "github.com/openstack-k8s-operators/manila-operator/api/v1beta1" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + novav1 "github.com/openstack-k8s-operators/nova-operator/api/v1beta1" openstackclientv1 "github.com/openstack-k8s-operators/openstack-operator/apis/client/v1beta1" corev1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1" dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/apis/dataplane/v1beta1" @@ -69,6 +70,8 @@ type Names struct { RabbitMQCertName types.NamespacedName RabbitMQCell1Name types.NamespacedName RabbitMQCell1CertName types.NamespacedName + RabbitMQNotificationsName types.NamespacedName + RabbitMQNotificationsCertName types.NamespacedName NoVNCProxyCell1CertPublicRouteName types.NamespacedName NoVNCProxyCell1CertPublicSvcName types.NamespacedName NoVNCProxyCell1CertVencryptName types.NamespacedName @@ -223,6 +226,14 @@ func CreateNames(openstackControlplaneName types.NamespacedName) Names { Namespace: openstackControlplaneName.Namespace, Name: "cert-rabbitmq-cell1-svc", }, + RabbitMQNotificationsName: types.NamespacedName{ + Namespace: openstackControlplaneName.Namespace, + Name: "rabbitmq-notifications", + }, + RabbitMQNotificationsCertName: types.NamespacedName{ + Namespace: openstackControlplaneName.Namespace, + Name: "cert-rabbitmq-notifications-svc", + }, NoVNCProxyCell1CertPublicRouteName: types.NamespacedName{ Name: "cert-nova-novncproxy-cell1-public-route", Namespace: openstackControlplaneName.Namespace, @@ -535,6 +546,9 @@ func GetDefaultOpenStackControlPlaneSpec() map[string]interface{} { names.RabbitMQCell1Name.Name: map[string]interface{}{ "replicas": 1, }, + names.RabbitMQNotificationsName.Name: map[string]interface{}{ + "replicas": 1, + }, } galeraTemplate := map[string]interface{}{ names.DBName.Name: map[string]interface{}{ @@ -876,6 +890,15 @@ func GetNeutron(name types.NamespacedName) *neutronv1.NeutronAPI { return instance } +// GetNova +func GetNova(name types.NamespacedName) *novav1.Nova { + instance := &novav1.Nova{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + // GetManila func GetManila(name types.NamespacedName) *manilav1.Manila { instance := &manilav1.Manila{} diff --git a/tests/functional/ctlplane/openstackoperator_controller_test.go b/tests/functional/ctlplane/openstackoperator_controller_test.go index 4ca96ba37..b3efb58e2 100644 --- a/tests/functional/ctlplane/openstackoperator_controller_test.go +++ b/tests/functional/ctlplane/openstackoperator_controller_test.go @@ -78,78 +78,109 @@ var _ = Describe("OpenStackOperator controller", func() { }) var ( - galeraService = Entry("the galera service", func() ( + galeraTopologyService = Entry("the galera service", func() ( client.Object, *topologyv1.TopoRef) { svc := mariadb.GetGalera(names.DBName) tp := svc.Spec.TopologyRef return svc, tp }) - keystoneService = Entry("the keystone service", func() ( + keystoneTopologyService = Entry("the keystone service", func() ( client.Object, *topologyv1.TopoRef) { svc := keystone.GetKeystoneAPI(names.KeystoneAPIName) tp := svc.Spec.TopologyRef return svc, tp }) - rabbitService = Entry("the rabbitmq service", func() ( + rabbitTopologyService = Entry("the rabbitmq service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetRabbitMQCluster(names.RabbitMQName) tp := svc.Spec.TopologyRef return svc, tp }) - memcachedService = Entry("the memcached service", func() ( + memcachedTopologyService = Entry("the memcached service", func() ( client.Object, *topologyv1.TopoRef) { svc := infra.GetMemcached(names.MemcachedName) tp := svc.Spec.TopologyRef return svc, tp }) - glanceService = Entry("the glance service", func() ( + glanceTopologyService = Entry("the glance service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetGlance(names.GlanceName) tp := svc.Spec.TopologyRef return svc, tp }) - cinderService = Entry("the cinder service", func() ( + cinderTopologyService = Entry("the cinder service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetCinder(names.CinderName) tp := svc.Spec.TopologyRef return svc, tp }) - manilaService = Entry("the manila service", func() ( + manilaTopologyService = Entry("the manila service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetManila(names.ManilaName) tp := svc.Spec.TopologyRef return svc, tp }) - neutronService = Entry("the neutron service", func() ( + neutronTopologyService = Entry("the neutron service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetNeutron(names.NeutronName) tp := svc.Spec.TopologyRef return svc, tp }) - horizonService = Entry("the horizon service", func() ( + horizonTopologyService = Entry("the horizon service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetHorizon(names.HorizonName) tp := svc.Spec.TopologyRef return svc, tp }) - heatService = Entry("the heat service", func() ( + heatTopologyService = Entry("the heat service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetHeat(names.HeatName) tp := svc.Spec.TopologyRef return svc, tp }) - telemetryService = Entry("the telemetry service", func() ( + telemetryTopologyService = Entry("the telemetry service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetTelemetry(names.TelemetryName) tp := svc.Spec.TopologyRef return svc, tp }) - watcherService = Entry("the watcher service", func() ( + watcherTopologyService = Entry("the watcher service", func() ( client.Object, *topologyv1.TopoRef) { svc := GetWatcher(names.WatcherName) tp := svc.Spec.TopologyRef return svc, tp }) + + glanceNotifSvc = Entry("the Glance service", func() ( + client.Object, *string) { + svc := GetGlance(names.GlanceName) + return svc, svc.Spec.NotificationBusInstance + }) + cinderNotifSvc = Entry("the Cinder service", func() ( + client.Object, *string) { + svc := GetCinder(names.CinderName) + return svc, svc.Spec.NotificationsBusInstance + }) + manilaNotifSvc = Entry("the Manila service", func() ( + client.Object, *string) { + svc := GetManila(names.ManilaName) + return svc, svc.Spec.NotificationsBusInstance + }) + neutronNotifSvc = Entry("the Neutron service", func() ( + client.Object, *string) { + svc := GetNeutron(names.NeutronName) + return svc, svc.Spec.NotificationsBusInstance + }) + novaNotifSvc = Entry("the Nova service", func() ( + client.Object, *string) { + svc := GetNova(names.NovaName) + return svc, svc.Spec.NotificationsBusInstance + }) + watcherNotifSvc = Entry("the Watcher service", func() ( + client.Object, *string) { + svc := GetWatcher(names.WatcherName) + return svc, svc.Spec.NotificationsBusInstance + }) ) // // Validate TLS input settings @@ -845,6 +876,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -1166,6 +1198,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -1293,6 +1326,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -1821,6 +1855,7 @@ var _ = Describe("OpenStackOperator controller", func() { // NOTE(bogdando): DBs certs need to be created here as well, but those are already existing somehow DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNNorthdCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNControllerCertName)) @@ -1985,6 +2020,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -2168,6 +2204,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -2643,6 +2680,7 @@ var _ = Describe("OpenStackOperator controller", func() { // create cert secrets for rabbitmq instances DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) // create cert secrets for memcached instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) // create cert secrets for ovn instance @@ -2709,18 +2747,18 @@ var _ = Describe("OpenStackOperator controller", func() { }, // The entry list depends on the default enabled services in the // default spec - galeraService, - keystoneService, - rabbitService, - memcachedService, - telemetryService, - glanceService, - cinderService, - manilaService, - neutronService, - horizonService, - heatService, - watcherService, + galeraTopologyService, + keystoneTopologyService, + rabbitTopologyService, + memcachedTopologyService, + telemetryTopologyService, + glanceTopologyService, + cinderTopologyService, + manilaTopologyService, + neutronTopologyService, + horizonTopologyService, + heatTopologyService, + watcherTopologyService, ) DescribeTable("An OpenStackControlplane updates the topology reference", func(serviceNameFunc func() (client.Object, *topologyv1.TopoRef)) { @@ -2741,18 +2779,18 @@ var _ = Describe("OpenStackOperator controller", func() { g.Expect(toporef).To(Equal(expectedTopology)) }, timeout, interval).Should(Succeed()) }, - galeraService, - keystoneService, - rabbitService, - memcachedService, - telemetryService, - glanceService, - cinderService, - manilaService, - neutronService, - horizonService, - heatService, - watcherService, + galeraTopologyService, + keystoneTopologyService, + rabbitTopologyService, + memcachedTopologyService, + telemetryTopologyService, + glanceTopologyService, + cinderTopologyService, + manilaTopologyService, + neutronTopologyService, + horizonTopologyService, + heatTopologyService, + watcherTopologyService, ) DescribeTable("An OpenStackControlplane Service (Glance) overrides the topology reference", func(serviceNameFunc func() (client.Object, *topologyv1.TopoRef)) { @@ -2790,17 +2828,17 @@ var _ = Describe("OpenStackOperator controller", func() { }, // The entry list depends on the default enabled services in the // default spec - galeraService, - keystoneService, - rabbitService, - memcachedService, - telemetryService, - cinderService, - manilaService, - neutronService, - horizonService, - heatService, - watcherService, + galeraTopologyService, + keystoneTopologyService, + rabbitTopologyService, + memcachedTopologyService, + telemetryTopologyService, + cinderTopologyService, + manilaTopologyService, + neutronTopologyService, + horizonTopologyService, + heatTopologyService, + watcherTopologyService, ) DescribeTable("An OpenStackControlplane removes the topology reference", func(serviceNameFunc func() (client.Object, *topologyv1.TopoRef)) { @@ -2817,24 +2855,222 @@ var _ = Describe("OpenStackOperator controller", func() { }, // The entry list depends on the default enabled services in the // default spec - galeraService, - keystoneService, - rabbitService, - memcachedService, - telemetryService, - glanceService, - cinderService, - manilaService, - neutronService, - horizonService, - heatService, - watcherService, + galeraTopologyService, + keystoneTopologyService, + rabbitTopologyService, + memcachedTopologyService, + telemetryTopologyService, + glanceTopologyService, + cinderTopologyService, + manilaTopologyService, + neutronTopologyService, + horizonTopologyService, + heatTopologyService, + watcherTopologyService, + ) + }) + + When("An OpenStackControlplane instance references a notificationsBusInstance", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + + // point notificationsBusInstance to the default rabbitmq instance + spec["notificationsBusInstance"] = names.RabbitMQName.Name + + spec["telemetry"] = map[string]interface{}{ + "enabled": true, + "template": map[string]interface{}{ + "ceilometer": map[string]interface{}{ + "enabled": true, + }, + "metricStorage": map[string]interface{}{ + "enabled": true, + }, + }, + } + spec["watcher"] = map[string]interface{}{ + "enabled": true, + } + + // enable nova and its dependencies + spec["nova"] = map[string]interface{}{ + "enabled": true, + "template": map[string]interface{}{ + "apiTimeout": 60, + "cellTemplates": map[string]interface{}{ + "cell0": map[string]interface{}{}, + }, + }, + } + spec["placement"] = map[string]interface{}{ + "enabled": true, + "template": map[string]interface{}{ + "apiTimeout": 60, + }, + } + + // create cert secrets for rabbitmq instances + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) + // create cert secrets for memcached instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) + // create cert secrets for ovn instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNNorthdCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNControllerCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.NeutronOVNCertName)) + + DeferCleanup( + th.DeleteInstance, + CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec), + ) + Eventually(func(g Gomega) { + keystoneAPI := keystone.GetKeystoneAPI(names.KeystoneAPIName) + g.Expect(keystoneAPI).Should(Not(BeNil())) + }, timeout, interval).Should(Succeed()) + keystone.SimulateKeystoneAPIReady(names.KeystoneAPIName) + + Eventually(func(g Gomega) { + osversion := GetOpenStackVersion(names.OpenStackControlplaneName) + g.Expect(osversion).Should(Not(BeNil())) + + th.ExpectCondition( + names.OpenStackVersionName, + ConditionGetterFunc(OpenStackVersionConditionGetter), + corev1.OpenStackVersionInitialized, + k8s_corev1.ConditionTrue, + ) + }, timeout, interval).Should(Succeed()) + th.CreateSecret(types.NamespacedName{ + Name: "openstack-config-secret", + Namespace: namespace, + }, map[string][]byte{"secure.yaml": []byte("foo")}) + + th.CreateConfigMap(types.NamespacedName{ + Name: "openstack-config", + Namespace: namespace, + }, map[string]interface{}{ + "clouds.yaml": string("foo"), + "OS_CLOUD": "default", + }) + }) + DescribeTable("it is propagated to", + func(serviceNameFunc func() (client.Object, *string)) { + + svc, notif := serviceNameFunc() + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + // service exists and notificationsBusInstance has been propagated + Eventually(func(g Gomega) { + g.Expect(OSCtlplane).Should(Not(BeNil())) + g.Expect(svc).Should(Not(BeNil())) + g.Expect(notif).To(Equal(OSCtlplane.Spec.NotificationsBusInstance)) + }, timeout, interval).Should(Succeed()) + }, + // The entry list depends on the services that currently implement + // the notificationsBusInstance interface + glanceNotifSvc, + cinderNotifSvc, + manilaNotifSvc, + neutronNotifSvc, + novaNotifSvc, + watcherNotifSvc, + ) + DescribeTable("A service (Nova) overrides the notification value", + func(serviceNameFunc func() (client.Object, *string)) { + rabbitMqOverride := "rabbitmq-notifications" + + Eventually(func(g Gomega) { + ctlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + + ctlplane.Spec.Nova.Template.NotificationsBusInstance = &rabbitMqOverride + g.Expect(k8sClient.Update(ctx, ctlplane)).To(Succeed()) + }, timeout, interval).Should(Succeed()) + + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + // Nova points to the override + Eventually(func(g Gomega) { + nova := GetNova(names.NovaName) + g.Expect(OSCtlplane).Should(Not(BeNil())) + g.Expect(nova).Should(Not(BeNil())) + g.Expect(nova.Spec.NotificationsBusInstance).To(Equal(OSCtlplane.Spec.Nova.Template.NotificationsBusInstance)) + g.Expect(*nova.Spec.NotificationsBusInstance).To(Equal(rabbitMqOverride)) + }, timeout, interval).Should(Succeed()) + + // The rest of the services still point to the top-level rabbit + svc, notif := serviceNameFunc() + Eventually(func(g Gomega) { + g.Expect(OSCtlplane).Should(Not(BeNil())) + g.Expect(svc).Should(Not(BeNil())) + g.Expect(notif).To(Equal(OSCtlplane.Spec.NotificationsBusInstance)) + }, timeout, interval).Should(Succeed()) + }, + // The entry list depends on the services that currently implement + // the notificationsBusInstance interface + glanceNotifSvc, + cinderNotifSvc, + manilaNotifSvc, + neutronNotifSvc, + watcherNotifSvc, + ) + DescribeTable("An OpenStackControlplane removes the notificationsBusInstance reference", + func(serviceNameFunc func() (client.Object, *string)) { + Eventually(func(g Gomega) { + ctlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + ctlplane.Spec.NotificationsBusInstance = nil + g.Expect(k8sClient.Update(ctx, ctlplane)).To(Succeed()) + + svc, notif := serviceNameFunc() + g.Expect(svc).Should(Not(BeNil())) + g.Expect(notif).To(BeNil()) + }, timeout, interval).Should(Succeed()) + }, + // The entry list depends on the services that currently implement + // the notificationsBusInstance interface + glanceNotifSvc, + cinderNotifSvc, + manilaNotifSvc, + neutronNotifSvc, + novaNotifSvc, + watcherNotifSvc, ) }) }) var _ = Describe("OpenStackOperator Webhook", func() { + DescribeTable("notificationsBusInstance", + func(getNotificationField func() (string, string)) { + spec := GetDefaultOpenStackControlPlaneSpec() + value, errMsg := getNotificationField() + spec["notificationsBusInstance"] = value + raw := map[string]interface{}{ + "apiVersion": "core.openstack.org/v1beta1", + "kind": "OpenStackControlPlane", + "metadata": map[string]interface{}{ + "name": "foo", + "namespace": namespace, + }, + "spec": spec, + } + unstructuredObj := &unstructured.Unstructured{Object: raw} + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) + Expect(err).Should(HaveOccurred()) + var statusError *k8s_errors.StatusError + Expect(errors.As(err, &statusError)).To(BeTrue()) + Expect(statusError.ErrStatus.Details.Kind).To(Equal("OpenStackControlPlane")) + Expect(statusError.ErrStatus.Message).To( + ContainSubstring(errMsg), + ) + }, + Entry("notificationsBusInstance is wrong", func() (string, string) { + return "foo", "spec.notificationsBusInstance: Invalid value: \"foo\": notificationsBusInstance must match an existing RabbitMQ instance name" + }), + Entry("notificationsBusInstance is an empty string", func() (string, string) { + return "", "spec.notificationsBusInstance: Invalid value: \"\": notificationsBusInstance is not a valid string" + }), + ) + It("Blocks creating multiple ctlplane CRs in the same namespace", func() { spec := GetDefaultOpenStackControlPlaneSpec() spec["tls"] = GetTLSPublicSpec() @@ -3562,6 +3798,7 @@ var _ = Describe("OpenStackOperator controller nova cell deletion", func() { // create cert secrets for rabbitmq instances th.CreateCertSecret(names.RabbitMQCertName) th.CreateCertSecret(names.RabbitMQCell1CertName) + th.CreateCertSecret(names.RabbitMQNotificationsCertName) // create cert secrets for ovn instance DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNNorthdCertName)) @@ -3704,7 +3941,7 @@ var _ = Describe("OpenStackOperator controller nova cell deletion", func() { Eventually(func(g Gomega) { OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) rabbitTemplates := *(OSCtlplane.Spec.Rabbitmq.Templates) - g.Expect(rabbitTemplates).Should(HaveLen(2)) + g.Expect(rabbitTemplates).Should(HaveLen(3)) delete(rabbitTemplates, names.RabbitMQCell1Name.Name) OSCtlplane.Spec.Rabbitmq.Templates = &rabbitTemplates g.Expect(k8sClient.Update(ctx, OSCtlplane)).Should(Succeed()) @@ -3714,7 +3951,7 @@ var _ = Describe("OpenStackOperator controller nova cell deletion", func() { Eventually(func(g Gomega) { OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) rabbitTemplates := *(OSCtlplane.Spec.Rabbitmq.Templates) - g.Expect(rabbitTemplates).Should(HaveLen(1)) + g.Expect(rabbitTemplates).Should(HaveLen(2)) }, timeout, interval).Should(Succeed()) // cell1.rabbitmq should not exists in db