Skip to content

Commit c8b3eed

Browse files
authored
Merge pull request #11846 from k8s-infra-cherrypick-robot/cherry-pick-11649-to-release-1.9
[release-1.9] ✨ clusterctl move support for a cross namespace ClusterClass reference
2 parents 88ed3fe + 7398fb6 commit c8b3eed

File tree

6 files changed

+480
-32
lines changed

6 files changed

+480
-32
lines changed

cmd/clusterctl/client/cluster/mover.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,7 @@ var (
11901190
// the objects gets immediately deleted (force delete).
11911191
func (o *objectMover) deleteSourceObject(ctx context.Context, nodeToDelete *node) error {
11921192
// Don't delete cluster-wide nodes or nodes that are below a hierarchy that starts with a global object (e.g. a secrets owned by a global identity object).
1193-
if nodeToDelete.isGlobal || nodeToDelete.isGlobalHierarchy {
1193+
if nodeToDelete.isGlobal || nodeToDelete.isGlobalHierarchy || nodeToDelete.shouldNotDelete {
11941194
return nil
11951195
}
11961196

cmd/clusterctl/client/cluster/mover_test.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ func Test_objectMover_move(t *testing.T) {
13051305

13061306
err := csFrom.Get(ctx, key, oFrom)
13071307
if err == nil {
1308-
if !node.isGlobal && !node.isGlobalHierarchy {
1308+
if !node.isGlobal && !node.isGlobalHierarchy && !node.shouldNotDelete {
13091309
t.Errorf("%s %v not deleted in source cluster", oFrom.GetKind(), key)
13101310
continue
13111311
}
@@ -1417,7 +1417,7 @@ func Test_objectMover_move_with_Mutator(t *testing.T) {
14171417

14181418
err := csFrom.Get(ctx, key, oFrom)
14191419
if err == nil {
1420-
if !node.isGlobal && !node.isGlobalHierarchy {
1420+
if !node.isGlobal && !node.isGlobalHierarchy && !node.shouldNotDelete {
14211421
t.Errorf("%s %v not deleted in source cluster", oFrom.GetKind(), key)
14221422
continue
14231423
}
@@ -1846,6 +1846,8 @@ func Test_objectMoverService_ensureNamespaces(t *testing.T) {
18461846

18471847
cluster1 := test.NewFakeCluster("namespace-1", "cluster-1")
18481848
cluster2 := test.NewFakeCluster("namespace-2", "cluster-2")
1849+
cluster3 := test.NewFakeCluster("namespace-1", "cluster-3").WithTopologyClass("cluster-class-1").WithTopologyClassNamespace("namespace-2")
1850+
clusterClass1 := test.NewFakeClusterClass("namespace-2", "cluster-class-1")
18491851
globalObj := test.NewFakeClusterExternalObject("eo-1")
18501852

18511853
clustersObjs := append(cluster1.Objs(), cluster2.Objs()...)
@@ -1876,6 +1878,16 @@ func Test_objectMoverService_ensureNamespaces(t *testing.T) {
18761878
},
18771879
expectedNamespaces: []string{"namespace-1", "namespace-2"},
18781880
},
1881+
{
1882+
name: "ensureNamespaces moves namespace-1 and namespace-2 from cross-namespace CC reference",
1883+
fields: fields{
1884+
objs: append(cluster3.Objs(), clusterClass1.Objs()...),
1885+
},
1886+
args: args{
1887+
toProxy: test.NewFakeProxy(),
1888+
},
1889+
expectedNamespaces: []string{"namespace-1", "namespace-2"},
1890+
},
18791891
{
18801892
name: "ensureNamespaces moves namespace-2 to target which already has namespace-1",
18811893
fields: fields{

cmd/clusterctl/client/cluster/objectgraph.go

+195-15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package cluster
1818

1919
import (
20+
"cmp"
2021
"context"
2122
"fmt"
2223
"strings"
@@ -28,16 +29,22 @@ import (
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3031
"k8s.io/apimachinery/pkg/types"
32+
kerrors "k8s.io/apimachinery/pkg/util/errors"
33+
"k8s.io/apimachinery/pkg/util/sets"
34+
"k8s.io/apimachinery/pkg/util/wait"
35+
"k8s.io/klog/v2"
3136
"sigs.k8s.io/controller-runtime/pkg/client"
3237

3338
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3439
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
3540
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
41+
"sigs.k8s.io/cluster-api/controllers/external"
3642
addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
3743
secretutil "sigs.k8s.io/cluster-api/util/secret"
3844
)
3945

4046
const clusterTopologyNameKey = "cluster.spec.topology.class"
47+
const clusterTopologyNamespaceKey = "cluster.spec.topology.classNamespace"
4148
const clusterResourceSetBindingClusterNameKey = "clusterresourcesetbinding.spec.clustername"
4249

4350
type empty struct{}
@@ -74,6 +81,9 @@ type node struct {
7481
// When this flag is true the object should not be deleted from the source cluster.
7582
isGlobalHierarchy bool
7683

84+
// shouldNotDelete marks object and direct its descendants to not be deleted
85+
shouldNotDelete bool
86+
7787
// virtual records if this node was discovered indirectly, e.g. by processing an OwnerRef, but not yet observed as a concrete object.
7888
virtual bool
7989

@@ -149,6 +159,7 @@ func (n *node) captureAdditionalInformation(obj *unstructured.Unstructured) erro
149159
n.additionalInfo = map[string]interface{}{}
150160
}
151161
n.additionalInfo[clusterTopologyNameKey] = cluster.GetClassKey().Name
162+
n.additionalInfo[clusterTopologyNamespaceKey] = cluster.GetClassKey().Namespace
152163
}
153164
}
154165

@@ -433,7 +444,7 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
433444
objList := new(unstructured.UnstructuredList)
434445

435446
if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
436-
return getObjList(ctx, o.proxy, typeMeta, selectors, objList)
447+
return getObjList(ctx, o.proxy, &typeMeta, selectors, objList)
437448
}); err != nil {
438449
return err
439450
}
@@ -449,7 +460,7 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
449460
providerNamespaceSelector := []client.ListOption{client.InNamespace(p.Namespace)}
450461
providerNamespaceSecretList := new(unstructured.UnstructuredList)
451462
if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
452-
return getObjList(ctx, o.proxy, typeMeta, providerNamespaceSelector, providerNamespaceSecretList)
463+
return getObjList(ctx, o.proxy, &typeMeta, providerNamespaceSelector, providerNamespaceSecretList)
453464
}); err != nil {
454465
return err
455466
}
@@ -471,6 +482,76 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
471482
}
472483
}
473484

485+
// On top of the object in the namespace being moved (secrets from the providers namespace),
486+
// it is also required to discover ClusterClasses in other namespaces referenced by clusters in the namespace being moved.
487+
discoveredExternalCC := sets.Set[string]{}
488+
for _, cluster := range o.getClusters() {
489+
className, hasName := cluster.additionalInfo[clusterTopologyNameKey]
490+
classNamespace, hasNamespace := cluster.additionalInfo[clusterTopologyNamespaceKey]
491+
// If the cluster doesn't have a reference to a ClusterClass, no-op.
492+
if !hasName || !hasNamespace {
493+
continue
494+
}
495+
496+
// If the cluster reference a ClusterClass in the namespace being moved, no-op (it has been already discovered).
497+
if classNamespace == namespace {
498+
continue
499+
}
500+
501+
// If the referenced ClusterClass has been already discovered, no-op.
502+
externalCCKey := klog.KRef(classNamespace.(string), className.(string))
503+
if discoveredExternalCC.Has(externalCCKey.String()) {
504+
continue
505+
}
506+
507+
// Get the CC and referenced templates.
508+
ccUnstructured, err := o.fetchRef(ctx, discoveryBackoff, &corev1.ObjectReference{
509+
Kind: "ClusterClass",
510+
Namespace: externalCCKey.Namespace,
511+
Name: externalCCKey.Name,
512+
APIVersion: clusterv1.GroupVersion.String(),
513+
})
514+
if err != nil {
515+
return errors.Wrap(err, "failed to get ClusterClass")
516+
}
517+
518+
cc := &clusterv1.ClusterClass{}
519+
if err := localScheme.Convert(ccUnstructured, cc, nil); err != nil {
520+
return errors.Wrap(err, "failed to convert Unstructured to ClusterClass")
521+
}
522+
523+
errs := []error{}
524+
_, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.Infrastructure.Ref)
525+
errs = append(errs, err)
526+
_, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.ControlPlane.Ref)
527+
errs = append(errs, err)
528+
529+
if cc.Spec.ControlPlane.MachineInfrastructure != nil {
530+
_, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.ControlPlane.MachineInfrastructure.Ref)
531+
errs = append(errs, err)
532+
}
533+
534+
for _, mdClass := range cc.Spec.Workers.MachineDeployments {
535+
_, err = o.fetchRef(ctx, discoveryBackoff, mdClass.Template.Infrastructure.Ref)
536+
errs = append(errs, err)
537+
_, err = o.fetchRef(ctx, discoveryBackoff, mdClass.Template.Bootstrap.Ref)
538+
errs = append(errs, err)
539+
}
540+
541+
for _, mpClass := range cc.Spec.Workers.MachinePools {
542+
_, err = o.fetchRef(ctx, discoveryBackoff, mpClass.Template.Infrastructure.Ref)
543+
errs = append(errs, err)
544+
_, err = o.fetchRef(ctx, discoveryBackoff, mpClass.Template.Bootstrap.Ref)
545+
errs = append(errs, err)
546+
}
547+
548+
if err := kerrors.NewAggregate(errs); err != nil {
549+
return errors.Wrap(err, "failed to fetch ClusterClass references")
550+
}
551+
552+
discoveredExternalCC.Insert(externalCCKey.String())
553+
}
554+
474555
log.V(1).Info("Total objects", "count", len(o.uidToNode))
475556

476557
// Completes the graph by searching for soft ownership relations such as secrets linked to the cluster
@@ -480,23 +561,52 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
480561
// Completes the graph by setting for each node the list of tenants the node belongs to.
481562
o.setTenants()
482563

483-
return nil
564+
// Ensure objects which are referenced across namespaces are not deleted.
565+
return o.setShouldNotDelete(ctx, namespace)
484566
}
485567

