Skip to content

Commit 91b889e

Browse files
committed
use deterministic hash instead of generateName
1 parent 49a0555 commit 91b889e

File tree

3 files changed

+118
-16
lines changed

3 files changed

+118
-16
lines changed

api/clusters/v1alpha1/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ const (
6666
DeleteWithoutRequestsLabel = GroupName + "/delete-without-requests"
6767
// ProfileLabel is used to make the profile information easily accessible on AccessRequests.
6868
ProfileLabel = GroupName + "/profile"
69+
// RandomizeClusterNameLabel can be set to "true" on ClusterRequests to have the corresponding Cluster get a randomized name.
70+
// This is meant as a tool for operators to resolve conflicts, which itself should be rare.
71+
RandomizeClusterNameLabel = GroupName + "/randomize-cluster-name"
6972
)
7073

7174
const (

api/clusters/v1alpha1/constants/reasons.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ const (
2525
ReasonWaitingForServices = "WaitingForServices"
2626
// ReasonWaitingForServiceDeletion indicates that something is waiting for a service to be deleted.
2727
ReasonWaitingForServiceDeletion = "WaitingForServiceDeletion"
28+
// ReasonClusterConflict indicates that another cluster with the same name and namespace conflicts with the desired one.
29+
ReasonClusterConflict = "ClusterConflict"
2830
)

internal/controllers/scheduler/controller.go

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)