Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/bases/core.openstack.org_openstackcontrolplanes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down Expand Up @@ -3649,6 +3651,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down
42 changes: 41 additions & 1 deletion api/core/v1beta1/openstackcontrolplane_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ const (
DeploymentStageAnnotation = "core.openstack.org/deployment-stage"
// DeploymentStageInfrastructureOnly - Annotation value to pause after infrastructure deployment
DeploymentStageInfrastructureOnly = "infrastructure-only"

// ReconcileTriggerAnnotation - Generic annotation to trigger reconciliation and webhook
// Value is typically a timestamp to ensure annotation changes trigger updates
// Used by controller to trigger UPDATE webhook when needed (e.g., for service name caching)
ReconcileTriggerAnnotation = "openstack.org/reconcile-trigger"
)

// OpenStackControlPlaneSpec defines the desired state of OpenStackControlPlane
Expand Down Expand Up @@ -450,6 +455,12 @@ type GlanceSection struct {
// Convenient to avoid podname (and thus hostname) collision between different deployments.
// Useful for CI jobs as well as preproduction and production environments that use the same storage backend, etc.
UniquePodNames bool `json:"uniquePodNames"`

// +kubebuilder:validation:Optional
// ServiceName - Cached service name for Glance CR. Set automatically when UniquePodNames is enabled.
// This field preserves the service name (with UID suffix) across reconciliations and restores,
// ensuring consistent resource naming even when the CR is recreated. Should not be manually set.
ServiceName string `json:"serviceName,omitempty"`
}

// CinderSection defines the desired state of Cinder service
Expand All @@ -476,6 +487,12 @@ type CinderSection struct {
// Convenient to avoid podname (and thus hostname) collision between different deployments.
// Useful for CI jobs as well as preproduction and production environments that use the same storage backend, etc.
UniquePodNames bool `json:"uniquePodNames"`

// +kubebuilder:validation:Optional
// ServiceName - Cached service name for Cinder CR. Set automatically when UniquePodNames is enabled.
// This field preserves the service name (with UID suffix) across reconciliations and restores,
// ensuring consistent resource naming even when the CR is recreated. Should not be manually set.
ServiceName string `json:"serviceName,omitempty"`
}

