Skip to content

Commit 813991b

Browse files
authored
Merge pull request #7966 from takirala/tga/support-resource-mutators
✨ feat: accept resource mutators in Move operation
2 parents 299024f + a8b898f commit 813991b

File tree

6 files changed

+265
-36
lines changed

6 files changed

+265
-36
lines changed

api/v1beta1/cluster_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const (
3434
// ClusterFinalizer is the finalizer used by the cluster controller to
3535
// cleanup the cluster resources when a Cluster is being deleted.
3636
ClusterFinalizer = "cluster.cluster.x-k8s.io"
37+
38+
// ClusterKind represents the Kind of Cluster.
39+
ClusterKind = "Cluster"
3740
)
3841

3942
// ANCHOR: ClusterSpec

api/v1beta1/clusterclass_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
"k8s.io/apimachinery/pkg/util/intstr"
2626
)
2727

28+
// ClusterClassKind represents the Kind of ClusterClass.
29+
const ClusterClassKind = "ClusterClass"
30+
2831
// +kubebuilder:object:root=true
2932
// +kubebuilder:resource:path=clusterclasses,shortName=cc,scope=Namespaced,categories=cluster-api
3033
// +kubebuilder:storageversion

cmd/clusterctl/client/cluster/mover.go

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
apierrors "k8s.io/apimachinery/pkg/api/errors"
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2929
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
30+
"k8s.io/apimachinery/pkg/runtime"
3031
"k8s.io/apimachinery/pkg/types"
3132
kerrors "k8s.io/apimachinery/pkg/util/errors"
3233
"k8s.io/apimachinery/pkg/util/sets"
@@ -42,10 +43,13 @@ import (
4243
"sigs.k8s.io/cluster-api/util/yaml"
4344
)
4445

