@@ -18,11 +18,14 @@ package v1beta1
1818
1919import (
2020 "context"
21+ "crypto/rand"
22+ "encoding/hex"
2123 "fmt"
2224 "slices"
2325 "strings"
2426
2527 keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
28+ "github.com/openstack-k8s-operators/lib-common/modules/common/object"
2629 "github.com/openstack-k8s-operators/lib-common/modules/common/route"
2730 common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
2831 mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1"
@@ -32,6 +35,7 @@ import (
3235 apierrors "k8s.io/apimachinery/pkg/api/errors"
3336 "k8s.io/apimachinery/pkg/runtime"
3437 "k8s.io/apimachinery/pkg/runtime/schema"
38+ "k8s.io/apimachinery/pkg/types"
3539 "k8s.io/apimachinery/pkg/util/validation/field"
3640 "k8s.io/utils/ptr"
3741 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -61,6 +65,95 @@ import (
6165// log is for logging in this package.
6266var openstackcontrolplanelog = logf .Log .WithName ("openstackcontrolplane-resource" )
6367
68+ // generateRandomID generates a random 5-character hexadecimal ID
69+ // Used for service naming when UniquePodNames is enabled and UID is not yet available
70+ func generateRandomID () (string , error ) {
71+ bytes := make ([]byte , 3 ) // 3 bytes = 6 hex chars, we'll take first 5
72+ if _ , err := rand .Read (bytes ); err != nil {
73+ return "" , err
74+ }
75+ return hex .EncodeToString (bytes )[:5 ], nil
76+ }
77+
78+ // lookupServiceCR attempts to find an existing service CR in the cluster owned by this OpenStackControlPlane
79+ // Returns the CR name if found, empty string if not found or not owned by the given owner UID
80+ // serviceName should be the base service name (e.g., CinderName, GlanceName)
81+ // ownerUID is the UID of the OpenStackControlPlane that should own the CR
82+ // This function lists CRs and finds ones that start with the service name prefix and are owned by ownerUID
83+ func lookupServiceCR (ctx context.Context , c client.Client , namespace , serviceName string , ownerUID types.UID ) (string , error ) {
84+ switch serviceName {
85+ case CinderName :
86+ cinderList := & cinderv1.CinderList {}
87+ if err := c .List (ctx , cinderList , client .InNamespace (namespace )); err != nil {
88+ return "" , fmt .Errorf ("failed to list Cinder CRs: %w" , err )
89+ }
90+ // Find any Cinder CR that starts with "cinder" and is owned by this OpenStackControlPlane
91+ for _ , cinder := range cinderList .Items {
92+ if strings .HasPrefix (cinder .Name , CinderName ) && object .CheckOwnerRefExist (ownerUID , cinder .GetOwnerReferences ()) {
93+ return cinder .Name , nil
94+ }
95+ }
96+
97+ case GlanceName :
98+ glanceList := & glancev1.GlanceList {}
99+ if err := c .List (ctx , glanceList , client .InNamespace (namespace )); err != nil {
100+ return "" , fmt .Errorf ("failed to list Glance CRs: %w" , err )
101+ }
102+ // Find any Glance CR that starts with "glance" and is owned by this OpenStackControlPlane
103+ for _ , glance := range glanceList .Items {
104+ if strings .HasPrefix (glance .Name , GlanceName ) && object .CheckOwnerRefExist (ownerUID , glance .GetOwnerReferences ()) {
105+ return glance .Name , nil
106+ }
107+ }
108+
109+ default :
110+ return "" , fmt .Errorf ("unsupported service name: %s" , serviceName )
111+ }
112+
113+ return "" , nil // Not found or not owned
114+ }
115+
116+ // CacheServiceNameForCreate handles service name caching during CREATE operations
117+ // Generates a random ID since UID is not yet available
118+ func (r * OpenStackControlPlane ) CacheServiceNameForCreate (serviceName string ) (string , error ) {
119+ randomID , err := generateRandomID ()
120+ if err != nil {
121+ return "" , fmt .Errorf ("failed to generate random ID: %w" , err )
122+ }
123+ return fmt .Sprintf ("%s-%s" , serviceName , randomID ), nil
124+ }
125+
126+ // CacheServiceNameForUpdate handles service name caching during UPDATE operations
127+ // Uses existing CR name if it's owned by this OpenStackControlPlane, otherwise generates based on current settings
128+ // This provides robust flip detection: if we created a CR previously, we preserve its name to avoid creating duplicates
129+ func (r * OpenStackControlPlane ) CacheServiceNameForUpdate (ctx context.Context , c client.Client , serviceName string ) (string , error ) {
130+ // Lookup existing CR owned by this OpenStackControlPlane
131+ existingName , err := lookupServiceCR (ctx , c , r .Namespace , serviceName , r .UID )
132+ if err != nil {
133+ return "" , fmt .Errorf ("failed to lookup existing CR: %w" , err )
134+ }
135+
136+ // If we find a CR owned by us, preserve its name regardless of format
137+ // This handles both flip scenarios and prevents creating duplicate CRs:
138+ // - If UniquePodNames changed from false→true, we keep the old "cinder" name
139+ // - If UniquePodNames changed from true→false, we keep the old "cinder-abc" name
140+ // - If UniquePodNames didn't change, we keep the existing name
141+ if existingName != "" {
142+ return existingName , nil
143+ }
144+
145+ // No existing CR found owned by us - generate name based on current UniquePodNames setting
146+ // This handles:
147+ // - First time deployment
148+ // - Operator upgrade scenarios where ServiceName wasn't cached yet
149+ name , _ := r .GetServiceName (serviceName , true )
150+ if name == serviceName {
151+ // GetServiceName returned base name, meaning UID is not available
152+ return "" , fmt .Errorf ("unable to generate service name: no existing CR and UID not available" )
153+ }
154+ return name , nil
155+ }
156+
64157// ValidateCreate validates the OpenStackControlPlane on creation
65158func (r * OpenStackControlPlane ) ValidateCreate (ctx context.Context , c client.Client ) (admission.Warnings , error ) {
66159 openstackcontrolplanelog .Info ("validate create" , "name" , r .Name )
@@ -293,7 +386,7 @@ func (r *OpenStackControlPlane) ValidateCreateServices(basePath *field.Path) (ad
293386 }
294387
295388 if r .Spec .Glance .Enabled {
296- glanceName , _ := r .GetServiceName (GlanceName , r .Spec .Glance .UniquePodNames )
389+ glanceName , _ := r .GetServiceNameCached (GlanceName , r .Spec .Glance .UniquePodNames , r . Spec . Glance . ServiceName )
297390 for key , glanceAPI := range r .Spec .Glance .Template .GlanceAPIs {
298391 err := common_webhook .ValidateDNS1123Label (
299392 basePath .Child ("glance" ).Child ("template" ).Child ("glanceAPIs" ),
@@ -310,7 +403,7 @@ func (r *OpenStackControlPlane) ValidateCreateServices(basePath *field.Path) (ad
310403 }
311404
312405 if r .Spec .Cinder .Enabled {
313- cinderName , _ := r .GetServiceName (CinderName , r .Spec .Cinder .UniquePodNames )
406+ cinderName , _ := r .GetServiceNameCached (CinderName , r .Spec .Cinder .UniquePodNames , r . Spec . Cinder . ServiceName )
314407 errs := common_webhook .ValidateDNS1123Label (
315408 basePath .Child ("cinder" ).Child ("template" ).Child ("cinderVolumes" ),
316409 maps .Keys (r .Spec .Cinder .Template .CinderVolumes ),
@@ -477,7 +570,7 @@ func (r *OpenStackControlPlane) ValidateUpdateServices(old OpenStackControlPlane
477570 if old .Glance .Template == nil {
478571 old .Glance .Template = & glancev1.GlanceSpecCore {}
479572 }
480- glanceName , _ := r .GetServiceName (GlanceName , r .Spec .Glance .UniquePodNames )
573+ glanceName , _ := r .GetServiceNameCached (GlanceName , r .Spec .Glance .UniquePodNames , r . Spec . Glance . ServiceName )
481574 for key , glanceAPI := range r .Spec .Glance .Template .GlanceAPIs {
482575 err := common_webhook .ValidateDNS1123Label (
483576 basePath .Child ("glance" ).Child ("template" ).Child ("glanceAPIs" ),
@@ -497,7 +590,7 @@ func (r *OpenStackControlPlane) ValidateUpdateServices(old OpenStackControlPlane
497590 if old .Cinder .Template == nil {
498591 old .Cinder .Template = & cinderv1.CinderSpecCore {}
499592 }
500- cinderName , _ := r .GetServiceName (CinderName , r .Spec .Cinder .UniquePodNames )
593+ cinderName , _ := r .GetServiceNameCached (CinderName , r .Spec .Cinder .UniquePodNames , r . Spec . Cinder . ServiceName )
501594 errs := common_webhook .ValidateDNS1123Label (
502595 basePath .Child ("cinder" ).Child ("template" ).Child ("cinderVolumes" ),
503596 maps .Keys (r .Spec .Cinder .Template .CinderVolumes ),
0 commit comments