// GaleraSection defines the desired state of Galera services
Expand Down Expand Up @@ -1036,10 +1053,33 @@ func (c CertConfig) GetRenewBeforeHours() string {
// GetServiceName - returns the name and altName depending if
// UniquePodNames is configured
func (instance OpenStackControlPlane) GetServiceName(name string, uniquePodNames bool) (string, string) {
altName := fmt.Sprintf("%s-%s", name, instance.UID[:5])
// Generate UID suffix only if UID is available and has sufficient length
var uidSuffix string
if len(instance.UID) >= 5 {
uidSuffix = string(instance.UID[:5])
}

altName := name
if uidSuffix != "" {
altName = fmt.Sprintf("%s-%s", name, uidSuffix)
}

if uniquePodNames {
name, altName = altName, name
}

return name, altName
}

// GetServiceNameCached - returns the name and altName depending if UniquePodNames is configured.
// If cachedName is provided (non-empty), it will be used instead of generating a new name with UID.
// This ensures consistent naming across reconciliations and restores.
func (instance OpenStackControlPlane) GetServiceNameCached(name string, uniquePodNames bool, cachedName string) (string, string) {
// If we have a cached name and uniquePodNames is enabled, use it
if uniquePodNames && cachedName != "" {
return cachedName, name
}

// Otherwise, fall back to the original logic
return instance.GetServiceName(name, uniquePodNames)
}
101 changes: 97 additions & 4 deletions api/core/v1beta1/openstackcontrolplane_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ package v1beta1

import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"slices"
"strings"

keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
"github.com/openstack-k8s-operators/lib-common/modules/common/object"
"github.com/openstack-k8s-operators/lib-common/modules/common/route"
common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1"
Expand All @@ -32,6 +35,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -61,6 +65,95 @@ import (
// log is for logging in this package.
var openstackcontrolplanelog = logf.Log.WithName("openstackcontrolplane-resource")

// generateRandomID generates a random 5-character hexadecimal ID
// Used for service naming when UniquePodNames is enabled and UID is not yet available
func generateRandomID() (string, error) {
bytes := make([]byte, 3) // 3 bytes = 6 hex chars, we'll take first 5
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes)[:5], nil
}

// lookupServiceCR attempts to find an existing service CR in the cluster owned by this OpenStackControlPlane
// Returns the CR name if found, empty string if not found or not owned by the given owner UID
// serviceName should be the base service name (e.g., CinderName, GlanceName)
// ownerUID is the UID of the OpenStackControlPlane that should own the CR
// This function lists CRs and finds ones that start with the service name prefix and are owned by ownerUID
func lookupServiceCR(ctx context.Context, c client.Client, namespace, serviceName string, ownerUID types.UID) (string, error) {
switch serviceName {
case CinderName:
cinderList := &cinderv1.CinderList{}
if err := c.List(ctx, cinderList, client.InNamespace(namespace)); err != nil {
return "", fmt.Errorf("failed to list Cinder CRs: %w", err)
}
// Find any Cinder CR that starts with "cinder" and is owned by this OpenStackControlPlane
for _, cinder := range cinderList.Items {
if strings.HasPrefix(cinder.Name, CinderName) && object.CheckOwnerRefExist(ownerUID, cinder.GetOwnerReferences()) {
return cinder.Name, nil
}
}

case GlanceName:
glanceList := &glancev1.GlanceList{}
if err := c.List(ctx, glanceList, client.InNamespace(namespace)); err != nil {
return "", fmt.Errorf("failed to list Glance CRs: %w", err)
}
// Find any Glance CR that starts with "glance" and is owned by this OpenStackControlPlane
for _, glance := range glanceList.Items {
if strings.HasPrefix(glance.Name, GlanceName) && object.CheckOwnerRefExist(ownerUID, glance.GetOwnerReferences()) {
return glance.Name, nil
}
}

default:
return "", fmt.Errorf("unsupported service name: %s", serviceName)
}

return "", nil // Not found or not owned
}

// CacheServiceNameForCreate handles service name caching during CREATE operations
// Generates a random ID since UID is not yet available
func (r *OpenStackControlPlane) CacheServiceNameForCreate(serviceName string) (string, error) {
randomID, err := generateRandomID()
if err != nil {
return "", fmt.Errorf("failed to generate random ID: %w", err)
}
return fmt.Sprintf("%s-%s", serviceName, randomID), nil
}

// CacheServiceNameForUpdate handles service name caching during UPDATE operations
// Uses existing CR name if it's owned by this OpenStackControlPlane, otherwise generates based on current settings
// This provides robust flip detection: if we created a CR previously, we preserve its name to avoid creating duplicates
func (r *OpenStackControlPlane) CacheServiceNameForUpdate(ctx context.Context, c client.Client, serviceName string) (string, error) {
// Lookup existing CR owned by this OpenStackControlPlane
existingName, err := lookupServiceCR(ctx, c, r.Namespace, serviceName, r.UID)
if err != nil {
return "", fmt.Errorf("failed to lookup existing CR: %w", err)
}

// If we find a CR owned by us, preserve its name regardless of format
// This handles both flip scenarios and prevents creating duplicate CRs:
// - If UniquePodNames changed from false→true, we keep the old "cinder" name
// - If UniquePodNames changed from true→false, we keep the old "cinder-abc" name
// - If UniquePodNames didn't change, we keep the existing name
if existingName != "" {
return existingName, nil
}

// No existing CR found owned by us - generate name based on current UniquePodNames setting
// This handles:
// - First time deployment
// - Operator upgrade scenarios where ServiceName wasn't cached yet
name, _ := r.GetServiceName(serviceName, true)
if name == serviceName {
// GetServiceName returned base name, meaning UID is not available
return "", fmt.Errorf("unable to generate service name: no existing CR and UID not available")
}
return name, nil
}

// ValidateCreate validates the OpenStackControlPlane on creation
func (r *OpenStackControlPlane) ValidateCreate(ctx context.Context, c client.Client) (admission.Warnings, error) {
openstackcontrolplanelog.Info("validate create", "name", r.Name)
Expand Down Expand Up @@ -293,7 +386,7 @@ func (r *OpenStackControlPlane) ValidateCreateServices(basePath *field.Path) (ad
}

if r.Spec.Glance.Enabled {
glanceName, _ := r.GetServiceName(GlanceName, r.Spec.Glance.UniquePodNames)
glanceName, _ := r.GetServiceNameCached(GlanceName, r.Spec.Glance.UniquePodNames, r.Spec.Glance.ServiceName)
for key, glanceAPI := range r.Spec.Glance.Template.GlanceAPIs {
err := common_webhook.ValidateDNS1123Label(
basePath.Child("glance").Child("template").Child("glanceAPIs"),
Expand All @@ -310,7 +403,7 @@ func (r *OpenStackControlPlane) ValidateCreateServices(basePath *field.Path) (ad
}

if r.Spec.Cinder.Enabled {
cinderName, _ := r.GetServiceName(CinderName, r.Spec.Cinder.UniquePodNames)
cinderName, _ := r.GetServiceNameCached(CinderName, r.Spec.Cinder.UniquePodNames, r.Spec.Cinder.ServiceName)
errs := common_webhook.ValidateDNS1123Label(
basePath.Child("cinder").Child("template").Child("cinderVolumes"),
maps.Keys(r.Spec.Cinder.Template.CinderVolumes),
Expand Down Expand Up @@ -477,7 +570,7 @@ func (r *OpenStackControlPlane) ValidateUpdateServices(old OpenStackControlPlane
if old.Glance.Template == nil {
old.Glance.Template = &glancev1.GlanceSpecCore{}
}
glanceName, _ := r.GetServiceName(GlanceName, r.Spec.Glance.UniquePodNames)
glanceName, _ := r.GetServiceNameCached(GlanceName, r.Spec.Glance.UniquePodNames, r.Spec.Glance.ServiceName)
for key, glanceAPI := range r.Spec.Glance.Template.GlanceAPIs {
err := common_webhook.ValidateDNS1123Label(
basePath.Child("glance").Child("template").Child("glanceAPIs"),
Expand All @@ -497,7 +590,7 @@ func (r *OpenStackControlPlane) ValidateUpdateServices(old OpenStackControlPlane
if old.Cinder.Template == nil {
old.Cinder.Template = &cinderv1.CinderSpecCore{}
}
cinderName, _ := r.GetServiceName(CinderName, r.Spec.Cinder.UniquePodNames)
cinderName, _ := r.GetServiceNameCached(CinderName, r.Spec.Cinder.UniquePodNames, r.Spec.Cinder.ServiceName)
errs := common_webhook.ValidateDNS1123Label(
basePath.Child("cinder").Child("template").Child("cinderVolumes"),
maps.Keys(r.Spec.Cinder.Template.CinderVolumes),
Expand Down
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260128074606-03b808364e4a
github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260126092810-cd39d45b6c0e
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.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260124125332-5046d6342e48
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260127154438-ff95971883bb
Expand Down
4 changes: 2 additions & 2 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260126092810-c
github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260126092810-cd39d45b6c0e/go.mod h1:6Y/hPIhXYgV0NHe7ZWIo+bdBxhnWkjbv7VLZbFnLNrc=
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.20260126081203-efc2df9207eb h1:S7tnYO/E1f1KQfcp7N5bam8+ax/ExDTOhZ1WqG4Bfu0=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef h1:SgzLekXtZuApbRylC3unCXnMaUClT5FPuqsxzIjt3Go=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0=
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35 h1:IdcI8DFvW8rXtchONSzbDmhhRp1YyO2YaBJDBXr44Gk=
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:zOX7Y05keiSppIvLabuyh42QHBMhCcoskAtxFRbwXKo=
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260126081203-efc2df9207eb h1:0kP9V1pKfRno6ss7qAy3GcfVK29CobWym6WA7AYA7wY=
Expand Down
4 changes: 4 additions & 0 deletions bindata/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down Expand Up @@ -3914,6 +3916,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down Expand Up @@ -3649,6 +3651,8 @@ spec:
enabled:
default: true
type: boolean
serviceName:
type: string
template:
properties:
apiTimeout:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126175636-114b4c65a959
github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260126081203-efc2df9207eb
github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260124125332-5046d6342e48
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.202601260
github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:tXxVkkk8HlATwTmDA5RTP3b+c8apfuMM15mZ2wW5iNs=
github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260126081203-efc2df9207eb h1:pCyizgwvB2tgFGhGtAV5rXV0kSu9l5RoR2XA+Gd5BuY=
github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:chsg6x4P7376/8MlmsC3OiasuDatbOLwC5D5KRD9fbo=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb h1:S7tnYO/E1f1KQfcp7N5bam8+ax/ExDTOhZ1WqG4Bfu0=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef h1:SgzLekXtZuApbRylC3unCXnMaUClT5FPuqsxzIjt3Go=
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260205083029-d03e9df035ef/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0=
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35 h1:IdcI8DFvW8rXtchONSzbDmhhRp1YyO2YaBJDBXr44Gk=
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:zOX7Y05keiSppIvLabuyh42QHBMhCcoskAtxFRbwXKo=
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260126081203-efc2df9207eb h1:0kP9V1pKfRno6ss7qAy3GcfVK29CobWym6WA7AYA7wY=
Expand Down
14 changes: 11 additions & 3 deletions internal/openstack/cinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
"github.com/openstack-k8s-operators/lib-common/modules/common/webhook"

"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

Expand All @@ -21,8 +22,16 @@ import (

// ReconcileCinder -
func ReconcileCinder(ctx context.Context, instance *corev1beta1.OpenStackControlPlane, version *corev1beta1.OpenStackVersion, helper *helper.Helper) (ctrl.Result, error) {
cinderName, altCinderName := instance.GetServiceName(corev1beta1.CinderName, instance.Spec.Cinder.UniquePodNames)
// Ensure the alternate cinder CR doesn't exist, as the ramdomPodNames flag may have been toggled
Log := GetLogger(ctx)

// Trigger webhook to cache service name if UniquePodNames is enabled and not yet cached
// This handles operator upgrade scenario where existing CRs don't have ServiceName set
if instance.Spec.Cinder.UniquePodNames && instance.Spec.Cinder.ServiceName == "" {
return webhook.EnsureWebhookTrigger(ctx, instance, corev1beta1.ReconcileTriggerAnnotation, "Cinder service name caching", Log, 0)
}

cinderName, altCinderName := instance.GetServiceNameCached(corev1beta1.CinderName, instance.Spec.Cinder.UniquePodNames, instance.Spec.Cinder.ServiceName)
// Ensure the alternate cinder CR doesn't exist, as the randomPodNames flag may have been toggled
cinder := &cinderv1.Cinder{
ObjectMeta: metav1.ObjectMeta{
Name: altCinderName,
Expand Down Expand Up @@ -52,7 +61,6 @@ func ReconcileCinder(ctx context.Context, instance *corev1beta1.OpenStackControl
instance.Status.ContainerImages.CinderVolumeImages = make(map[string]*string)
return ctrl.Result{}, nil
}
Log := GetLogger(ctx)

if instance.Spec.Cinder.Template == nil {
instance.Spec.Cinder.Template = &cinderv1.CinderSpecCore{}
Expand Down
Loading