From d5bf4bf5e924abe3eb21fe46e34a7acf5ac27974 Mon Sep 17 00:00:00 2001 From: Luca Miccini Date: Thu, 18 Dec 2025 18:00:41 +0100 Subject: [PATCH] Rabbitmq vhost and user support Add new messagingBus and notificationsBus interfaces to hold cluster, user and vhost names for optional usage. The controller adds these values to the TransportURL create request when present. Additionally, we migrate RabbitMQ cluster name to RabbitMq config struct using DefaultRabbitMqConfig from infra-operator to automatically populate the new Cluster field from legacy RabbitMqClusterName. Example usage: spec: messagingBus: cluster: rpc-rabbitmq user: rpc-user vhost: rpc-vhost notificationsBus: cluster: notifications-rabbitmq user: notifications-user vhost: notifications-vhost Finally, we add the rabbitmquser crs to the secret so they can be stored for dataplane finalizers management and do auto cleanup of orphaned users after credential rotations. Jira: https://issues.redhat.com/browse/OSPRH-22697 --- api/bases/nova.openstack.org_nova.yaml | 50 +++++++++ api/go.mod | 5 +- api/go.sum | 7 +- api/v1beta1/nova_types.go | 9 ++ api/v1beta1/nova_webhook.go | 53 ++++++++- api/v1beta1/novacell_types.go | 5 + api/v1beta1/zz_generated.deepcopy.go | 8 ++ config/crd/bases/nova.openstack.org_nova.yaml | 50 +++++++++ go.mod | 3 + go.sum | 4 +- internal/controller/common.go | 8 ++ internal/controller/nova_controller.go | 104 +++++++++++------- test/functional/nova_controller_test.go | 23 +++- test/functional/nova_reconfiguration_test.go | 3 +- .../00-cleanup-nova.yaml | 6 + .../nova-notificationsbus/01-assert.yaml | 65 +++++++++++ .../nova-notificationsbus/01-deploy.yaml | 16 +++ .../nova-same-cluster/00-cleanup-nova.yaml | 6 + .../default/nova-same-cluster/01-assert.yaml | 61 ++++++++++ .../default/nova-same-cluster/01-deploy.yaml | 16 +++ 20 files changed, 450 insertions(+), 52 deletions(-) create mode 100644 test/kuttl/test-suites/default/nova-notificationsbus/00-cleanup-nova.yaml create mode 100644 test/kuttl/test-suites/default/nova-notificationsbus/01-assert.yaml create mode 100644 test/kuttl/test-suites/default/nova-notificationsbus/01-deploy.yaml create mode 100644 test/kuttl/test-suites/default/nova-same-cluster/00-cleanup-nova.yaml create mode 100644 test/kuttl/test-suites/default/nova-same-cluster/01-assert.yaml create mode 100644 test/kuttl/test-suites/default/nova-same-cluster/01-deploy.yaml diff --git a/api/bases/nova.openstack.org_nova.yaml b/api/bases/nova.openstack.org_nova.yaml index 58303ff6a..1356b378f 100644 --- a/api/bases/nova.openstack.org_nova.yaml +++ b/api/bases/nova.openstack.org_nova.yaml @@ -540,6 +540,23 @@ spec: MemcachedInstance is the name of the Memcached CR that the services in the cell will use. If defined then this takes precedence over Nova.Spec.MemcachedInstance for this cel type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and + cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object metadataServiceTemplate: description: |- MetadataServiceTemplate - defines the metadata service dedicated for the @@ -1340,6 +1357,22 @@ spec: description: MemcachedInstance is the name of the Memcached CR that all nova service will use. type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object metadataContainerImageURL: description: MetadataContainerImageURL type: string @@ -1648,6 +1681,23 @@ spec: NodeSelector here acts as a default value and can be overridden by service specific NodeSelector Settings. type: object + notificationsBus: + description: NotificationsBus configuration (username, vhost, and + cluster) for notifications + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object notificationsBusInstance: description: |- NotificationsBusInstance is the name of the RabbitMqCluster CR to select diff --git a/api/go.mod b/api/go.mod index e871ea6d7..2fb428c32 100644 --- a/api/go.mod +++ b/api/go.mod @@ -18,7 +18,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -44,6 +43,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/rabbitmq/cluster-operator/v2 v2.16.0 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect @@ -69,6 +69,9 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) +// Use lmiccini's infra-operator branch with status.RabbitmqUserRef support +replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3 + // mschuppert: map to latest commit from release-4.16 tag // must consistent within modules and service operators replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e //allow-merging diff --git a/api/go.sum b/api/go.sum index 1e3cbb9d5..668ace769 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,3 +1,4 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -64,6 +65,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3 h1:S1WlXTdXw0PA0yZT1acQ1Jwxf/zLGdF2R9DAxbtpVRw= +github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -78,10 +81,10 @@ github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8 github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f h1:xcCGJ/g5vvbWhtEJCbv8UeBneI5yrMawm+CXRsJrJZo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 h1:pF3mJ3nwq6r4qwom+rEWZNquZpcQW/iftHlJ1KPIDsk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:kycZyoe7OZdW1HUghr2nI3N7wSJtNahXf6b/ypD14f4= +github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= +github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec/go.mod h1:Nh2NEePLjovUQof2krTAg4JaAoLacqtPTZQXK6izNfg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/api/v1beta1/nova_types.go b/api/v1beta1/nova_types.go index 2f0ad588e..3781aedc9 100644 --- a/api/v1beta1/nova_types.go +++ b/api/v1beta1/nova_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,6 +50,10 @@ type NovaSpecCore struct { // communicate. APIMessageBusInstance string `json:"apiMessageBusInstance"` + // +kubebuilder:validation:Optional + // MessagingBus configuration (username, vhost, and cluster) + MessagingBus rabbitmqv1.RabbitMqConfig `json:"messagingBus,omitempty"` + // +kubebuilder:validation:Optional // +kubebuilder:default={cell0: {cellDatabaseAccount: nova-cell0, hasAPIAccess: true}, cell1: {cellDatabaseAccount: nova-cell1, cellDatabaseInstance: openstack-cell1, cellMessageBusInstance: rabbitmq-cell1, hasAPIAccess: true}} // Cells is a mapping of cell names to NovaCellTemplate objects defining @@ -131,6 +136,10 @@ type NovaSpecCore struct { // Avoid colocating it with RabbitMqClusterName, APIMessageBusInstance or CellMessageBusInstance used for RPC. // For particular Nova cells, notifications cannot be disabled, nor configured differently. NotificationsBusInstance *string `json:"notificationsBusInstance,omitempty"` + + // +kubebuilder:validation:Optional + // NotificationsBus configuration (username, vhost, and cluster) for notifications + NotificationsBus *rabbitmqv1.RabbitMqConfig `json:"notificationsBus,omitempty"` } // NovaSpec defines the desired state of Nova diff --git a/api/v1beta1/nova_webhook.go b/api/v1beta1/nova_webhook.go index b144b1601..c29618669 100644 --- a/api/v1beta1/nova_webhook.go +++ b/api/v1beta1/nova_webhook.go @@ -26,6 +26,7 @@ import ( "fmt" "github.com/google/go-cmp/cmp" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" service "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/robfig/cron/v3" @@ -88,6 +89,24 @@ func (spec *NovaSpecCore) Default() { spec.APITimeout = novaDefaults.APITimeout } + // Default MessagingBus.Cluster from APIMessageBusInstance if not already set + if spec.MessagingBus.Cluster == "" { + spec.MessagingBus.Cluster = spec.APIMessageBusInstance + } + + // Default NotificationsBus if NotificationsBusInstance is specified + if spec.NotificationsBusInstance != nil && *spec.NotificationsBusInstance != "" { + if spec.NotificationsBus == nil { + // Initialize empty NotificationsBus - credentials will be created dynamically + // to ensure separation from MessagingBus (RPC and notifications should never share credentials) + spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{} + } + // Default cluster name if not already set + if spec.NotificationsBus.Cluster == "" { + spec.NotificationsBus.Cluster = *spec.NotificationsBusInstance + } + } + for cellName, cellTemplate := range spec.CellTemplates { if cellTemplate.MetadataServiceTemplate.Enabled == nil { @@ -106,6 +125,11 @@ func (spec *NovaSpecCore) Default() { } } + // Default MessagingBus.Cluster from CellMessageBusInstance if not already set + if cellTemplate.MessagingBus.Cluster == "" { + cellTemplate.MessagingBus.Cluster = cellTemplate.CellMessageBusInstance + } + // "cellTemplate" is a by-value copy, so we need to re-inject the updated version of it into the map spec.CellTemplates[cellName] = cellTemplate } @@ -315,7 +339,34 @@ func (spec *NovaSpec) ValidateUpdate(old NovaSpec, basePath *field.Path, namespa // expected to be called by the validation webhook in the higher level meta // operator func (spec *NovaSpecCore) ValidateUpdate(old NovaSpecCore, basePath *field.Path, namespace string) field.ErrorList { - errors := spec.ValidateCellTemplates(basePath, namespace) + var errors field.ErrorList + + // Reject changes to deprecated messagingBusInstance fields - users should use the new messagingBus fields instead + if spec.APIMessageBusInstance != old.APIMessageBusInstance { + errors = append(errors, field.Forbidden( + basePath.Child("apiMessageBusInstance"), + "apiMessageBusInstance is deprecated and cannot be changed. Please use messagingBus.cluster instead")) + } + + if spec.NotificationsBusInstance != nil && old.NotificationsBusInstance != nil && + *spec.NotificationsBusInstance != *old.NotificationsBusInstance { + errors = append(errors, field.Forbidden( + basePath.Child("notificationsBusInstance"), + "notificationsBusInstance is deprecated and cannot be changed. Please use notificationsBus.cluster instead")) + } + + // Check cell template changes + for cellName, cellTemplate := range spec.CellTemplates { + if oldCell, exists := old.CellTemplates[cellName]; exists { + if cellTemplate.CellMessageBusInstance != oldCell.CellMessageBusInstance { + errors = append(errors, field.Forbidden( + basePath.Child("cellTemplates").Key(cellName).Child("cellMessageBusInstance"), + "cellMessageBusInstance is deprecated and cannot be changed. Please use messagingBus.cluster instead")) + } + } + } + + errors = append(errors, spec.ValidateCellTemplates(basePath, namespace)...) // Validate top-level TopologyRef errors = append(errors, topologyv1.ValidateTopologyRef( spec.TopologyRef, *basePath.Child("topologyRef"), namespace)...) diff --git a/api/v1beta1/novacell_types.go b/api/v1beta1/novacell_types.go index 091e60e00..72c128bd7 100644 --- a/api/v1beta1/novacell_types.go +++ b/api/v1beta1/novacell_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/tls" @@ -51,6 +52,10 @@ type NovaCellTemplate struct { // communicate in this cell. For cell0 it is unused. CellMessageBusInstance string `json:"cellMessageBusInstance"` + // +kubebuilder:validation:Optional + // MessagingBus configuration (username, vhost, and cluster) + MessagingBus rabbitmqv1.RabbitMqConfig `json:"messagingBus,omitempty"` + // +kubebuilder:validation:Required // HasAPIAccess defines if this Cell is configured to have access to the // API DB and message bus. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index e493aca03..026f24def 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1beta1 import ( + rabbitmqv1beta1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1beta1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/service" @@ -505,6 +506,7 @@ func (in *NovaCellStatus) DeepCopy() *NovaCellStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NovaCellTemplate) DeepCopyInto(out *NovaCellTemplate) { *out = *in + out.MessagingBus = in.MessagingBus if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = new(map[string]string) @@ -1659,6 +1661,7 @@ func (in *NovaSpec) DeepCopy() *NovaSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NovaSpecCore) DeepCopyInto(out *NovaSpecCore) { *out = *in + out.MessagingBus = in.MessagingBus if in.CellTemplates != nil { in, out := &in.CellTemplates, &out.CellTemplates *out = make(map[string]NovaCellTemplate, len(*in)) @@ -1691,6 +1694,11 @@ func (in *NovaSpecCore) DeepCopyInto(out *NovaSpecCore) { *out = new(string) **out = **in } + if in.NotificationsBus != nil { + in, out := &in.NotificationsBus, &out.NotificationsBus + *out = new(rabbitmqv1beta1.RabbitMqConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NovaSpecCore. diff --git a/config/crd/bases/nova.openstack.org_nova.yaml b/config/crd/bases/nova.openstack.org_nova.yaml index 58303ff6a..1356b378f 100644 --- a/config/crd/bases/nova.openstack.org_nova.yaml +++ b/config/crd/bases/nova.openstack.org_nova.yaml @@ -540,6 +540,23 @@ spec: MemcachedInstance is the name of the Memcached CR that the services in the cell will use. If defined then this takes precedence over Nova.Spec.MemcachedInstance for this cel type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and + cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object metadataServiceTemplate: description: |- MetadataServiceTemplate - defines the metadata service dedicated for the @@ -1340,6 +1357,22 @@ spec: description: MemcachedInstance is the name of the Memcached CR that all nova service will use. type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object metadataContainerImageURL: description: MetadataContainerImageURL type: string @@ -1648,6 +1681,23 @@ spec: NodeSelector here acts as a default value and can be overridden by service specific NodeSelector Settings. type: object + notificationsBus: + description: NotificationsBus configuration (username, vhost, and + cluster) for notifications + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object notificationsBusInstance: description: |- NotificationsBusInstance is the name of the RabbitMqCluster CR to select diff --git a/go.mod b/go.mod index 4f43fa60c..de50110d7 100644 --- a/go.mod +++ b/go.mod @@ -118,6 +118,9 @@ require ( replace github.com/openstack-k8s-operators/nova-operator/api => ./api +// Use lmiccini's infra-operator branch with status.RabbitmqUserRef support +replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3 + // mschuppert: map to latest commit from release-4.18 tag // must consistent within modules and service operators replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e //allow-merging diff --git a/go.sum b/go.sum index 8ccddddbf..6bd93d784 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3 h1:S1WlXTdXw0PA0yZT1acQ1Jwxf/zLGdF2R9DAxbtpVRw= +github.com/lmiccini/infra-operator/apis v0.0.0-20260116071214-b5e11cdcbab3/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -118,8 +120,6 @@ github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f h1:xcCGJ/g5vvbWhtEJCbv8UeBneI5yrMawm+CXRsJrJZo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251206133124-593df0a7a9e1 h1:qcgbrF9c0axkaDcFGfIA2wGz8bkaxPuXHj3mdKAyz6M= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251206133124-593df0a7a9e1/go.mod h1:0XsZ6Fc4hTV6a/BBP8+jiH8LR+IP5z9aStdPTDHALNk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 h1:pF3mJ3nwq6r4qwom+rEWZNquZpcQW/iftHlJ1KPIDsk= diff --git a/internal/controller/common.go b/internal/controller/common.go index ec23010aa..7eb39f530 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -110,6 +110,14 @@ const ( // the message bus quorum queues configuration QuorumQueuesTemplateKey = "quorum_queues" + // RabbitmqUserNameSelector is the name of key in the internal Secret for + // the RabbitMQUser CR name for the RPC/messaging bus + RabbitmqUserNameSelector = "rabbitmq_user_name" + + // NotificationRabbitmqUserNameSelector is the name of key in the internal + // Secret for the RabbitMQUser CR name for the notifications bus + NotificationRabbitmqUserNameSelector = "notification_rabbitmq_user_name" + // fields to index to reconcile when change passwordSecretField = ".spec.secret" caBundleSecretNameField = ".spec.tls.caBundleSecretName" // #nosec G101 diff --git a/internal/controller/nova_controller.go b/internal/controller/nova_controller.go index f59d01c6e..6a499e400 100644 --- a/internal/controller/nova_controller.go +++ b/internal/controller/nova_controller.go @@ -367,8 +367,8 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul // Create TransportURLs to access the message buses of each cell. Cell0 // message bus is always the same as the top level API message bus so // we create API MQ separately first - apiTransportURL, apiQuorumQueues, apiMQStatus, apiMQError := r.ensureMQ( - ctx, h, instance, instance.Name+"-api-transport", instance.Spec.APIMessageBusInstance) + apiTransportURL, apiRabbitmqUserName, apiQuorumQueues, apiMQStatus, apiMQError := r.ensureMQ( + ctx, h, instance, instance.Name+"-api-transport", instance.Spec.MessagingBus) switch apiMQStatus { case nova.MQFailed: instance.Status.Conditions.Set(condition.FalseCondition( @@ -392,20 +392,19 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul return ctrl.Result{}, fmt.Errorf("%w from for the API MQ: %d", util.ErrInvalidStatus, apiMQStatus) } - // nova broadcaster rabbit - notificationBusName := "" - if instance.Spec.NotificationsBusInstance != nil { - notificationBusName = *instance.Spec.NotificationsBusInstance - } - + // Determine if notifications are enabled by checking NotificationsBus.Cluster + // (the webhook defaults this from the deprecated NotificationsBusInstance field) var notificationTransportURL string + var notificationRabbitmqUserName string var notificationMQStatus nova.MessageBusStatus var notificationMQError error - notificationTransportURLName := instance.Name + "-notification-transport" - if notificationBusName != "" { - notificationTransportURL, _, notificationMQStatus, notificationMQError = r.ensureMQ( - ctx, h, instance, notificationTransportURLName, notificationBusName) + notificationTransportName := instance.Name + "-notification-transport" + if instance.Spec.NotificationsBus != nil && instance.Spec.NotificationsBus.Cluster != "" { + // Use NotificationsBus config (never fall back to MessagingBus to ensure separation) + notificationsRabbitMqConfig := *instance.Spec.NotificationsBus + notificationTransportURL, notificationRabbitmqUserName, _, notificationMQStatus, notificationMQError = r.ensureMQ( + ctx, h, instance, notificationTransportName, notificationsRabbitMqConfig) switch notificationMQStatus { case nova.MQFailed: @@ -443,7 +442,7 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul } for _, url := range transportURLList.Items { - if strings.Contains(url.Name, notificationTransportURLName) { + if strings.Contains(url.Name, notificationTransportName) { err = r.ensureMQDeleted(ctx, instance, url.Name) if err != nil { return ctrl.Result{}, err @@ -453,10 +452,12 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul } cellMQs := map[string]*nova.MessageBus{} + cellRabbitmqUserNames := map[string]string{} var failedMQs []string var creatingMQs []string for _, cellName := range orderedCellNames { var cellTransportURL string + var cellRabbitmqUserName string var status nova.MessageBusStatus var err error cellTemplate := instance.Spec.CellTemplates[cellName] @@ -465,12 +466,13 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul // API message bus instead if cellName == novav1.Cell0Name { cellTransportURL = apiTransportURL + cellRabbitmqUserName = apiRabbitmqUserName cellQuorumQueues = apiQuorumQueues status = apiMQStatus err = apiMQError } else { - cellTransportURL, cellQuorumQueues, status, err = r.ensureMQ( - ctx, h, instance, instance.Name+"-"+cellName+"-transport", cellTemplate.CellMessageBusInstance) + cellTransportURL, cellRabbitmqUserName, cellQuorumQueues, status, err = r.ensureMQ( + ctx, h, instance, instance.Name+"-"+cellName+"-transport", cellTemplate.MessagingBus) } switch status { case nova.MQFailed: @@ -482,6 +484,7 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul return ctrl.Result{}, fmt.Errorf("%w from ensureMQ: %d for cell %s", util.ErrInvalidStatus, status, cellName) } cellMQs[cellName] = &nova.MessageBus{TransportURL: cellTransportURL, QuorumQueues: cellQuorumQueues, Status: status} + cellRabbitmqUserNames[cellName] = cellRabbitmqUserName } if len(failedMQs) > 0 { instance.Status.Conditions.Set(condition.FalseCondition( @@ -544,7 +547,8 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul } cell, status, err := r.ensureCell( ctx, h, instance, cellName, cellTemplate, - cellDB.Database, apiDB, cellMQ.TransportURL, cellMQ.QuorumQueues, notificationTransportURL, + cellDB.Database, apiDB, cellMQ.TransportURL, cellRabbitmqUserNames[cellName], cellMQ.QuorumQueues, + notificationTransportURL, notificationRabbitmqUserName, keystoneInternalAuthURL, secret, ) cells[cellName] = cell @@ -611,8 +615,8 @@ func (r *NovaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resul topLevelSecretName, err := r.ensureTopLevelSecret( ctx, h, instance, - apiTransportURL, apiQuorumQueues, - notificationTransportURL, + apiTransportURL, apiRabbitmqUserName, apiQuorumQueues, + notificationTransportURL, notificationRabbitmqUserName, secret) if err != nil { return ctrl.Result{}, err @@ -1199,8 +1203,10 @@ func (r *NovaReconciler) ensureCell( cellDB *mariadbv1.Database, apiDB *mariadbv1.Database, cellTransportURL string, + cellRabbitmqUserName string, cellQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, keystoneAuthURL string, secret corev1.Secret, ) (*novav1.NovaCell, nova.CellDeploymentStatus, error) { @@ -1208,7 +1214,8 @@ func (r *NovaReconciler) ensureCell( cellSecretName, err := r.ensureCellSecret( ctx, h, instance, cellName, cellTemplate, - cellTransportURL, cellQuorumQueues, notificationTransportURL, + cellTransportURL, cellRabbitmqUserName, cellQuorumQueues, + notificationTransportURL, notificationRabbitmqUserName, secret) if err != nil { return nil, nova.CellDeploying, err @@ -1711,8 +1718,8 @@ func (r *NovaReconciler) ensureMQ( h *helper.Helper, instance *novav1.Nova, transportName string, - messageBusInstanceName string, -) (string, bool, nova.MessageBusStatus, error) { + rabbitMqConfig rabbitmqv1.RabbitMqConfig, +) (string, string, bool, nova.MessageBusStatus, error) { Log := r.GetLogger(ctx) transportURL := &rabbitmqv1.TransportURL{ ObjectMeta: metav1.ObjectMeta{ @@ -1722,14 +1729,19 @@ func (r *NovaReconciler) ensureMQ( } op, err := controllerutil.CreateOrPatch(ctx, r.Client, transportURL, func() error { - transportURL.Spec.RabbitmqClusterName = messageBusInstanceName + transportURL.Spec.RabbitmqClusterName = rabbitMqConfig.Cluster + if rabbitMqConfig.User != "" { + transportURL.Spec.Username = rabbitMqConfig.User + } + // Always set Vhost - empty string means use default "/" vhost + transportURL.Spec.Vhost = rabbitMqConfig.Vhost err := controllerutil.SetControllerReference(instance, transportURL, r.Scheme) return err }) if err != nil && !k8s_errors.IsNotFound(err) { - return "", false, nova.MQFailed, util.WrapErrorForObject( + return "", "", false, nova.MQFailed, util.WrapErrorForObject( fmt.Sprintf("Error create or update TransportURL object %s", transportName), transportURL, err, @@ -1738,12 +1750,12 @@ func (r *NovaReconciler) ensureMQ( if op != controllerutil.OperationResultNone { Log.Info(fmt.Sprintf("TransportURL object %s created or patched", transportName)) - return "", false, nova.MQCreating, nil + return "", "", false, nova.MQCreating, nil } err = r.Client.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: transportName}, transportURL) if err != nil && !k8s_errors.IsNotFound(err) { - return "", false, nova.MQFailed, util.WrapErrorForObject( + return "", "", false, nova.MQFailed, util.WrapErrorForObject( fmt.Sprintf("Error reading TransportURL object %s", transportName), transportURL, err, @@ -1751,7 +1763,7 @@ func (r *NovaReconciler) ensureMQ( } if k8s_errors.IsNotFound(err) || !transportURL.IsReady() || transportURL.Status.SecretName == "" { - return "", false, nova.MQCreating, nil + return "", "", false, nova.MQCreating, nil } secretName := types.NamespacedName{Namespace: instance.Namespace, Name: transportURL.Status.SecretName} @@ -1760,14 +1772,14 @@ func (r *NovaReconciler) ensureMQ( err = h.GetClient().Get(ctx, secretName, secret) if err != nil { if k8s_errors.IsNotFound(err) { - return "", false, nova.MQCreating, nil + return "", "", false, nova.MQCreating, nil } - return "", false, nova.MQFailed, err + return "", "", false, nova.MQFailed, err } url, ok := secret.Data[TransportURLSelector] if !ok { - return "", false, nova.MQFailed, fmt.Errorf( + return "", "", false, nova.MQFailed, fmt.Errorf( "%w: the TransportURL secret %s does not have 'transport_url' field", util.ErrFieldNotFound, transportURL.Status.SecretName) } @@ -1777,7 +1789,13 @@ func (r *NovaReconciler) ensureMQ( quorumQueues = string(val) == "true" } - return string(url), quorumQueues, nova.MQCompleted, nil + // Get the RabbitMQUser CR name from the TransportURL status + // The infra-operator populates status.RabbitmqUserRef with the name of the RabbitMQUser CR + // that was created or referenced by this TransportURL. + // Empty string means using default RabbitMQ user (no dedicated RabbitMQUser CR) + rabbitmqUserName := transportURL.Status.RabbitmqUserRef + + return string(url), rabbitmqUserName, quorumQueues, nova.MQCompleted, nil } func (r *NovaReconciler) ensureMQDeleted( @@ -2004,8 +2022,10 @@ func (r *NovaReconciler) ensureCellSecret( cellName string, cellTemplate novav1.NovaCellTemplate, cellTransportURL string, + cellRabbitmqUserName string, cellQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, externalSecret corev1.Secret, ) (string, error) { // NOTE(gibi): We can move other sensitive data to the internal Secret from @@ -2016,10 +2036,12 @@ func (r *NovaReconciler) ensureCellSecret( } data := map[string]string{ - ServicePasswordSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.Service]), - TransportURLSelector: cellTransportURL, - NotificationTransportURLSelector: notificationTransportURL, - QuorumQueuesTemplateKey: quorumQueuesValue, + ServicePasswordSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.Service]), + TransportURLSelector: cellTransportURL, + RabbitmqUserNameSelector: cellRabbitmqUserName, + NotificationTransportURLSelector: notificationTransportURL, + NotificationRabbitmqUserNameSelector: notificationRabbitmqUserName, + QuorumQueuesTemplateKey: quorumQueuesValue, } // If metadata is enabled in the cell then the cell secret needs the @@ -2063,8 +2085,10 @@ func (r *NovaReconciler) ensureTopLevelSecret( h *helper.Helper, instance *novav1.Nova, apiTransportURL string, + apiRabbitmqUserName string, apiQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, externalSecret corev1.Secret, ) (string, error) { // NOTE(gibi): We can move other sensitive data to the internal Secret from @@ -2075,11 +2099,13 @@ func (r *NovaReconciler) ensureTopLevelSecret( } data := map[string]string{ - ServicePasswordSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.Service]), - MetadataSecretSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.MetadataSecret]), - TransportURLSelector: apiTransportURL, - NotificationTransportURLSelector: notificationTransportURL, - QuorumQueuesTemplateKey: quorumQueuesValue, + ServicePasswordSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.Service]), + MetadataSecretSelector: string(externalSecret.Data[instance.Spec.PasswordSelectors.MetadataSecret]), + TransportURLSelector: apiTransportURL, + RabbitmqUserNameSelector: apiRabbitmqUserName, + NotificationTransportURLSelector: notificationTransportURL, + NotificationRabbitmqUserNameSelector: notificationRabbitmqUserName, + QuorumQueuesTemplateKey: quorumQueuesValue, } // NOTE(gibi): When we switch to immutable secrets then we need to include diff --git a/test/functional/nova_controller_test.go b/test/functional/nova_controller_test.go index 59bd0a67f..ab0fc4ae6 100644 --- a/test/functional/nova_controller_test.go +++ b/test/functional/nova_controller_test.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" mariadb_test "github.com/openstack-k8s-operators/mariadb-operator/api/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" @@ -77,13 +78,15 @@ var _ = Describe("Nova controller - notifications", func() { It("notification transport url is set with new rabbit", func() { - // add new-rabbit in Nova CR + // add new-rabbit in Nova CR using the new notificationsBus API notificationsBus := GetNotificationsBusNames(novaNames.NovaName) DeferCleanup(k8sClient.Delete, ctx, CreateNotificationTransportURLSecret(notificationsBus)) Eventually(func(g Gomega) { nova := GetNova(novaNames.NovaName) - nova.Spec.NotificationsBusInstance = ¬ificationsBus.BusName + nova.Spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{ + Cluster: notificationsBus.BusName, + } g.Expect(k8sClient.Update(ctx, nova)).Should(Succeed()) }, timeout, interval).Should(Succeed()) @@ -123,10 +126,10 @@ var _ = Describe("Nova controller - notifications", func() { configData = string(configDataMap.Data["01-nova.conf"]) AssertHaveNotificationTransportURL(notificationsBus.TransportURLName.Name, configData) - // cleanup notifications transporturl + // cleanup notifications transporturl by clearing the notificationsBus Eventually(func(g Gomega) { nova := GetNova(novaNames.NovaName) - nova.Spec.NotificationsBusInstance = nil + nova.Spec.NotificationsBus = nil g.Expect(k8sClient.Update(ctx, nova)).Should(Succeed()) }, timeout, interval).Should(Succeed()) @@ -534,13 +537,17 @@ var _ = Describe("Nova controller", func() { // proper content and the cell subCRs are configured to use the // internal secret internalCellSecret := th.GetSecret(cell0.InternalCellSecretName) - Expect(internalCellSecret.Data).To(HaveLen(4)) + Expect(internalCellSecret.Data).To(HaveLen(6)) Expect(internalCellSecret.Data).To( HaveKeyWithValue(controllers.ServicePasswordSelector, []byte("service-password"))) Expect(internalCellSecret.Data).To( HaveKeyWithValue("transport_url", []byte("rabbit://cell0/fake"))) Expect(internalCellSecret.Data).To( HaveKeyWithValue("notification_transport_url", []byte(""))) + Expect(internalCellSecret.Data).To( + HaveKey(controllers.RabbitmqUserNameSelector)) + Expect(internalCellSecret.Data).To( + HaveKey(controllers.NotificationRabbitmqUserNameSelector)) Expect(cell.Spec.Secret).To(Equal(cell0.InternalCellSecretName.Name)) Expect(conductor.Spec.Secret).To(Equal(cell0.InternalCellSecretName.Name)) @@ -650,7 +657,7 @@ var _ = Describe("Nova controller", func() { // assert that a the top level internal internal secret is created // with the proper data internalTopLevelSecret := th.GetSecret(novaNames.InternalTopLevelSecretName) - Expect(internalTopLevelSecret.Data).To(HaveLen(5)) + Expect(internalTopLevelSecret.Data).To(HaveLen(7)) Expect(internalTopLevelSecret.Data).To( HaveKeyWithValue(controllers.ServicePasswordSelector, []byte("service-password"))) Expect(internalTopLevelSecret.Data).To( @@ -659,6 +666,10 @@ var _ = Describe("Nova controller", func() { HaveKeyWithValue("transport_url", []byte("rabbit://cell0/fake"))) Expect(internalTopLevelSecret.Data).To( HaveKeyWithValue("notification_transport_url", []byte(""))) + Expect(internalTopLevelSecret.Data).To( + HaveKey(controllers.RabbitmqUserNameSelector)) + Expect(internalTopLevelSecret.Data).To( + HaveKey(controllers.NotificationRabbitmqUserNameSelector)) }) It("creates NovaAPI", func() { diff --git a/test/functional/nova_reconfiguration_test.go b/test/functional/nova_reconfiguration_test.go index 29be006a1..442054c62 100644 --- a/test/functional/nova_reconfiguration_test.go +++ b/test/functional/nova_reconfiguration_test.go @@ -689,7 +689,8 @@ var _ = Describe("Nova reconfiguration", func() { nova := GetNova(novaNames.NovaName) cell1 := nova.Spec.CellTemplates["cell1"] - cell1.CellMessageBusInstance = "alternate-mq-for-cell1" + // Use the new messagingBus.cluster field instead of the deprecated cellMessageBusInstance + cell1.MessagingBus.Cluster = "alternate-mq-for-cell1" nova.Spec.CellTemplates["cell1"] = cell1 g.Expect(k8sClient.Update(ctx, nova)).To(Succeed()) diff --git a/test/kuttl/test-suites/default/nova-notificationsbus/00-cleanup-nova.yaml b/test/kuttl/test-suites/default/nova-notificationsbus/00-cleanup-nova.yaml new file mode 100644 index 000000000..b96266521 --- /dev/null +++ b/test/kuttl/test-suites/default/nova-notificationsbus/00-cleanup-nova.yaml @@ -0,0 +1,6 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl + namespace: nova-kuttl-default +$patch: delete diff --git a/test/kuttl/test-suites/default/nova-notificationsbus/01-assert.yaml b/test/kuttl/test-suites/default/nova-notificationsbus/01-assert.yaml new file mode 100644 index 000000000..7b25bfbce --- /dev/null +++ b/test/kuttl/test-suites/default/nova-notificationsbus/01-assert.yaml @@ -0,0 +1,65 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl +status: + conditions: + - message: Setup complete + reason: Ready + status: "True" + type: Ready + - message: Notification message bus created successfully + reason: Ready + status: "True" + type: NovaNotificationMQReady +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: nova-kuttl-nova-transport +spec: + rabbitmqClusterName: rabbitmq +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: nova-kuttl-nova-notification-rabbitmq-notifications +spec: + rabbitmqClusterName: rabbitmq-notifications +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +# Verify that 2 TransportURL CRs were created (separate clusters) +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + # Count TransportURL CRs - should be at least 2 (one for messaging, one for notifications) + count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "nova-kuttl-nova" | wc -l) + if [ "$count" -lt "2" ]; then + echo "Expected at least 2 TransportURLs for nova, found $count" + exit 1 + fi + echo "Correctly found $count TransportURLs (separate clusters)" + + # Verify that nova.conf contains the notifications transport_url + NOVA_API_POD=$(kubectl get pods -n $NAMESPACE -l "service=nova-api" -o custom-columns=:metadata.name --no-headers | grep -v ^$ | head -1) + if [ -z "${NOVA_API_POD}" ]; then + echo "No nova-api pod found" + exit 1 + fi + # Verify oslo_messaging_notifications section has transport_url configured + kubectl exec -n $NAMESPACE ${NOVA_API_POD} -c nova-kuttl-api-api -- cat /etc/nova/nova.conf.d/01-default.conf | grep -A 2 '\[oslo_messaging_notifications\]' | grep -q 'transport_url' + echo "Successfully verified notifications transport_url in nova.conf" + exit 0 diff --git a/test/kuttl/test-suites/default/nova-notificationsbus/01-deploy.yaml b/test/kuttl/test-suites/default/nova-notificationsbus/01-deploy.yaml new file mode 100644 index 000000000..0e5bd9de0 --- /dev/null +++ b/test/kuttl/test-suites/default/nova-notificationsbus/01-deploy.yaml @@ -0,0 +1,16 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl +spec: + secret: osp-secret + messagingBus: + cluster: rabbitmq + notificationsBus: + cluster: rabbitmq-notifications + cellTemplates: + cell0: + cellDatabaseInstance: openstack + cellDatabaseAccount: nova-cell0 + hasAPIAccess: true + memcachedInstance: memcached diff --git a/test/kuttl/test-suites/default/nova-same-cluster/00-cleanup-nova.yaml b/test/kuttl/test-suites/default/nova-same-cluster/00-cleanup-nova.yaml new file mode 100644 index 000000000..b96266521 --- /dev/null +++ b/test/kuttl/test-suites/default/nova-same-cluster/00-cleanup-nova.yaml @@ -0,0 +1,6 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl + namespace: nova-kuttl-default +$patch: delete diff --git a/test/kuttl/test-suites/default/nova-same-cluster/01-assert.yaml b/test/kuttl/test-suites/default/nova-same-cluster/01-assert.yaml new file mode 100644 index 000000000..78c4fcc9c --- /dev/null +++ b/test/kuttl/test-suites/default/nova-same-cluster/01-assert.yaml @@ -0,0 +1,61 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl +status: + conditions: + - message: Setup complete + reason: Ready + status: "True" + type: Ready + - message: Notification message bus created successfully + reason: Ready + status: "True" + type: NovaNotificationMQReady +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: nova-kuttl-nova-transport +spec: + rabbitmqClusterName: rabbitmq +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +# Verify that NO second TransportURL was created (same cluster optimization) +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + # Count TransportURL CRs for nova - should be exactly 1 (API transport) + # Note: Nova may have cell TransportURLs too, so we only count the main nova transport + nova_transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "nova-kuttl-nova-transport" | wc -l) + notification_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "nova-kuttl-nova-notification" | wc -l) + + if [ "$nova_transport_count" -ne "1" ]; then + echo "Expected 1 nova-transport TransportURL, found $nova_transport_count" + exit 1 + fi + + if [ "$notification_count" -ne "0" ]; then + echo "Expected 0 notification TransportURL (same-cluster optimization), found $notification_count" + exit 1 + fi + + echo "Correctly found 1 TransportURL (same-cluster optimization working)" + + # Verify that nova.conf still has notifications transport_url configured (reusing main transport) + NOVA_API_POD=$(kubectl get pods -n $NAMESPACE -l "service=nova-api" -o custom-columns=:metadata.name --no-headers | grep -v ^$ | head -1) + if [ -z "${NOVA_API_POD}" ]; then + echo "No nova-api pod found" + exit 1 + fi + # Verify oslo_messaging_notifications section has transport_url configured + kubectl exec -n $NAMESPACE ${NOVA_API_POD} -c nova-kuttl-api-api -- cat /etc/nova/nova.conf.d/01-default.conf | grep -A 2 '\[oslo_messaging_notifications\]' | grep -q 'transport_url' + echo "Successfully verified notifications transport_url in nova.conf (using same cluster)" + exit 0 diff --git a/test/kuttl/test-suites/default/nova-same-cluster/01-deploy.yaml b/test/kuttl/test-suites/default/nova-same-cluster/01-deploy.yaml new file mode 100644 index 000000000..d271b0ec5 --- /dev/null +++ b/test/kuttl/test-suites/default/nova-same-cluster/01-deploy.yaml @@ -0,0 +1,16 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl +spec: + secret: osp-secret + messagingBus: + cluster: rabbitmq + notificationsBus: + cluster: rabbitmq + cellTemplates: + cell0: + cellDatabaseInstance: openstack + cellDatabaseAccount: nova-cell0 + hasAPIAccess: true + memcachedInstance: memcached