diff --git a/api/bases/nova.openstack.org_nova.yaml b/api/bases/nova.openstack.org_nova.yaml index c3f3b5f8b..322c6ee19 100644 --- a/api/bases/nova.openstack.org_nova.yaml +++ b/api/bases/nova.openstack.org_nova.yaml @@ -54,11 +54,11 @@ spec: Service instance used for the Nova API DB. type: string apiMessageBusInstance: - default: rabbitmq description: |- APIMessageBusInstance is the name of the RabbitMqCluster CR to select the Message Bus Service instance used by the Nova top level services to communicate. + Deprecated: Use MessagingBus.Cluster instead type: string apiServiceTemplate: default: @@ -398,11 +398,11 @@ spec: Service instance used as the DB of this cell. type: string cellMessageBusInstance: - default: rabbitmq description: |- CellMessageBusInstance is the name of the RabbitMqCluster CR to select the Message Bus Service instance used by the nova services to communicate in this cell. For cell0 it is unused. + Deprecated: Use MessagingBus.Cluster instead type: string conductorServiceTemplate: description: ConductorServiceTemplate - defines the cell conductor @@ -550,6 +550,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 @@ -1325,8 +1342,9 @@ spec: cell1: cellDatabaseAccount: nova-cell1 cellDatabaseInstance: openstack-cell1 - cellMessageBusInstance: rabbitmq-cell1 hasAPIAccess: true + messagingBus: + cluster: rabbitmq-cell1 description: |- Cells is a mapping of cell names to NovaCellTemplate objects defining the cells in the deployment. The "cell0" cell is a mandatory cell in @@ -1350,6 +1368,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 @@ -1658,6 +1692,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 9e4d78fc1..22d1a9626 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,8 +4,8 @@ go 1.24.4 require ( github.com/google/go-cmp v0.7.0 - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260123105816-865d02e287a9 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a github.com/robfig/cron/v3 v3.0.1 k8s.io/api v0.31.14 k8s.io/apimachinery v0.31.14 @@ -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 @@ -40,12 +39,12 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.27.5 // indirect - github.com/onsi/gomega v1.39.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect 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 diff --git a/api/go.sum b/api/go.sum index c741f5c9f..5cbe305ee 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= @@ -78,10 +79,12 @@ github.com/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260123105816-865d02e287a9 h1:tD6nnTRcyUCXdVMWPHLApk12tzQlQni5eoxvQ8XdbP8= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260123105816-865d02e287a9/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= -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/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 h1:vhAGLKZitJIffj7ONiPpKmOX7Tmt/LGJpaY0Z2LeyfQ= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a h1:97OfmmJgoIKTfbED2SfyjoPkivoiMHg4jfbrTuwSGQw= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0= +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/common_webhook.go b/api/v1beta1/common_webhook.go index 6f3397dc1..7bbc52681 100644 --- a/api/v1beta1/common_webhook.go +++ b/api/v1beta1/common_webhook.go @@ -21,6 +21,7 @@ import ( "path/filepath" "strings" + common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -58,3 +59,128 @@ func matchAny(requested string, allowed []string) bool { } return false } + +// getDeprecatedFields returns the centralized list of deprecated fields for NovaSpecCore +func (spec *NovaSpecCore) getDeprecatedFields(old *NovaSpecCore) []common_webhook.DeprecatedFieldUpdate { + // Get new field value (handle nil NotificationsBus) + var newNotifBusCluster *string + if spec.NotificationsBus != nil { + newNotifBusCluster = &spec.NotificationsBus.Cluster + } + + deprecatedFields := []common_webhook.DeprecatedFieldUpdate{ + { + DeprecatedFieldName: "apiMessageBusInstance", + NewFieldPath: []string{"messagingBus", "cluster"}, + NewDeprecatedValue: &spec.APIMessageBusInstance, + NewValue: &spec.MessagingBus.Cluster, + }, + { + DeprecatedFieldName: "notificationsBusInstance", + NewFieldPath: []string{"notificationsBus", "cluster"}, + NewDeprecatedValue: spec.NotificationsBusInstance, + NewValue: newNotifBusCluster, + }, + } + + // If old spec is provided (UPDATE operation), add old values + if old != nil { + deprecatedFields[0].OldDeprecatedValue = &old.APIMessageBusInstance + deprecatedFields[1].OldDeprecatedValue = old.NotificationsBusInstance + } + + return deprecatedFields +} + +// validateDeprecatedFieldsCreate validates deprecated fields during CREATE operations +func (spec *NovaSpecCore) validateDeprecatedFieldsCreate(basePath *field.Path) ([]string, field.ErrorList) { + // Get deprecated fields list (without old values for CREATE) + deprecatedFieldsUpdate := spec.getDeprecatedFields(nil) + + // Convert to DeprecatedField list for CREATE validation + deprecatedFields := make([]common_webhook.DeprecatedField, len(deprecatedFieldsUpdate)) + for i, df := range deprecatedFieldsUpdate { + deprecatedFields[i] = common_webhook.DeprecatedField{ + DeprecatedFieldName: df.DeprecatedFieldName, + NewFieldPath: df.NewFieldPath, + DeprecatedValue: df.NewDeprecatedValue, + NewValue: df.NewValue, + } + } + + // Validate top-level NovaSpecCore fields + warnings := common_webhook.ValidateDeprecatedFieldsCreate(deprecatedFields, basePath) + + // Validate deprecated fields in cell templates + for cellName, cellTemplate := range spec.CellTemplates { + cellPath := basePath.Child("cellTemplates").Key(cellName) + cellWarnings := cellTemplate.validateDeprecatedFieldsCreate(cellPath) + warnings = append(warnings, cellWarnings...) + } + + return warnings, nil +} + +// validateDeprecatedFieldsUpdate validates deprecated fields during UPDATE operations +func (spec *NovaSpecCore) validateDeprecatedFieldsUpdate(old NovaSpecCore, basePath *field.Path) ([]string, field.ErrorList) { + // Get deprecated fields list with old values + deprecatedFields := spec.getDeprecatedFields(&old) + warnings, errors := common_webhook.ValidateDeprecatedFieldsUpdate(deprecatedFields, basePath) + + // Validate deprecated fields in cell templates + for cellName, cellTemplate := range spec.CellTemplates { + if oldCell, exists := old.CellTemplates[cellName]; exists { + cellPath := basePath.Child("cellTemplates").Key(cellName) + cellWarnings, cellErrors := cellTemplate.validateDeprecatedFieldsUpdate(oldCell, cellPath) + warnings = append(warnings, cellWarnings...) + errors = append(errors, cellErrors...) + } + } + + return warnings, errors +} + +// getDeprecatedFields returns the centralized list of deprecated fields for NovaCellTemplate +func (spec *NovaCellTemplate) getDeprecatedFields(old *NovaCellTemplate) []common_webhook.DeprecatedFieldUpdate { + deprecatedFields := []common_webhook.DeprecatedFieldUpdate{ + { + DeprecatedFieldName: "cellMessageBusInstance", + NewFieldPath: []string{"messagingBus", "cluster"}, + NewDeprecatedValue: &spec.CellMessageBusInstance, + NewValue: &spec.MessagingBus.Cluster, + }, + } + + // If old spec is provided (UPDATE operation), add old values + if old != nil { + deprecatedFields[0].OldDeprecatedValue = &old.CellMessageBusInstance + } + + return deprecatedFields +} + +// validateDeprecatedFieldsCreate validates deprecated fields during CREATE operations for NovaCellTemplate +func (spec *NovaCellTemplate) validateDeprecatedFieldsCreate(basePath *field.Path) []string { + // Get deprecated fields list (without old values for CREATE) + deprecatedFieldsUpdate := spec.getDeprecatedFields(nil) + + // Convert to DeprecatedField list for CREATE validation + deprecatedFields := make([]common_webhook.DeprecatedField, len(deprecatedFieldsUpdate)) + for i, df := range deprecatedFieldsUpdate { + deprecatedFields[i] = common_webhook.DeprecatedField{ + DeprecatedFieldName: df.DeprecatedFieldName, + NewFieldPath: df.NewFieldPath, + DeprecatedValue: df.NewDeprecatedValue, + NewValue: df.NewValue, + } + } + + return common_webhook.ValidateDeprecatedFieldsCreate(deprecatedFields, basePath) +} + +// validateDeprecatedFieldsUpdate validates deprecated fields during UPDATE operations for NovaCellTemplate +func (spec *NovaCellTemplate) validateDeprecatedFieldsUpdate(old NovaCellTemplate, basePath *field.Path) ([]string, field.ErrorList) { + // Get deprecated fields list with old values + deprecatedFields := spec.getDeprecatedFields(&old) + return common_webhook.ValidateDeprecatedFieldsUpdate(deprecatedFields, basePath) +} diff --git a/api/v1beta1/nova_types.go b/api/v1beta1/nova_types.go index 7b22d24b7..39d4a5c11 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" @@ -43,14 +44,18 @@ type NovaSpecCore struct { APIDatabaseInstance string `json:"apiDatabaseInstance"` // +kubebuilder:validation:Optional - // +kubebuilder:default=rabbitmq // APIMessageBusInstance is the name of the RabbitMqCluster CR to select // the Message Bus Service instance used by the Nova top level services to // communicate. - APIMessageBusInstance string `json:"apiMessageBusInstance"` + // Deprecated: Use MessagingBus.Cluster instead + APIMessageBusInstance string `json:"apiMessageBusInstance,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:default={cell0: {cellDatabaseAccount: nova-cell0, hasAPIAccess: true}, cell1: {cellDatabaseAccount: nova-cell1, cellDatabaseInstance: openstack-cell1, cellMessageBusInstance: rabbitmq-cell1, hasAPIAccess: true}} + // 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, messagingBus: {cluster: rabbitmq-cell1}, hasAPIAccess: true}} // Cells is a mapping of cell names to NovaCellTemplate objects defining // the cells in the deployment. The "cell0" cell is a mandatory cell in // every deployment. Moreover any real deployment needs at least one @@ -130,7 +135,11 @@ type NovaSpecCore struct { // An empty value "" leaves the notification drivers unconfigured and emitting no notifications at all. // 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"` + NotificationsBusInstance *string `json:"notificationsBusInstance,omitempty" deprecated:"true" deprecatedNew:"notificationsBus.cluster"` + + // +kubebuilder:validation:Optional + // NotificationsBus configuration (username, vhost, and cluster) for notifications + NotificationsBus *rabbitmqv1.RabbitMqConfig `json:"notificationsBus,omitempty"` // +kubebuilder:validation:Optional // +operator-sdk:csv:customresourcedefinitions:type=spec diff --git a/api/v1beta1/nova_webhook.go b/api/v1beta1/nova_webhook.go index c4c28ebc3..1e5762c79 100644 --- a/api/v1beta1/nova_webhook.go +++ b/api/v1beta1/nova_webhook.go @@ -88,6 +88,15 @@ func (spec *NovaSpecCore) Default() { spec.APITimeout = novaDefaults.APITimeout } + // Default MessagingBus.Cluster if not set + // Migration from deprecated fields is handled by openstack-operator + if spec.MessagingBus.Cluster == "" { + spec.MessagingBus.Cluster = "rabbitmq" + } + + // NotificationsBus.Cluster is not defaulted - it must be explicitly set if NotificationsBus is configured + // This ensures users make a conscious choice about which cluster to use for notifications + for cellName, cellTemplate := range spec.CellTemplates { if cellTemplate.MetadataServiceTemplate.Enabled == nil { @@ -106,6 +115,16 @@ func (spec *NovaSpecCore) Default() { } } + // Default MessagingBus.Cluster if not set + // Migration from deprecated fields is handled by openstack-operator + if cellTemplate.MessagingBus.Cluster == "" { + if cellName == Cell0Name { + cellTemplate.MessagingBus.Cluster = "rabbitmq" + } else { + cellTemplate.MessagingBus.Cluster = "rabbitmq-" + cellName + } + } + // "cellTemplate" is a by-value copy, so we need to re-inject the updated version of it into the map spec.CellTemplates[cellName] = cellTemplate } @@ -138,10 +157,26 @@ func (spec *NovaSpecCore) ValidateCellTemplates(basePath *field.Path, namespace cell.TopologyRef, *basePath.Child("topologyRef"), namespace)...) if name != Cell0Name { - if dupName, ok := cellMessageBusNames[cell.CellMessageBusInstance]; ok { + // Determine which rabbit cluster this cell is using + // Prefer the new MessagingBus.Cluster field, fall back to deprecated CellMessageBusInstance + var cellCluster string + if cell.MessagingBus.Cluster != "" { + cellCluster = cell.MessagingBus.Cluster + } else { + cellCluster = cell.CellMessageBusInstance + } + + // Check if this rabbit cluster is already used by another cell + if dupName, ok := cellMessageBusNames[cellCluster]; ok { + // Determine which field to report the error on + fieldPath := cellPath.Child("messagingBus").Child("cluster") + if cell.MessagingBus.Cluster == "" { + fieldPath = cellPath.Child("cellMessageBusInstance") + } + errors = append(errors, field.Invalid( - cellPath.Child("cellMessageBusInstance"), - cell.CellMessageBusInstance, + fieldPath, + cellCluster, fmt.Sprintf( "RabbitMqCluster CR need to be uniq per cell. It's duplicated with cell: %s", dupName), @@ -149,7 +184,7 @@ func (spec *NovaSpecCore) ValidateCellTemplates(basePath *field.Path, namespace ) } - cellMessageBusNames[cell.CellMessageBusInstance] = name + cellMessageBusNames[cellCluster] = name } if *cell.MetadataServiceTemplate.Enabled && *spec.MetadataServiceTemplate.Enabled { errors = append( @@ -260,16 +295,24 @@ func (spec *NovaSpecCore) ValidateSchedulerServiceTemplate(basePath *field.Path, return errors } + // ValidateCreate validates the NovaSpec during the webhook invocation. -func (spec *NovaSpec) ValidateCreate(basePath *field.Path, namespace string) field.ErrorList { +func (spec *NovaSpec) ValidateCreate(basePath *field.Path, namespace string) (admission.Warnings, field.ErrorList) { return spec.NovaSpecCore.ValidateCreate(basePath, namespace) } // ValidateCreate validates the NovaSpecCore during the webhook invocation. It is // expected to be called by the validation webhook in the higher level meta // operator -func (spec *NovaSpecCore) ValidateCreate(basePath *field.Path, namespace string) field.ErrorList { - errors := spec.ValidateCellTemplates(basePath, namespace) +func (spec *NovaSpecCore) ValidateCreate(basePath *field.Path, namespace string) (admission.Warnings, field.ErrorList) { + var warnings admission.Warnings + + // Validate deprecated fields + deprecatedWarnings, deprecatedErrors := spec.validateDeprecatedFieldsCreate(basePath) + warnings = append(warnings, deprecatedWarnings...) + + errors := deprecatedErrors + errors = append(errors, spec.ValidateCellTemplates(basePath, namespace)...) errors = append(errors, spec.ValidateAPIServiceTemplate(basePath, namespace)...) errors = append(errors, spec.ValidateSchedulerServiceTemplate(basePath, namespace)...) @@ -289,33 +332,41 @@ func (spec *NovaSpecCore) ValidateCreate(basePath *field.Path, namespace string) topologyv1.ValidateTopologyRef( spec.TopologyRef, *basePath.Child("topologyRef"), namespace)...) - return errors + return warnings, errors } // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *Nova) ValidateCreate() (admission.Warnings, error) { novalog.Info("validate create", "name", r.Name) - errors := r.Spec.ValidateCreate(field.NewPath("spec"), r.Namespace) + warnings, errors := r.Spec.ValidateCreate(field.NewPath("spec"), r.Namespace) if len(errors) != 0 { novalog.Info("validation failed", "name", r.Name) - return nil, apierrors.NewInvalid( + return warnings, apierrors.NewInvalid( schema.GroupKind{Group: "nova.openstack.org", Kind: "Nova"}, r.Name, errors) } - return nil, nil + return warnings, nil } // ValidateUpdate validates the NovaSpec during the webhook invocation. -func (spec *NovaSpec) ValidateUpdate(old NovaSpec, basePath *field.Path, namespace string) field.ErrorList { +func (spec *NovaSpec) ValidateUpdate(old NovaSpec, basePath *field.Path, namespace string) (admission.Warnings, field.ErrorList) { return spec.NovaSpecCore.ValidateUpdate(old.NovaSpecCore, basePath, namespace) } // ValidateUpdate validates the NovaSpecCore during the webhook invocation. It is // 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) +func (spec *NovaSpecCore) ValidateUpdate(old NovaSpecCore, basePath *field.Path, namespace string) (admission.Warnings, field.ErrorList) { + var errors field.ErrorList + var warnings admission.Warnings + + // Validate deprecated fields + deprecatedWarnings, deprecatedErrors := spec.validateDeprecatedFieldsUpdate(old, basePath) + warnings = append(warnings, deprecatedWarnings...) + errors = append(errors, deprecatedErrors...) + + errors = append(errors, spec.ValidateCellTemplates(basePath, namespace)...) // Validate top-level TopologyRef errors = append(errors, topologyv1.ValidateTopologyRef( spec.TopologyRef, *basePath.Child("topologyRef"), namespace)...) @@ -334,7 +385,7 @@ func (spec *NovaSpecCore) ValidateUpdate(old NovaSpecCore, basePath *field.Path, spec.MetadataServiceTemplate.ValidateDefaultConfigOverwrite( basePath.Child("metadataServiceTemplate"))...) - return errors + return warnings, errors } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -347,14 +398,14 @@ func (r *Nova) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { novalog.Info("validate update", "diff", cmp.Diff(oldNova, r)) - errors := r.Spec.ValidateUpdate(oldNova.Spec, field.NewPath("spec"), r.Namespace) + warnings, errors := r.Spec.ValidateUpdate(oldNova.Spec, field.NewPath("spec"), r.Namespace) if len(errors) != 0 { novalog.Info("validation failed", "name", r.Name) - return nil, apierrors.NewInvalid( + return warnings, apierrors.NewInvalid( schema.GroupKind{Group: "nova.openstack.org", Kind: "Nova"}, r.Name, errors) } - return nil, nil + return warnings, nil } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/api/v1beta1/novacell_types.go b/api/v1beta1/novacell_types.go index 775bd473f..7b35e9ddf 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" @@ -45,11 +46,15 @@ type NovaCellTemplate struct { CellDatabaseAccount string `json:"cellDatabaseAccount"` // +kubebuilder:validation:Optional - // +kubebuilder:default=rabbitmq // CellMessageBusInstance is the name of the RabbitMqCluster CR to select // the Message Bus Service instance used by the nova services to // communicate in this cell. For cell0 it is unused. - CellMessageBusInstance string `json:"cellMessageBusInstance"` + // Deprecated: Use MessagingBus.Cluster instead + CellMessageBusInstance string `json:"cellMessageBusInstance,omitempty"` + + // +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 diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 3869b1c84..0da528695 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" @@ -520,6 +521,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) @@ -1674,6 +1676,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)) @@ -1706,6 +1709,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 + } out.Auth = in.Auth } diff --git a/ci/nova-operator-tempest-multinode/control_plane_hook.yaml b/ci/nova-operator-tempest-multinode/control_plane_hook.yaml index f685e2899..ad83a4e0f 100644 --- a/ci/nova-operator-tempest-multinode/control_plane_hook.yaml +++ b/ci/nova-operator-tempest-multinode/control_plane_hook.yaml @@ -24,9 +24,18 @@ path: /spec/nova/template/apiServiceTemplate/replicas value: 2 - - op: replace + - op: add + path: /spec/nova/template/apiMessageBusInstance + value: "" + + - op: add + path: /spec/nova/template/notificationsBus + value: + cluster: rabbitmq + + - op: add path: /spec/nova/template/notificationsBusInstance - value: rabbitmq + value: null - op: replace path: /spec/neutron/template/replicas @@ -47,13 +56,16 @@ value: cell0: cellDatabaseAccount: nova-cell0 + cellMessageBusInstance: "" hasAPIAccess: true metadataServiceTemplate: enabled: false cell1: cellDatabaseAccount: nova-cell1 + cellMessageBusInstance: "" hasAPIAccess: true - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 cellDatabaseInstance: openstack-cell1 metadataServiceTemplate: enabled: true diff --git a/config/crd/bases/nova.openstack.org_nova.yaml b/config/crd/bases/nova.openstack.org_nova.yaml index c3f3b5f8b..322c6ee19 100644 --- a/config/crd/bases/nova.openstack.org_nova.yaml +++ b/config/crd/bases/nova.openstack.org_nova.yaml @@ -54,11 +54,11 @@ spec: Service instance used for the Nova API DB. type: string apiMessageBusInstance: - default: rabbitmq description: |- APIMessageBusInstance is the name of the RabbitMqCluster CR to select the Message Bus Service instance used by the Nova top level services to communicate. + Deprecated: Use MessagingBus.Cluster instead type: string apiServiceTemplate: default: @@ -398,11 +398,11 @@ spec: Service instance used as the DB of this cell. type: string cellMessageBusInstance: - default: rabbitmq description: |- CellMessageBusInstance is the name of the RabbitMqCluster CR to select the Message Bus Service instance used by the nova services to communicate in this cell. For cell0 it is unused. + Deprecated: Use MessagingBus.Cluster instead type: string conductorServiceTemplate: description: ConductorServiceTemplate - defines the cell conductor @@ -550,6 +550,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 @@ -1325,8 +1342,9 @@ spec: cell1: cellDatabaseAccount: nova-cell1 cellDatabaseInstance: openstack-cell1 - cellMessageBusInstance: rabbitmq-cell1 hasAPIAccess: true + messagingBus: + cluster: rabbitmq-cell1 description: |- Cells is a mapping of cell names to NovaCellTemplate objects defining the cells in the deployment. The "cell0" cell is a mandatory cell in @@ -1350,6 +1368,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 @@ -1658,6 +1692,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/config/samples/nova_v1beta1_nova-multi-cell.yaml b/config/samples/nova_v1beta1_nova-multi-cell.yaml index 37e86f633..d50179145 100644 --- a/config/samples/nova_v1beta1_nova-multi-cell.yaml +++ b/config/samples/nova_v1beta1_nova-multi-cell.yaml @@ -10,7 +10,8 @@ spec: # This is the name of the single RabbitMqCluster CR we deploy today # The Service is labelled with # app.kubernetes.io/component=rabbitmq, app.kubernetes.io/name= - apiMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq # This is the name of the KeystoneAPI CR we deploy today # The Service is labelled with service=keystone,internal=true keystoneInstance: keystone @@ -49,7 +50,8 @@ spec: cell0: cellDatabaseInstance: openstack cellDatabaseAccount: nova-cell0 - cellMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq # cell0 always needs access to the API DB and MQ as it hosts the super # conductor. It will inherit the API DB and MQ access from the Nova CR # that creates it. @@ -63,7 +65,8 @@ spec: cell1: cellDatabaseInstance: mariadb-cell1 cellDatabaseAccount: nova-cell1 - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 # cell1 will have upcalls support. It will inherit the API DB and MQ # access from the Nova CR that creates it. hasAPIAccess: true @@ -82,7 +85,8 @@ spec: cell2: cellDatabaseInstance: mariadb-cell2 cellDatabaseAccount: nova-cell2 - cellMessageBusInstance: rabbitmq-cell2 + messagingBus: + cluster: rabbitmq-cell2 # cell2 will not get the API DB and MQ connection info from the Nova CR hasAPIAccess: false conductorServiceTemplate: diff --git a/go.mod b/go.mod index 845b1753a..49d1cc228 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 github.com/onsi/ginkgo/v2 v2.27.5 github.com/onsi/gomega v1.39.0 - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260123105816-865d02e287a9 + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126175636-114b4c65a959 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260128142552-e2c25eccae5a - github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35 + github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260128142552-e2c25eccae5a github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260120155328-e04f52e73f01 github.com/openstack-k8s-operators/nova-operator/api v0.0.0-20221209164002-f9e6b9363961 go.uber.org/zap v1.27.1 diff --git a/go.sum b/go.sum index a2475798b..e3460bf43 100644 --- a/go.sum +++ b/go.sum @@ -118,18 +118,18 @@ github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/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.20260123105816-865d02e287a9 h1:tD6nnTRcyUCXdVMWPHLApk12tzQlQni5eoxvQ8XdbP8= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260123105816-865d02e287a9/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 h1:vhAGLKZitJIffj7ONiPpKmOX7Tmt/LGJpaY0Z2LeyfQ= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126175636-114b4c65a959 h1:8FSpTYAoLq27ElDGe3igPl2QUq9IYD6RJGu2Xu+Ymus= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126175636-114b4c65a959/go.mod h1:pN/s+czXvApiE9nxeTtDeRTXWcaaCLZSrtoyOSUb37k= -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/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a h1:97OfmmJgoIKTfbED2SfyjoPkivoiMHg4jfbrTuwSGQw= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260128142552-e2c25eccae5a/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260128142552-e2c25eccae5a h1:rMrtMsHAfkEqodLUD4Yu5NYtjGW8U3f7zxJTJpwjvPs= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260128142552-e2c25eccae5a/go.mod h1:zOX7Y05keiSppIvLabuyh42QHBMhCcoskAtxFRbwXKo= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35 h1:8WZYfCt1VJHa5sJRX0UhpmoXud/fn8LHQhXsakdYXuQ= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:H0aQANk8iJPRhS2Bg9n6cYb/IHF0Cks9g7+uZG04Rhk= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35 h1:8rQc4Fsfe6yqRU5Xjt9lWXqUqfBjRubr0utnUpUBKTE= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:QWzyC+tTBB2OGuYyIiLLo1oA0+I/0NUMXD+dj4Quv4M= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260128142552-e2c25eccae5a h1:EIjfIa5m79FIVfEU9zgLJmkMqNN3PhXGKaS6CpaXyfw= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260128142552-e2c25eccae5a/go.mod h1:sqKTKvYhSzu4Opnjx/J+zzetXKRqYrhxsfvrST/NjoU= github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260120155328-e04f52e73f01 h1:93NxJ/fFx41HcFXk4nJk4PPz4lrqzNMviTmKyWwa+vg= github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260120155328-e04f52e73f01/go.mod h1:X6W8pIULiWUc6smaTqiNocjxoXaRLgXediwpI/dxD9s= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= diff --git a/internal/controller/common.go b/internal/controller/common.go index b5da03e03..dfe5bfa12 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -111,6 +111,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" authAppCredSecretField = ".spec.auth.applicationCredentialSecret" // #nosec G101 diff --git a/internal/controller/nova_controller.go b/internal/controller/nova_controller.go index b9b96f69a..b6c751792 100644 --- a/internal/controller/nova_controller.go +++ b/internal/controller/nova_controller.go @@ -410,8 +410,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( @@ -435,20 +435,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: @@ -486,7 +485,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 @@ -496,10 +495,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] @@ -508,12 +509,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: @@ -525,6 +527,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( @@ -587,7 +590,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, region, ospSecret, acData, ) cells[cellName] = cell @@ -654,8 +658,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, ospSecret, acData) if err != nil { return ctrl.Result{}, err @@ -1243,8 +1247,10 @@ func (r *NovaReconciler) ensureCell( cellDB *mariadbv1.Database, apiDB *mariadbv1.Database, cellTransportURL string, + cellRabbitmqUserName string, cellQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, keystoneAuthURL string, region string, secret corev1.Secret, @@ -1254,7 +1260,8 @@ func (r *NovaReconciler) ensureCell( cellSecretName, err := r.ensureCellSecret( ctx, h, instance, cellName, cellTemplate, - cellTransportURL, cellQuorumQueues, notificationTransportURL, + cellTransportURL, cellRabbitmqUserName, cellQuorumQueues, + notificationTransportURL, notificationRabbitmqUserName, secret, acData) if err != nil { return nil, nova.CellDeploying, err @@ -1771,8 +1778,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{ @@ -1782,14 +1789,20 @@ func (r *NovaReconciler) ensureMQ( } op, err := controllerutil.CreateOrPatch(ctx, r.Client, transportURL, func() error { - transportURL.Spec.RabbitmqClusterName = messageBusInstanceName + transportURL.Spec.RabbitmqClusterName = rabbitMqConfig.Cluster + // Always set Username and Vhost to allow clearing/resetting them + // The infra-operator TransportURL controller handles empty values: + // - Empty Username: uses default cluster admin credentials + // - Empty Vhost: defaults to "/" vhost + transportURL.Spec.Username = rabbitMqConfig.User + 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, @@ -1798,12 +1811,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, @@ -1811,7 +1824,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} @@ -1820,14 +1833,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) } @@ -1837,7 +1850,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( @@ -2066,8 +2085,10 @@ func (r *NovaReconciler) ensureCellSecret( cellName string, cellTemplate novav1.NovaCellTemplate, cellTransportURL string, + cellRabbitmqUserName string, cellQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, externalSecret corev1.Secret, acData *keystonev1.ApplicationCredentialData, ) (string, error) { @@ -2080,10 +2101,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, } // Add Application Credential data @@ -2133,8 +2156,10 @@ func (r *NovaReconciler) ensureTopLevelSecret( h *helper.Helper, instance *novav1.Nova, apiTransportURL string, + apiRabbitmqUserName string, apiQuorumQueues bool, notificationTransportURL string, + notificationRabbitmqUserName string, externalSecret corev1.Secret, acData *keystonev1.ApplicationCredentialData, ) (string, error) { @@ -2146,11 +2171,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, } // Add Application Credential data if provided diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 9cfdd0bc8..889d65a78 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -112,10 +112,12 @@ func NovaSchedulerConditionGetter(name types.NamespacedName) condition.Condition func GetDefaultNovaSpec() map[string]any { return map[string]any{ - "secret": SecretName, - "cellTemplates": map[string]any{}, - "apiMessageBusInstance": cell0.TransportURLName.Name, - "apiDatabaseAccount": novaNames.APIMariaDBDatabaseAccount.Name, + "secret": SecretName, + "cellTemplates": map[string]any{}, + "apiDatabaseAccount": novaNames.APIMariaDBDatabaseAccount.Name, + "messagingBus": map[string]any{ + "cluster": cell0.TransportURLName.Name, + }, } } @@ -161,7 +163,9 @@ func CreateNovaWithCell0(name types.NamespacedName) client.Object { }, }, }, - "apiMessageBusInstance": cell0.TransportURLName.Name, + "messagingBus": map[string]any{ + "cluster": cell0.TransportURLName.Name, + }, }, } @@ -1025,7 +1029,9 @@ func CreateNovaWithNCellsAndEnsureReady(cellNumber int, novaNames *NovaNames) { template["cellDatabaseAccount"] = account.Name if i != 0 { // cell0 - template["cellMessageBusInstance"] = cell.TransportURLName.Name + template["messagingBus"] = map[string]any{ + "cluster": cell.TransportURLName.Name, + } } if i == 1 { @@ -1046,7 +1052,9 @@ func CreateNovaWithNCellsAndEnsureReady(cellNumber int, novaNames *NovaNames) { spec := GetDefaultNovaSpec() spec["cellTemplates"] = cellTemplates spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = novaNames.Cells["cell0"].TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": novaNames.Cells["cell0"].TransportURLName.Name, + } // Deploy Nova and simulate its dependencies DeferCleanup(th.DeleteInstance, CreateNova(novaNames.NovaName, spec)) diff --git a/test/functional/nova_controller_test.go b/test/functional/nova_controller_test.go index f89118821..44b9c7153 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" @@ -81,13 +82,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()) @@ -127,10 +130,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()) @@ -538,13 +541,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)) @@ -654,7 +661,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( @@ -663,6 +670,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() { @@ -1177,9 +1188,11 @@ var _ = Describe("Nova controller", func() { }, ) rawSpec := map[string]any{ - "secret": SecretName, - "apiDatabaseAccount": novaNames.APIMariaDBDatabaseAccount.Name, - "apiMessageBusInstance": cell0.TransportURLName.Name, + "secret": SecretName, + "apiDatabaseAccount": novaNames.APIMariaDBDatabaseAccount.Name, + "messagingBus": map[string]any{ + "cluster": cell0.TransportURLName.Name, + }, "cellTemplates": map[string]any{ "cell0": map[string]any{ "apiDatabaseAccount": novaNames.APIMariaDBDatabaseAccount.Name, @@ -1473,7 +1486,9 @@ var _ = Describe("Nova controller", func() { cell1Template := GetDefaultNovaCellTemplate() cell1Template["cellDatabaseInstance"] = cell1.MariaDBDatabaseName.Name cell1Template["cellDatabaseAccount"] = cell1.MariaDBAccountName.Name - cell1Template["cellMessageBusInstance"] = cell1.TransportURLName.Name + cell1Template["messagingBus"] = map[string]any{ + "cluster": cell1.TransportURLName.Name, + } // We reference the cell1 topology that is inherited by the cell1 conductor, // metadata, and novncproxy cell1Template["topologyRef"] = map[string]any{"name": topologyRefCell.Name} @@ -1489,7 +1504,9 @@ var _ = Describe("Nova controller", func() { "enabled": false, } spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = cell0.TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } // We reference the global topology and is inherited by the sub components // except cell1 that has an override spec["topologyRef"] = map[string]any{"name": topologyRefTopLevel.Name} diff --git a/test/functional/nova_multicell_test.go b/test/functional/nova_multicell_test.go index 7b46d59ea..7596204d0 100644 --- a/test/functional/nova_multicell_test.go +++ b/test/functional/nova_multicell_test.go @@ -79,7 +79,9 @@ var _ = Describe("Nova multi cell", func() { cell1Template := GetDefaultNovaCellTemplate() cell1Template["cellDatabaseInstance"] = cell1.MariaDBDatabaseName.Name cell1Template["cellDatabaseAccount"] = cell1.MariaDBAccountName.Name - cell1Template["cellMessageBusInstance"] = cell1.TransportURLName.Name + cell1Template["messagingBus"] = map[string]any{ + "cluster": cell1.TransportURLName.Name, + } cell1Template["passwordSelectors"] = map[string]any{ "database": "NovaCell1DatabasePassword", } @@ -92,7 +94,9 @@ var _ = Describe("Nova multi cell", func() { cell2Template := GetDefaultNovaCellTemplate() cell2Template["cellDatabaseInstance"] = cell2.MariaDBDatabaseName.Name cell2Template["cellDatabaseAccount"] = cell2.MariaDBAccountName.Name - cell2Template["cellMessageBusInstance"] = cell2.TransportURLName.Name + cell2Template["messagingBus"] = map[string]any{ + "cluster": cell2.TransportURLName.Name, + } cell2Template["hasAPIAccess"] = false cell2Template["passwordSelectors"] = map[string]any{ "database": "NovaCell2DatabasePassword", @@ -104,7 +108,9 @@ var _ = Describe("Nova multi cell", func() { "cell2": cell2Template, } spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = cell0.TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } DeferCleanup(th.DeleteInstance, CreateNova(novaNames.NovaName, spec)) DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(novaNames.NovaName.Namespace)) @@ -675,7 +681,9 @@ var _ = Describe("Nova multi cell", func() { // will act both as a super conductor and as cell1 conductor cell1Template["cellDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name cell1Template["cellDatabaseAccount"] = cell1.MariaDBAccountName.Name - cell1Template["cellMessageBusInstance"] = cell0.TransportURLName.Name + cell1Template["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } cell1Template["hasAPIAccess"] = true spec["cellTemplates"] = map[string]any{ @@ -683,7 +691,9 @@ var _ = Describe("Nova multi cell", func() { "cell1": cell1Template, } spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = cell0.TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } DeferCleanup(th.DeleteInstance, CreateNova(novaNames.NovaName, spec)) memcachedSpec := infra.GetDefaultMemcachedSpec() @@ -790,14 +800,18 @@ var _ = Describe("Nova multi cell", func() { cell1Template := GetDefaultNovaCellTemplate() cell1Template["cellDatabaseInstance"] = cell1.MariaDBDatabaseName.Name cell1Template["cellDatabaseAccount"] = cell1.MariaDBAccountName.Name - cell1Template["cellMessageBusInstance"] = cell1.TransportURLName.Name + cell1Template["messagingBus"] = map[string]any{ + "cluster": cell1.TransportURLName.Name, + } spec["cellTemplates"] = map[string]any{ "cell0": cell0Template, "cell1": cell1Template, } spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = cell0.TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } DeferCleanup(th.DeleteInstance, CreateNova(novaNames.NovaName, spec)) memcachedSpec := infra.GetDefaultMemcachedSpec() @@ -849,7 +863,9 @@ var _ = Describe("Nova multi cell", func() { cell1Template := GetDefaultNovaCellTemplate() cell1Template["cellDatabaseInstance"] = cell1.MariaDBDatabaseName.Name cell1Template["cellDatabaseAccount"] = cell1.MariaDBAccountName.Name - cell1Template["cellMessageBusInstance"] = cell1.TransportURLName.Name + cell1Template["messagingBus"] = map[string]any{ + "cluster": cell1.TransportURLName.Name, + } cell1Template["metadataServiceTemplate"] = map[string]any{ "enabled": true, } @@ -862,7 +878,9 @@ var _ = Describe("Nova multi cell", func() { "enabled": false, } spec["apiDatabaseInstance"] = novaNames.APIMariaDBDatabaseName.Name - spec["apiMessageBusInstance"] = cell0.TransportURLName.Name + spec["messagingBus"] = map[string]any{ + "cluster": cell0.TransportURLName.Name, + } DeferCleanup(th.DeleteInstance, CreateNova(novaNames.NovaName, spec)) memcachedSpec := infra.GetDefaultMemcachedSpec() diff --git a/test/functional/nova_reconfiguration_test.go b/test/functional/nova_reconfiguration_test.go index 29be006a1..664312cc9 100644 --- a/test/functional/nova_reconfiguration_test.go +++ b/test/functional/nova_reconfiguration_test.go @@ -689,7 +689,10 @@ var _ = Describe("Nova reconfiguration", func() { nova := GetNova(novaNames.NovaName) cell1 := nova.Spec.CellTemplates["cell1"] - cell1.CellMessageBusInstance = "alternate-mq-for-cell1" + // Migrate from deprecated cellMessageBusInstance to new messagingBus.cluster field + // Must null out the old field when setting the new one to avoid validation error + cell1.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/functional/validation_webhook_test.go b/test/functional/validation_webhook_test.go index 453b19f6d..ff6ee4ea2 100644 --- a/test/functional/validation_webhook_test.go +++ b/test/functional/validation_webhook_test.go @@ -662,13 +662,17 @@ var _ = Describe("Nova validation", func() { ), ) }) - It("check Cell validation with duplicate cellMessageBusInstance", func() { + It("check Cell validation with duplicate messagingBus.cluster", func() { spec := GetDefaultNovaSpec() cell0 := GetDefaultNovaCellTemplate() cell1 := GetDefaultNovaCellTemplate() cell2 := GetDefaultNovaCellTemplate() - cell1["cellMessageBusInstance"] = "rabbitmq-of-caerbannog" - cell2["cellMessageBusInstance"] = "rabbitmq-of-caerbannog" + cell1["messagingBus"] = map[string]any{ + "cluster": "rabbitmq-of-caerbannog", + } + cell2["messagingBus"] = map[string]any{ + "cluster": "rabbitmq-of-caerbannog", + } spec["cellTemplates"] = map[string]any{"cell0": cell0, "cell1": cell1, "cell2": cell2} raw := map[string]any{ "apiVersion": "nova.openstack.org/v1beta1", diff --git a/test/kuttl/test-suites/default/cell-tests/01-assert.yaml b/test/kuttl/test-suites/default/cell-tests/01-assert.yaml index 63dab5d87..2f4e88d6b 100644 --- a/test/kuttl/test-suites/default/cell-tests/01-assert.yaml +++ b/test/kuttl/test-suites/default/cell-tests/01-assert.yaml @@ -8,7 +8,8 @@ metadata: spec: apiDatabaseInstance: openstack apiDatabaseAccount: nova-api - apiMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq apiServiceTemplate: customServiceConfig: "" replicas: 1 @@ -17,7 +18,8 @@ spec: cell0: cellDatabaseInstance: openstack cellDatabaseAccount: nova-cell0 - cellMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq conductorServiceTemplate: customServiceConfig: "" replicas: 1 @@ -30,7 +32,8 @@ spec: cell1: cellDatabaseInstance: openstack-cell1 cellDatabaseAccount: nova-cell1 - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 conductorServiceTemplate: customServiceConfig: "" replicas: 1 diff --git a/test/kuttl/test-suites/default/config-tests/01-deploy-with-default-config-overwrite.yaml b/test/kuttl/test-suites/default/config-tests/01-deploy-with-default-config-overwrite.yaml index 563fd7daf..31545f0bf 100644 --- a/test/kuttl/test-suites/default/config-tests/01-deploy-with-default-config-overwrite.yaml +++ b/test/kuttl/test-suites/default/config-tests/01-deploy-with-default-config-overwrite.yaml @@ -12,13 +12,13 @@ spec: cell0: cellDatabaseInstance: openstack cellDatabaseAccount: nova-cell0 - cellMessageBusInstance: rabbitmq hasAPIAccess: true memcachedInstance: memcached cell1: cellDatabaseInstance: openstack-cell1 cellDatabaseAccount: nova-cell1 - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 memcachedInstance: memcached novaComputeTemplates: compute-fake1: diff --git a/test/kuttl/test-suites/default/config-tests/02-enable-notifications.yaml b/test/kuttl/test-suites/default/config-tests/02-enable-notifications.yaml index 15ecdd8f4..25bdbe24d 100644 --- a/test/kuttl/test-suites/default/config-tests/02-enable-notifications.yaml +++ b/test/kuttl/test-suites/default/config-tests/02-enable-notifications.yaml @@ -4,4 +4,5 @@ metadata: name: nova-kuttl spec: secret: osp-secret - notificationsBusInstance: rabbitmq-broadcaster + notificationsBus: + cluster: rabbitmq-broadcaster diff --git a/test/kuttl/test-suites/default/deps/infra.yaml b/test/kuttl/test-suites/default/deps/infra.yaml index 428fb21d2..7b87da888 100644 --- a/test/kuttl/test-suites/default/deps/infra.yaml +++ b/test/kuttl/test-suites/default/deps/infra.yaml @@ -20,6 +20,8 @@ spec: replicas: 1 rabbitmq-broadcaster: replicas: 1 + rabbitmq-notifications: + replicas: 1 memcached: templates: memcached: diff --git a/test/kuttl/test-suites/default/rmquser-vhost/00-cleanup-nova.yaml b/test/kuttl/test-suites/default/rmquser-vhost/00-cleanup-nova.yaml new file mode 100644 index 000000000..b8d66a4b0 --- /dev/null +++ b/test/kuttl/test-suites/default/rmquser-vhost/00-cleanup-nova.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: nova.openstack.org/v1beta1 + kind: Nova + name: nova-kuttl + namespace: nova-kuttl-default diff --git a/test/kuttl/test-suites/default/rmquser-vhost/01-assert.yaml b/test/kuttl/test-suites/default/rmquser-vhost/01-assert.yaml new file mode 100644 index 000000000..72c8fa64a --- /dev/null +++ b/test/kuttl/test-suites/default/rmquser-vhost/01-assert.yaml @@ -0,0 +1,126 @@ +# Verify the TransportURLs have correct cluster, user, and vhost configured +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: nova-kuttl-api-transport +spec: + rabbitmqClusterName: rabbitmq + username: nova-rpc + vhost: nova-rpc +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: nova-kuttl-notification-transport +spec: + rabbitmqClusterName: rabbitmq-notifications + username: nova-notifications + vhost: nova-notifications +--- +# Verify that 2 TransportURL CRs were created (separate clusters) +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + + # Wait for Nova to be Ready + kubectl wait --for=condition=Ready nova/nova-kuttl -n $NAMESPACE --timeout=300s + + # Verify NovaNotificationMQReady condition exists and is True + kubectl get nova nova-kuttl -n $NAMESPACE -o jsonpath='{.status.conditions[?(@.type=="NovaNotificationMQReady")].status}' | grep -q "True" + echo "NovaNotificationMQReady condition is True" + + # Count TransportURL CRs - should be exactly 2 (one for messaging, one for notifications) + api_transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "nova-kuttl-api-transport" | wc -l) + notification_transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "nova-kuttl-notification-transport" | wc -l) + + if [ "$api_transport_count" -ne "1" ]; then + echo "Expected 1 api-transport TransportURL, found $api_transport_count" + exit 1 + fi + + if [ "$notification_transport_count" -ne "1" ]; then + echo "Expected 1 notification-transport TransportURL, found $notification_transport_count" + exit 1 + fi + + echo "Correctly found 2 TransportURLs (separate clusters: api and notification)" + + # Verify api-transport has correct user and vhost + api_user=$(kubectl get transporturl nova-kuttl-api-transport -n $NAMESPACE -o jsonpath='{.spec.username}') + api_vhost=$(kubectl get transporturl nova-kuttl-api-transport -n $NAMESPACE -o jsonpath='{.spec.vhost}') + if [ "$api_user" != "nova-rpc" ]; then + echo "Expected api-transport username 'nova-rpc', found '$api_user'" + exit 1 + fi + if [ "$api_vhost" != "nova-rpc" ]; then + echo "Expected api-transport vhost 'nova-rpc', found '$api_vhost'" + exit 1 + fi + echo "API transport has correct user (nova-rpc) and vhost (nova-rpc)" + + # Verify notification-transport has correct user and vhost + notif_user=$(kubectl get transporturl nova-kuttl-notification-transport -n $NAMESPACE -o jsonpath='{.spec.username}') + notif_vhost=$(kubectl get transporturl nova-kuttl-notification-transport -n $NAMESPACE -o jsonpath='{.spec.vhost}') + if [ "$notif_user" != "nova-notifications" ]; then + echo "Expected notification-transport username 'nova-notifications', found '$notif_user'" + exit 1 + fi + if [ "$notif_vhost" != "nova-notifications" ]; then + echo "Expected notification-transport vhost 'nova-notifications', found '$notif_vhost'" + exit 1 + fi + echo "Notification transport has correct user (nova-notifications) and vhost (nova-notifications)" + + # 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 RPC transport_url in DEFAULT section + rpc_transport_url=$(kubectl exec -n $NAMESPACE ${NOVA_API_POD} -c nova-kuttl-api-api -- cat /etc/nova/nova.conf.d/01-nova.conf | grep -E '^\[DEFAULT\]' -A 50 | grep 'transport_url' | head -1 || true) + if [ -z "$rpc_transport_url" ]; then + echo "transport_url not found in DEFAULT section" + exit 1 + fi + echo "Found RPC transport_url: $rpc_transport_url" + + # Verify the RPC transport_url contains the correct vhost (nova-rpc) + if ! echo "$rpc_transport_url" | grep -q '/nova-rpc'; then + echo "RPC transport_url does not contain expected vhost '/nova-rpc'" + exit 1 + fi + echo "Successfully verified vhost 'nova-rpc' in RPC transport_url" + + # Verify the RPC transport_url contains the correct username (nova-rpc) + if ! echo "$rpc_transport_url" | grep -q 'nova-rpc:'; then + echo "RPC transport_url does not contain expected username 'nova-rpc:'" + exit 1 + fi + echo "Successfully verified username 'nova-rpc' in RPC transport_url" + + # Verify oslo_messaging_notifications section has transport_url configured + notif_transport_url=$(kubectl exec -n $NAMESPACE ${NOVA_API_POD} -c nova-kuttl-api-api -- cat /etc/nova/nova.conf.d/01-nova.conf | grep -A 2 '\[oslo_messaging_notifications\]' | grep 'transport_url' || true) + if [ -z "$notif_transport_url" ]; then + echo "transport_url not found in oslo_messaging_notifications section" + exit 1 + fi + echo "Found notifications transport_url: $notif_transport_url" + + # Verify the notifications transport_url contains the correct vhost (nova-notifications) + if ! echo "$notif_transport_url" | grep -q '/nova-notifications'; then + echo "Notifications transport_url does not contain expected vhost '/nova-notifications'" + exit 1 + fi + echo "Successfully verified vhost 'nova-notifications' in notifications transport_url" + + # Verify the notifications transport_url contains the correct username (nova-notifications) + if ! echo "$notif_transport_url" | grep -q 'nova-notifications:'; then + echo "Notifications transport_url does not contain expected username 'nova-notifications:'" + exit 1 + fi + echo "Successfully verified username 'nova-notifications' in notifications transport_url" + + exit 0 diff --git a/test/kuttl/test-suites/default/rmquser-vhost/01-deploy.yaml b/test/kuttl/test-suites/default/rmquser-vhost/01-deploy.yaml new file mode 100644 index 000000000..d31053621 --- /dev/null +++ b/test/kuttl/test-suites/default/rmquser-vhost/01-deploy.yaml @@ -0,0 +1,14 @@ +apiVersion: nova.openstack.org/v1beta1 +kind: Nova +metadata: + name: nova-kuttl +spec: + secret: osp-secret + messagingBus: + cluster: rabbitmq + user: nova-rpc + vhost: nova-rpc + notificationsBus: + cluster: rabbitmq-notifications + user: nova-notifications + vhost: nova-notifications diff --git a/test/kuttl/test-suites/default/scale-tests/01-assert.yaml b/test/kuttl/test-suites/default/scale-tests/01-assert.yaml index 63dab5d87..2f4e88d6b 100644 --- a/test/kuttl/test-suites/default/scale-tests/01-assert.yaml +++ b/test/kuttl/test-suites/default/scale-tests/01-assert.yaml @@ -8,7 +8,8 @@ metadata: spec: apiDatabaseInstance: openstack apiDatabaseAccount: nova-api - apiMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq apiServiceTemplate: customServiceConfig: "" replicas: 1 @@ -17,7 +18,8 @@ spec: cell0: cellDatabaseInstance: openstack cellDatabaseAccount: nova-cell0 - cellMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq conductorServiceTemplate: customServiceConfig: "" replicas: 1 @@ -30,7 +32,8 @@ spec: cell1: cellDatabaseInstance: openstack-cell1 cellDatabaseAccount: nova-cell1 - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 conductorServiceTemplate: customServiceConfig: "" replicas: 1 diff --git a/test/kuttl/test-suites/default/scale-tests/04-assert.yaml b/test/kuttl/test-suites/default/scale-tests/04-assert.yaml index 9df6c6196..d95994f75 100644 --- a/test/kuttl/test-suites/default/scale-tests/04-assert.yaml +++ b/test/kuttl/test-suites/default/scale-tests/04-assert.yaml @@ -15,7 +15,8 @@ metadata: spec: apiDatabaseInstance: openstack apiDatabaseAccount: nova-api - apiMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq apiServiceTemplate: replicas: 0 metadataServiceTemplate: @@ -25,7 +26,8 @@ spec: cell0: cellDatabaseInstance: openstack cellDatabaseAccount: nova-cell0 - cellMessageBusInstance: rabbitmq + messagingBus: + cluster: rabbitmq conductorServiceTemplate: customServiceConfig: "" replicas: 0 @@ -37,7 +39,8 @@ spec: cell1: cellDatabaseInstance: openstack-cell1 cellDatabaseAccount: nova-cell1 - cellMessageBusInstance: rabbitmq-cell1 + messagingBus: + cluster: rabbitmq-cell1 conductorServiceTemplate: customServiceConfig: "" replicas: 0