46+
// ResourceMutatorFunc holds the type for mutators to be applied on resources during a move operation.
47+
type ResourceMutatorFunc func(u *unstructured.Unstructured) error
48+
4549
// ObjectMover defines methods for moving Cluster API objects to another management cluster.
4650
type ObjectMover interface {
4751
// Move moves all the Cluster API objects existing in a namespace (or from all the namespaces if empty) to a target management cluster.
48-
Move(namespace string, toCluster Client, dryRun bool) error
52+
Move(namespace string, toCluster Client, dryRun bool, mutators ...ResourceMutatorFunc) error
4953

5054
// ToDirectory writes all the Cluster API objects existing in a namespace (or from all the namespaces if empty) to a target directory.
5155
ToDirectory(namespace string, directory string) error
@@ -64,7 +68,7 @@ type objectMover struct {
6468
// ensure objectMover implements the ObjectMover interface.
6569
var _ ObjectMover = &objectMover{}
6670

67-
func (o *objectMover) Move(namespace string, toCluster Client, dryRun bool) error {
71+
func (o *objectMover) Move(namespace string, toCluster Client, dryRun bool, mutators ...ResourceMutatorFunc) error {
6872
log := logf.Log
6973
log.Info("Performing move...")
7074
o.dryRun = dryRun
@@ -92,7 +96,7 @@ func (o *objectMover) Move(namespace string, toCluster Client, dryRun bool) erro
9296
proxy = toCluster.Proxy()
9397
}
9498

95-
return o.move(objectGraph, proxy)
99+
return o.move(objectGraph, proxy, mutators...)
96100
}
97101

98102
func (o *objectMover) ToDirectory(namespace string, directory string) error {
@@ -309,7 +313,7 @@ func getMachineObj(proxy Proxy, machine *node, machineObj *clusterv1.Machine) er
309313
}
310314

311315
// Move moves all the Cluster API objects existing in a namespace (or from all the namespaces if empty) to a target management cluster.
312-
func (o *objectMover) move(graph *objectGraph, toProxy Proxy) error {
316+
func (o *objectMover) move(graph *objectGraph, toProxy Proxy, mutators ...ResourceMutatorFunc) error {
313317
log := logf.Log
314318

315319
clusters := graph.getClusters()
@@ -329,11 +333,9 @@ func (o *objectMover) move(graph *objectGraph, toProxy Proxy) error {
329333
return errors.Wrap(err, "error pausing ClusterClasses")
330334
}
331335

332-
// Ensure all the expected target namespaces are in place before creating objects.
333-
log.V(1).Info("Creating target namespaces, if missing")
334-
if err := o.ensureNamespaces(graph, toProxy); err != nil {
335-
return err
336-
}
336+
// Nb. DO NOT call ensureNamespaces at this point because:
337+
// - namespace will be ensured to exist before creating the resource.
338+
// - If it's done here, we might create a namespace that can end up unused on target cluster (due to mutators).
337339

338340
// Define the move sequence by processing the ownerReference chain, so we ensure that a Kubernetes object is moved only after its owners.
339341
// The sequence is bases on object graph nodes, each one representing a Kubernetes object; nodes are grouped, so bulk of nodes can be moved in parallel. e.g.
@@ -345,11 +347,15 @@ func (o *objectMover) move(graph *objectGraph, toProxy Proxy) error {
345347
// Create all objects group by group, ensuring all the ownerReferences are re-created.
346348
log.Info("Creating objects in the target cluster")
347349
for groupIndex := 0; groupIndex < len(moveSequence.groups); groupIndex++ {
348-
if err := o.createGroup(moveSequence.getGroup(groupIndex), toProxy); err != nil {
350+
if err := o.createGroup(moveSequence.getGroup(groupIndex), toProxy, mutators...); err != nil {
349351
return err
350352
}
351353
}
352354

355+
// Nb. mutators used after this point (after creating the resources on target clusters) are mainly intended for
356+
// using the right namespace to fetch the resource from the target cluster.
357+
// mutators affecting non metadata fields are no-op after this point.
358+
353359
// Delete all objects group by group in reverse order.
354360
log.Info("Deleting objects from the source cluster")
355361
for groupIndex := len(moveSequence.groups) - 1; groupIndex >= 0; groupIndex-- {
@@ -360,13 +366,13 @@ func (o *objectMover) move(graph *objectGraph, toProxy Proxy) error {
360366

361367
// Resume the ClusterClasses in the target management cluster, so the controllers start reconciling it.
362368
log.V(1).Info("Resuming the target ClusterClasses")
363-
if err := setClusterClassPause(toProxy, clusterClasses, false, o.dryRun); err != nil {
369+
if err := setClusterClassPause(toProxy, clusterClasses, false, o.dryRun, mutators...); err != nil {
364370
return errors.Wrap(err, "error resuming ClusterClasses")
365371
}
366372

367373
// Reset the pause field on the Cluster object in the target management cluster, so the controllers start reconciling it.
368374
log.V(1).Info("Resuming the target cluster")
369-
return setClusterPause(toProxy, clusters, false, o.dryRun)
375+
return setClusterPause(toProxy, clusters, false, o.dryRun, mutators...)
370376
}
371377

372378
func (o *objectMover) toDirectory(graph *objectGraph, directory string) error {
@@ -533,7 +539,7 @@ func getMoveSequence(graph *objectGraph) *moveSequence {
533539
}
534540

535541
// setClusterPause sets the paused field on nodes referring to Cluster objects.
536-
func setClusterPause(proxy Proxy, clusters []*node, value bool, dryRun bool) error {
542+
func setClusterPause(proxy Proxy, clusters []*node, value bool, dryRun bool, mutators ...ResourceMutatorFunc) error {
537543
if dryRun {
538544
return nil
539545
}
@@ -554,7 +560,7 @@ func setClusterPause(proxy Proxy, clusters []*node, value bool, dryRun bool) err
554560

555561
// Nb. The operation is wrapped in a retry loop to make setClusterPause more resilient to unexpected conditions.
556562
if err := retryWithExponentialBackoff(setClusterPauseBackoff, func() error {
557-
return patchCluster(proxy, cluster, patch)
563+
return patchCluster(proxy, cluster, patch, mutators...)
558564
}); err != nil {
559565
return errors.Wrapf(err, "error setting Cluster.Spec.Paused=%t", value)
560566
}
@@ -563,7 +569,7 @@ func setClusterPause(proxy Proxy, clusters []*node, value bool, dryRun bool) err
563569
}
564570

565571
// setClusterClassPause sets the paused annotation on nodes referring to ClusterClass objects.
566-
func setClusterClassPause(proxy Proxy, clusterclasses []*node, pause bool, dryRun bool) error {
572+
func setClusterClassPause(proxy Proxy, clusterclasses []*node, pause bool, dryRun bool, mutators ...ResourceMutatorFunc) error {
567573
if dryRun {
568574
return nil
569575
}
@@ -581,7 +587,7 @@ func setClusterClassPause(proxy Proxy, clusterclasses []*node, pause bool, dryRu
581587

582588
// Nb. The operation is wrapped in a retry loop to make setClusterClassPause more resilient to unexpected conditions.
583589
if err := retryWithExponentialBackoff(setClusterClassPauseBackoff, func() error {
584-
return pauseClusterClass(proxy, clusterclass, pause)
590+
return pauseClusterClass(proxy, clusterclass, pause, mutators...)
585591
}); err != nil {
586592
return errors.Wrapf(err, "error updating ClusterClass %s/%s", clusterclass.identity.Namespace, clusterclass.identity.Name)
587593
}
@@ -590,19 +596,29 @@ func setClusterClassPause(proxy Proxy, clusterclasses []*node, pause bool, dryRu
590596
}
591597

592598
// patchCluster applies a patch to a node referring to a Cluster object.
593-
func patchCluster(proxy Proxy, cluster *node, patch client.Patch) error {
599+
func patchCluster(proxy Proxy, n *node, patch client.Patch, mutators ...ResourceMutatorFunc) error {
594600
cFrom, err := proxy.NewClient()
595601
if err != nil {
596602
return err
597603
}
598604

599-
clusterObj := &clusterv1.Cluster{}
600-
clusterObjKey := client.ObjectKey{
601-
Namespace: cluster.identity.Namespace,
602-
Name: cluster.identity.Name,
605+
// Since the patch has been generated already in caller of this function, the ONLY affect that mutators can have
606+
// here is on namespace of the resource.
607+
clusterObj, err := applyMutators(&clusterv1.Cluster{
608+
TypeMeta: metav1.TypeMeta{
609+
Kind: clusterv1.ClusterKind,
610+
APIVersion: clusterv1.GroupVersion.String(),
611+
},
612+
ObjectMeta: metav1.ObjectMeta{
613+
Name: n.identity.Name,
614+
Namespace: n.identity.Namespace,
615+
},
616+
}, mutators...)
617+
if err != nil {
618+
return err
603619
}
604620

605-
if err := cFrom.Get(ctx, clusterObjKey, clusterObj); err != nil {
621+
if err := cFrom.Get(ctx, client.ObjectKeyFromObject(clusterObj), clusterObj); err != nil {
606622
return errors.Wrapf(err, "error reading Cluster %s/%s",
607623
clusterObj.GetNamespace(), clusterObj.GetName())
608624
}
@@ -615,18 +631,35 @@ func patchCluster(proxy Proxy, cluster *node, patch client.Patch) error {
615631
return nil
616632
}
617633

618-
func pauseClusterClass(proxy Proxy, n *node, pause bool) error {
634+
func pauseClusterClass(proxy Proxy, n *node, pause bool, mutators ...ResourceMutatorFunc) error {
619635
cFrom, err := proxy.NewClient()
620636
if err != nil {
621637
return errors.Wrap(err, "error creating client")
622638
}
623639

624-
// Get the ClusterClass from the server
640+
// Get a mutated copy of the ClusterClass to identify the target namespace.
641+
// The ClusterClass could have been moved to a different namespace after the move.
642+
mutatedClusterClass, err := applyMutators(&clusterv1.ClusterClass{
643+
TypeMeta: metav1.TypeMeta{
644+
Kind: clusterv1.ClusterClassKind,
645+
APIVersion: clusterv1.GroupVersion.String(),
646+
},
647+
ObjectMeta: metav1.ObjectMeta{
648+
Name: n.identity.Name,
649+
Namespace: n.identity.Namespace,
650+
}}, mutators...)
651+
if err != nil {
652+
return err
653+
}
654+
625655
clusterClass := &clusterv1.ClusterClass{}
656+
// Construct an object key using the mutatedClusterClass reflecting any changes to the namespace.
626657
clusterClassObjKey := client.ObjectKey{
627-
Name: n.identity.Name,
628-
Namespace: n.identity.Namespace,
658+
Name: mutatedClusterClass.GetName(),
659+
Namespace: mutatedClusterClass.GetNamespace(),
629660
}
661+
// Get a copy of the ClusterClass.
662+
// This will ensure that any other changes from the mutator are ignored here as we work with a fresh copy of the cluster class.
630663
if err := cFrom.Get(ctx, clusterClassObjKey, clusterClass); err != nil {
631664
return errors.Wrapf(err, "error reading ClusterClass %s/%s", n.identity.Namespace, n.identity.Name)
632665
}
@@ -736,7 +769,7 @@ func (o *objectMover) ensureNamespace(toProxy Proxy, namespace string) error {
736769
return err
737770
}
738771

739-
// If the namespace does not exists, create it.
772+
// If the namespace does not exist, create it.
740773
ns = &corev1.Namespace{
741774
TypeMeta: metav1.TypeMeta{
742775
APIVersion: "v1",
@@ -754,15 +787,18 @@ func (o *objectMover) ensureNamespace(toProxy Proxy, namespace string) error {
754787
}
755788

756789
// createGroup creates all the Kubernetes objects into the target management cluster corresponding to the object graph nodes in a moveGroup.
757-
func (o *objectMover) createGroup(group moveGroup, toProxy Proxy) error {
790+
func (o *objectMover) createGroup(group moveGroup, toProxy Proxy, mutators ...ResourceMutatorFunc) error {
758791
createTargetObjectBackoff := newWriteBackoff()
759792
errList := []error{}
760793

794+
// Maintain a cache of namespaces that have been verified to already exist.
795+
// Nb. This prevents us from making repetitive (and expensive) calls in listing all namespaces to ensure a namespace exists before creating a resource.
796+
existingNamespaces := sets.New[string]()
761797
for _, nodeToCreate := range group {
762798
// Creates the Kubernetes object corresponding to the nodeToCreate.
763799
// Nb. The operation is wrapped in a retry loop to make move more resilient to unexpected conditions.
764800
err := retryWithExponentialBackoff(createTargetObjectBackoff, func() error {
765-
return o.createTargetObject(nodeToCreate, toProxy)
801+
return o.createTargetObject(nodeToCreate, toProxy, mutators, existingNamespaces)
766802
})
767803
if err != nil {
768804
errList = append(errList, err)
@@ -821,7 +857,7 @@ func (o *objectMover) restoreGroup(group moveGroup, toProxy Proxy) error {
821857
}
822858

823859
// createTargetObject creates the Kubernetes object in the target Management cluster corresponding to the object graph node, taking care of restoring the OwnerReference with the owner nodes, if any.
824-
func (o *objectMover) createTargetObject(nodeToCreate *node, toProxy Proxy) error {
860+
func (o *objectMover) createTargetObject(nodeToCreate *node, toProxy Proxy, mutators []ResourceMutatorFunc, existingNamespaces sets.Set[string]) error {
825861
log := logf.Log
826862
log.V(1).Info("Creating", nodeToCreate.identity.Kind, nodeToCreate.identity.Name, "Namespace", nodeToCreate.identity.Namespace)
827863

@@ -854,7 +890,7 @@ func (o *objectMover) createTargetObject(nodeToCreate *node, toProxy Proxy) erro
854890
// Removes current OwnerReferences
855891
obj.SetOwnerReferences(nil)
856892

857-
// Rebuild the owne reference chain
893+
// Rebuild the owner reference chain
858894
o.buildOwnerChain(obj, nodeToCreate)
859895

860896
// FIXME Workaround for https://github.com/kubernetes/kubernetes/issues/32220. Remove when the issue is fixed.
@@ -869,6 +905,17 @@ func (o *objectMover) createTargetObject(nodeToCreate *node, toProxy Proxy) erro
869905
return err
870906
}
871907

908+
obj, err = applyMutators(obj, mutators...)
909+
if err != nil {
910+
return err
911+
}
912+
// Applying mutators MAY change the namespace, so ensure the namespace exists before creating the resource.
913+
if !nodeToCreate.isGlobal && !existingNamespaces.Has(obj.GetNamespace()) {
914+
if err = o.ensureNamespace(toProxy, obj.GetNamespace()); err != nil {
915+
return err
916+
}
917+
existingNamespaces.Insert(obj.GetNamespace())
918+
}
872919
oldManagedFields := obj.GetManagedFields()
873920
if err := cTo.Create(ctx, obj); err != nil {
874921
if !apierrors.IsAlreadyExists(err) {
@@ -1195,3 +1242,22 @@ func patchTopologyManagedFields(ctx context.Context, oldManagedFields []metav1.M
11951242
}
11961243
return nil
11971244
}
1245+
1246+
func applyMutators(object client.Object, mutators ...ResourceMutatorFunc) (*unstructured.Unstructured, error) {
1247+
if object == nil {
1248+
return nil, nil
1249+
}
1250+
u := &unstructured.Unstructured{}
1251+
to, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
1252+
if err != nil {
1253+
return nil, err
1254+
}
1255+
u.SetUnstructuredContent(to)
1256+
for _, mutator := range mutators {
1257+
if err := mutator(u); err != nil {
1258+
return nil, errors.Wrapf(err, "error applying resource mutator to %q %s/%s",
1259+
u.GroupVersionKind(), object.GetNamespace(), object.GetName())
1260+
}
1261+
}
1262+
return u, nil
1263+
}

0 commit comments

Comments
 (0)