486-
func getObjList(ctx context.Context, proxy Proxy, typeMeta metav1.TypeMeta, selectors []client.ListOption, objList *unstructured.UnstructuredList) error {
568+
// fetchRef collects specified reference and adds to moved objects.
569+
func (o *objectGraph) fetchRef(ctx context.Context, opts wait.Backoff, ref *corev1.ObjectReference) (*unstructured.Unstructured, error) {
570+
if ref == nil {
571+
return nil, nil
572+
}
573+
574+
var obj *unstructured.Unstructured
575+
576+
if err := retryWithExponentialBackoff(ctx, opts, func(ctx context.Context) error {
577+
c, err := o.proxy.NewClient(ctx)
578+
if err != nil {
579+
return err
580+
}
581+
582+
obj, err = external.Get(ctx, c, ref)
583+
return err
584+
}); err != nil {
585+
return nil, errors.Wrapf(err, "failed to get referenced %q resource", ref.String())
586+
}
587+
588+
if err := o.addObj(obj); err != nil {
589+
return nil, errors.Wrapf(err, "failed to add obj (Kind=%s, Name=%s) to graph", obj.GetKind(), obj.GetName())
590+
}
591+
592+
return obj, nil
593+
}
594+
595+
func getObjList(ctx context.Context, proxy Proxy, typeMeta *metav1.TypeMeta, selectors []client.ListOption, objList client.ObjectList) error {
487596
c, err := proxy.NewClient(ctx)
488597
if err != nil {
489598
return err
490599
}
491600

492-
objList.SetAPIVersion(typeMeta.APIVersion)
493-
objList.SetKind(typeMeta.Kind)
601+
if typeMeta != nil {
602+
objList.GetObjectKind().SetGroupVersionKind(typeMeta.GroupVersionKind())
603+
}
494604

495605
if err := c.List(ctx, objList, selectors...); err != nil {
496606
if apierrors.IsNotFound(err) {
497607
return nil
498608
}
499-
return errors.Wrapf(err, "failed to list %q resources", objList.GroupVersionKind())
609+
return errors.Wrapf(err, "failed to list %q resources", objList.GetObjectKind().GroupVersionKind())
500610
}
501611
return nil
502612
}
@@ -615,15 +725,20 @@ func (o *objectGraph) setSoftOwnership() {
615725
}
616726

617727
clusterClasses := o.getClusterClasses()
728+
618729
// Cluster that uses a ClusterClass are soft owned by that ClusterClass.
619730
for _, clusterClass := range clusterClasses {
620731
for _, cluster := range clusters {
621732
// if the cluster uses a managed topology and uses the clusterclass
622733
// set the clusterclass as a soft owner of the cluster.
623-
if className, ok := cluster.additionalInfo[clusterTopologyNameKey]; ok {
624-
if className == clusterClass.identity.Name && clusterClass.identity.Namespace == cluster.identity.Namespace {
625-
cluster.addSoftOwner(clusterClass)
626-
}
734+
className, hasName := cluster.additionalInfo[clusterTopologyNameKey]
735+
classNamespace, hasNamespace := cluster.additionalInfo[clusterTopologyNamespaceKey]
736+
if namespace, ok := classNamespace.(string); !hasNamespace || !ok {
737+
classNamespace = cmp.Or(namespace, cluster.identity.Namespace)
738+
}
739+
740+
if hasName && className == clusterClass.identity.Name && clusterClass.identity.Namespace == classNamespace {
741+
cluster.addSoftOwner(clusterClass)
627742
}
628743
}
629744
}
@@ -649,18 +764,18 @@ func (o *objectGraph) setSoftOwnership() {
649764
func (o *objectGraph) setTenants() {
650765
for _, node := range o.getNodes() {
651766
if node.forceMoveHierarchy {
652-
o.setTenant(node, node, node.isGlobal)
767+
o.setTenantHierarchy(node, node, node.isGlobal)
653768
}
654769
}
655770
}
656771

657-
// setTenant sets a tenant for a node and for its own dependents/sofDependents.
658-
func (o *objectGraph) setTenant(node, tenant *node, isGlobalHierarchy bool) {
772+
// setTenantHierarchy sets a tenant for a node and for its own dependents/softDependents.
773+
func (o *objectGraph) setTenantHierarchy(node, tenant *node, isGlobalHierarchy bool) {
659774
node.tenant[tenant] = empty{}
660775
node.isGlobalHierarchy = node.isGlobalHierarchy || isGlobalHierarchy
661776
for _, other := range o.getNodes() {
662777
if other.isOwnedBy(node) || other.isSoftOwnedBy(node) {
663-
o.setTenant(other, tenant, isGlobalHierarchy)
778+
o.setTenantHierarchy(other, tenant, isGlobalHierarchy)
664779
}
665780
}
666781
}
@@ -674,3 +789,68 @@ func (o *objectGraph) checkVirtualNode() {
674789
}
675790
}
676791
}
792+
793+
// Ensure objects which are referenced across namespaces are not deleted.
794+
func (o *objectGraph) setShouldNotDelete(ctx context.Context, namespace string) error {
795+
if namespace == "" {
796+
return nil
797+
}
798+
799+
// If there are ClusterClasses outside of the namespace being moved, those CC should not be deleted (we are moving a namespace).
800+
for _, class := range o.getClusterClasses() {
801+
if class.identity.Namespace != namespace {
802+
class.shouldNotDelete = true
803+
// Ensure that also the templates referenced by the CC won't be deleted.
804+
o.setShouldNotDeleteHierarchy(class)
805+
}
806+
}
807+
808+
// If there are clusters outside of the namespace being moved and referencing one of ClusterClass in the namespace being moved,
809+
// ensure those ClusterClass are not deleted.
810+
discoveryBackoff := newReadBackoff()
811+
allClusters := &clusterv1.ClusterList{}
812+
if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
813+
return getObjList(ctx, o.proxy, nil, []client.ListOption{}, allClusters)
814+
}); err != nil {
815+
return err
816+
}
817+
818+
for _, cluster := range allClusters.Items {
819+
// ignore cluster in the namespace being moved.
820+
if cluster.Namespace == namespace {
821+
continue
822+
}
823+
824+
// ignore cluster not using a CC.
825+
if cluster.Spec.Topology == nil {
826+
continue
827+
}
828+
829+
// ignore cluster not referencing a CC in the namespace being moved.
830+
if cluster.Spec.Topology.ClassNamespace != namespace {
831+
continue
832+
}
833+
834+
// Otherwise mark the referenced CC as should not be deleted.
835+
for _, class := range o.getClusterClasses() {
836+
if class.identity.Namespace == cluster.Spec.Topology.ClassNamespace && class.identity.Name == cluster.Spec.Topology.Class {
837+
class.shouldNotDelete = true
838+
// Ensure that also the templates referenced by the CC won't be deleted.
839+
o.setShouldNotDeleteHierarchy(class)
840+
}
841+
}
842+
}
843+
844+
return nil
845+
}
846+
847+
// setShouldNotDeleteHierarchy sets should not delete for a node and for its own dependents/softDependents.
848+
func (o *objectGraph) setShouldNotDeleteHierarchy(node *node) {
849+
node.shouldNotDelete = true
850+
for _, other := range o.getNodes() {
851+
// Skip removal only for direct owners from the object namespace
852+
if other.isOwnedBy(node) {
853+
o.setShouldNotDeleteHierarchy(other)
854+
}
855+
}
856+
}

0 commit comments

Comments
 (0)