diff --git a/cmd/clusterctl/client/cluster/mover_test.go b/cmd/clusterctl/client/cluster/mover_test.go index 1ee0aec6392a..1c17a04848ee 100644 --- a/cmd/clusterctl/client/cluster/mover_test.go +++ b/cmd/clusterctl/client/cluster/mover_test.go @@ -1846,6 +1846,8 @@ func Test_objectMoverService_ensureNamespaces(t *testing.T) { cluster1 := test.NewFakeCluster("namespace-1", "cluster-1") cluster2 := test.NewFakeCluster("namespace-2", "cluster-2") + cluster3 := test.NewFakeCluster("namespace-1", "cluster-3").WithTopologyClass("cluster-class-1").WithTopologyClassWithin("namespace-2") + clusterClass1 := test.NewFakeClusterClass("namespace-2", "cluster-class-1") globalObj := test.NewFakeClusterExternalObject("eo-1") clustersObjs := append(cluster1.Objs(), cluster2.Objs()...) @@ -1876,6 +1878,16 @@ func Test_objectMoverService_ensureNamespaces(t *testing.T) { }, expectedNamespaces: []string{"namespace-1", "namespace-2"}, }, + { + name: "ensureNamespaces moves namespace-1 and namespace-2 from cross-namespace CC reference", + fields: fields{ + objs: append(cluster3.Objs(), clusterClass1.Objs()...), + }, + args: args{ + toProxy: test.NewFakeProxy(), + }, + expectedNamespaces: []string{"namespace-1", "namespace-2"}, + }, { name: "ensureNamespaces moves namespace-2 to target which already has namespace-1", fields: fields{ diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go index 73ec6450d742..0d38400a9b9a 100644 --- a/cmd/clusterctl/client/cluster/objectgraph.go +++ b/cmd/clusterctl/client/cluster/objectgraph.go @@ -38,6 +38,7 @@ import ( ) const clusterTopologyNameKey = "cluster.spec.topology.class" +const clusterTopologyNamespaceKey = "cluster.spec.topology.classNamespace" const clusterResourceSetBindingClusterNameKey = "clusterresourcesetbinding.spec.clustername" type empty struct{} @@ -149,6 +150,7 @@ func (n *node) captureAdditionalInformation(obj *unstructured.Unstructured) erro n.additionalInfo = map[string]interface{}{} } n.additionalInfo[clusterTopologyNameKey] = cluster.GetClassKey().Name + n.additionalInfo[clusterTopologyNamespaceKey] = cluster.GetClassKey().Namespace } } @@ -620,7 +622,13 @@ func (o *objectGraph) setSoftOwnership() { for _, cluster := range clusters { // if the cluster uses a managed topology and uses the clusterclass // set the clusterclass as a soft owner of the cluster. - if className, ok := cluster.additionalInfo[clusterTopologyNameKey]; ok { + className, hasName := cluster.additionalInfo[clusterTopologyNameKey] + classNamespace, hasNamespace := cluster.additionalInfo[clusterTopologyNamespaceKey] + if hasNamespace && hasName { + if className == clusterClass.identity.Name && clusterClass.identity.Namespace == classNamespace { + cluster.addSoftOwner(clusterClass) + } + } else if hasName { if className == clusterClass.identity.Name && clusterClass.identity.Namespace == cluster.identity.Namespace { cluster.addSoftOwner(clusterClass) } diff --git a/cmd/clusterctl/client/cluster/objectgraph_test.go b/cmd/clusterctl/client/cluster/objectgraph_test.go index ebc2b1092e68..88d5817a74d5 100644 --- a/cmd/clusterctl/client/cluster/objectgraph_test.go +++ b/cmd/clusterctl/client/cluster/objectgraph_test.go @@ -2047,6 +2047,57 @@ func Test_objectGraph_setSoftOwnership(t *testing.T) { }, }, }, + { + name: "A different namespaced ClusterClass with a soft owned Cluster", + fields: fields{ + objs: func() []client.Object { + objs := test.NewFakeClusterClass("ns1", "class1").Objs() + objs = append(objs, test.NewFakeCluster("ns2", "cluster1").WithTopologyClass("class1").WithTopologyClassWithin("ns1").Objs()...) + + return objs + }(), + }, + want: wantGraph{ + nodes: map[string]wantGraphItem{ + "cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1": { + forceMove: true, + forceMoveHierarchy: true, + }, + "infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/class1": { + owners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1", + }, + }, + "controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/class1": { + owners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1", + }, + }, + "cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1": { + forceMove: true, + forceMoveHierarchy: true, + softOwners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1", // NB. this cluster is not linked to the clusterclass through owner ref, but it is detected as soft ownership + }, + }, + "infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns2/cluster1": { + owners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1", + }, + }, + "/v1, Kind=Secret, ns2/cluster1-ca": { + softOwners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1", // NB. this secret is not linked to the cluster through owner ref, but it is detected as soft ownership + }, + }, + "/v1, Kind=Secret, ns2/cluster1-kubeconfig": { + owners: []string{ + "cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1", + }, + }, + }, + }, + }, { name: "A Cluster with a soft owned ClusterResourceSetBinding", fields: fields{ diff --git a/cmd/clusterctl/internal/test/fake_objects.go b/cmd/clusterctl/internal/test/fake_objects.go index 4fdb9a49897a..295c1fe81f62 100644 --- a/cmd/clusterctl/internal/test/fake_objects.go +++ b/cmd/clusterctl/internal/test/fake_objects.go @@ -44,16 +44,17 @@ import ( ) type FakeCluster struct { - namespace string - name string - controlPlane *FakeControlPlane - machinePools []*FakeMachinePool - machineDeployments []*FakeMachineDeployment - machineSets []*FakeMachineSet - machines []*FakeMachine - withCloudConfigSecret bool - withCredentialSecret bool - topologyClass *string + namespace string + name string + controlPlane *FakeControlPlane + machinePools []*FakeMachinePool + machineDeployments []*FakeMachineDeployment + machineSets []*FakeMachineSet + machines []*FakeMachine + withCloudConfigSecret bool + withCredentialSecret bool + topologyClass *string + topologyClassNamespace *string } // NewFakeCluster return a FakeCluster that can generate a cluster object, all its own ancillary objects: @@ -109,6 +110,11 @@ func (f *FakeCluster) WithTopologyClass(class string) *FakeCluster { return f } +func (f *FakeCluster) WithTopologyClassWithin(namespace string) *FakeCluster { + f.topologyClassNamespace = &namespace + return f +} + func (f *FakeCluster) Objs() []client.Object { clusterInfrastructure := &fakeinfrastructure.GenericInfrastructureCluster{ TypeMeta: metav1.TypeMeta{ @@ -145,6 +151,9 @@ func (f *FakeCluster) Objs() []client.Object { if f.topologyClass != nil { cluster.Spec.Topology = &clusterv1.Topology{Class: *f.topologyClass} + if f.topologyClassNamespace != nil { + cluster.Spec.Topology.ClassNamespace = *f.topologyClassNamespace + } } // Ensure the cluster gets a UID to be used by dependant objects for creating OwnerReferences.