@@ -17,6 +17,7 @@ import (
1717
1818 "github.com/openmcp-project/controller-utils/pkg/clusters"
1919 "github.com/openmcp-project/controller-utils/pkg/collections/filters"
20+ maputils "github.com/openmcp-project/controller-utils/pkg/collections/maps"
2021 ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller"
2122 errutils "github.com/openmcp-project/controller-utils/pkg/errors"
2223 "github.com/openmcp-project/controller-utils/pkg/logging"
@@ -237,15 +238,82 @@ func (r *ClusterScheduler) handleCreateOrUpdate(ctx context.Context, req reconci
237238 }
238239 }
239240 } else {
240- cluster = r .initializeNewCluster (ctx , cr , cDef )
241+ var err error
242+ cluster , err = r .initializeNewCluster (ctx , cr , cDef )
243+ if err != nil {
244+ rr .ReconcileError = errutils .WithReason (fmt .Errorf ("error initializing new cluster: %w" , err ), cconst .ReasonInternalError )
245+ return rr
246+ }
241247
242- // create Cluster resource
243- if err := r .PlatformCluster .Client ().Create (ctx , cluster ); err != nil {
244- if apierrors .IsAlreadyExists (err ) {
245- rr .ReconcileError = errutils .WithReason (fmt .Errorf ("cluster '%s/%s' already exists, this is not supposed to happen" , cluster .Namespace , cluster .Name ), cconst .ReasonInternalError )
246- return rr
248+ // check for conflicts
249+ // A conflict occurs if
250+ // - a cluster with the same name and namespace already exists
251+ // - and it does not have a finalizer referencing this ClusterRequest
252+ // - but it has a finalizer referencing another ClusterRequest
253+ // - and this other ClusterRequest still exists
254+ conflict := false
255+ fin := cr .FinalizerForCluster ()
256+ existingCluster := & clustersv1alpha1.Cluster {}
257+ existingCluster .Namespace = cluster .Namespace
258+ existingCluster .Name = cluster .Name
259+ finalizersToRemove := sets .New [string ]()
260+ if err := r .PlatformCluster .Client ().Get (ctx , client .ObjectKeyFromObject (existingCluster ), existingCluster ); err == nil {
261+ var crs * clustersv1alpha1.ClusterRequestList
262+ for _ , cfin := range existingCluster .Finalizers {
263+ if cfin == fin {
264+ conflict = false
265+ break
266+ }
267+ // if we have not already found a conflict, check if the cluster contains a finalizer from another request which still exists
268+ if ! conflict && strings .HasPrefix (cfin , clustersv1alpha1 .RequestFinalizerOnClusterPrefix ) {
269+ // check if the other request still exists
270+ if crs == nil {
271+ // fetch existing ClusterRequests if not done already
272+ crs = & clustersv1alpha1.ClusterRequestList {}
273+ if err := r .PlatformCluster .Client ().List (ctx , crs ); err != nil {
274+ rr .ReconcileError = errutils .WithReason (fmt .Errorf ("error listing ClusterRequests to check for conflicts: %w" , err ), cconst .ReasonPlatformClusterInteractionProblem )
275+ return rr
276+ }
277+ found := false
278+ for _ , ecr := range crs .Items {
279+ if cfin == ecr .FinalizerForCluster () {
280+ conflict = true
281+ break
282+ }
283+ }
284+ if ! found {
285+ // the finalizer does not belong to any existing ClusterRequest, let's remove it
286+ finalizersToRemove .Insert (cfin )
287+ }
288+ }
289+ }
247290 }
248- rr .ReconcileError = errutils .WithReason (fmt .Errorf ("error creating cluster '%s/%s': %w" , cluster .Namespace , cluster .Name , err ), cconst .ReasonPlatformClusterInteractionProblem )
291+ } else if ! apierrors .IsNotFound (err ) {
292+ rr .ReconcileError = errutils .WithReason (fmt .Errorf ("error checking whether cluster '%s/%s' exists: %w" , cluster .Namespace , cluster .Name , err ), cconst .ReasonPlatformClusterInteractionProblem )
293+ return rr
294+ }
295+
296+ if conflict {
297+ rr .ReconcileError = errutils .WithReason (fmt .Errorf ("cluster '%s/%s' already exists and is used by a different ClusterRequest (the '%s' label with value 'true' can be set on the ClusterRequest to randomize the cluster name and avoid this conflict)" , cluster .Namespace , cluster .Name , clustersv1alpha1 .RandomizeClusterNameLabel ), cconst .ReasonClusterConflict )
298+ return rr
299+ }
300+
301+ // create/update Cluster resource
302+ // Note that clusters are usually not updated. This should only happen if the status of a ClusterRequest was lost and an existing cluster is recovered.
303+ if _ , err := controllerutil .CreateOrUpdate (ctx , r .PlatformCluster .Client (), existingCluster , func () error {
304+ // merge finalizers, labels, and annotations from initialized cluster into existing one
305+ finSet := sets .New (existingCluster .Finalizers ... )
306+ finSet .Delete (finalizersToRemove .UnsortedList ()... )
307+ finSet .Insert (cluster .Finalizers ... )
308+ existingCluster .Finalizers = sets .List (finSet )
309+ existingCluster .Labels = maputils .Merge (existingCluster .Labels , cluster .Labels )
310+ existingCluster .Annotations = maputils .Merge (existingCluster .Annotations , cluster .Annotations )
311+ // copy spec from initialized cluster
312+ existingCluster .Spec = cluster .Spec
313+
314+ return nil
315+ }); err != nil {
316+ rr .ReconcileError = errutils .WithReason (fmt .Errorf ("error creating/updating cluster '%s/%s': %w" , cluster .Namespace , cluster .Name , err ), cconst .ReasonPlatformClusterInteractionProblem )
249317 return rr
250318 }
251319 }
@@ -377,34 +445,53 @@ func (r *ClusterScheduler) fetchRelevantClusters(ctx context.Context, cr *cluste
377445}
378446
379447// initializeNewCluster creates a new Cluster resource based on the given ClusterRequest and ClusterDefinition.
380- func (r * ClusterScheduler ) initializeNewCluster (ctx context.Context , cr * clustersv1alpha1.ClusterRequest , cDef * config.ClusterDefinition ) * clustersv1alpha1.Cluster {
448+ func (r * ClusterScheduler ) initializeNewCluster (ctx context.Context , cr * clustersv1alpha1.ClusterRequest , cDef * config.ClusterDefinition ) ( * clustersv1alpha1.Cluster , error ) {
381449 log := logging .FromContextOrPanic (ctx )
382450 purpose := cr .Spec .Purpose
383451 cluster := & clustersv1alpha1.Cluster {}
384452 // choose a name for the cluster
385453 // priority as follows:
386454 // - for singleton clusters (shared unlimited):
387- // 1. generateName of template
455+ // 1. generateName of template (1)
388456 // 2. name of template
389457 // 3. purpose
390458 // - for exclusive clusters or shared limited:
391- // 1. generateName of template
392- // 2. purpose used as generateName
459+ // 1. generateName of template (1)
460+ // 2. purpose used as generateName (1)
461+ //
462+ // (1) Note that the kubernetes 'generateName' field will only be used if the ClusterRequest has the 'clusters.openmcp.cloud/randomize-cluster-name' label set to 'true'.
463+ // Otherwise, a random-looking but deterministic name based on a hash of name and namespace of the ClusterRequest will be used.
393464 if cDef .Template .Spec .Tenancy == clustersv1alpha1 .TENANCY_SHARED && cDef .TenancyCount == 0 {
394465 // there will only be one instance of this cluster
395466 if cDef .Template .GenerateName != "" {
396- cluster .SetGenerateName (cDef .Template .GenerateName )
467+ if ctrlutils .HasLabelWithValue (cr , clustersv1alpha1 .RandomizeClusterNameLabel , "true" ) {
468+ cluster .SetGenerateName (cDef .Template .GenerateName )
469+ } else {
470+ name , err := GenerateClusterName (cDef .Template .GenerateName , cr )
471+ if err != nil {
472+ return nil , fmt .Errorf ("error generating cluster name: %w" , err )
473+ }
474+ cluster .SetName (name )
475+ }
397476 } else if cDef .Template .Name != "" {
398477 cluster .SetName (cDef .Template .Name )
399478 } else {
400479 cluster .SetName (purpose )
401480 }
402481 } else {
403482 // there might be multiple instances of this cluster
483+ prefix := purpose + "-"
404484 if cDef .Template .GenerateName != "" {
405- cluster .SetGenerateName (cDef .Template .GenerateName )
485+ prefix = cDef .Template .GenerateName
486+ }
487+ if ctrlutils .HasLabelWithValue (cr , clustersv1alpha1 .RandomizeClusterNameLabel , "true" ) {
488+ cluster .SetGenerateName (prefix )
406489 } else {
407- cluster .SetGenerateName (purpose + "-" )
490+ name , err := GenerateClusterName (prefix , cr )
491+ if err != nil {
492+ return nil , fmt .Errorf ("error generating cluster name: %w" , err )
493+ }
494+ cluster .SetName (name )
408495 }
409496 }
410497 // choose a namespace for the cluster
@@ -428,7 +515,7 @@ func (r *ClusterScheduler) initializeNewCluster(ctx context.Context, cr *cluster
428515 }
429516 }
430517 cluster .SetAnnotations (cDef .Template .Annotations )
431- cluster .Spec = cDef .Template .Spec
518+ cluster .Spec = * cDef .Template .Spec . DeepCopy ()
432519
433520 // set purpose, if not set
434521 if len (cluster .Spec .Purposes ) == 0 {
@@ -439,5 +526,15 @@ func (r *ClusterScheduler) initializeNewCluster(ctx context.Context, cr *cluster
439526 }
440527 }
441528
442- return cluster
529+ return cluster , nil
530+ }
531+
532+ // GenerateClusterName generates a deterministic name for a new Cluster based on a prefix and the corresponding ClusterRequest.
533+ // The name will always contain the prefix, followed by a hash of the ClusterRequest's namespace and name.
534+ func GenerateClusterName (prefix string , cr * clustersv1alpha1.ClusterRequest ) (string , error ) {
535+ suffix , err := ctrlutils .ShortenToXCharacters (ctrlutils .NameHashSHAKE128Base32 (cr .Namespace , cr .Name ), ctrlutils .K8sMaxNameLength - len (prefix ))
536+ if err != nil {
537+ return "" , err
538+ }
539+ return prefix + suffix , nil
443540}
0 commit comments