sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/objectgraph_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cluster
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"testing"
    24  
    25  	. "github.com/onsi/gomega"
    26  	"github.com/pkg/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    34  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    35  	"sigs.k8s.io/cluster-api/internal/test/builder"
    36  )
    37  
    38  func TestObjectGraph_getDiscoveryTypeMetaList(t *testing.T) {
    39  	type fields struct {
    40  		proxy Proxy
    41  	}
    42  	tests := []struct {
    43  		name    string
    44  		fields  fields
    45  		want    map[string]*discoveryTypeInfo
    46  		wantErr bool
    47  	}{
    48  		{
    49  			name: "Return CRDs + ConfigMap & Secrets",
    50  			fields: fields{
    51  				proxy: test.NewFakeProxy().
    52  					WithObjs(
    53  						test.FakeNamespacedCustomResourceDefinition("foo", "Bar", "v2", "v1"), // NB. foo/v1 Bar is not a storage version, so it should be ignored
    54  						test.FakeNamespacedCustomResourceDefinition("foo", "Baz", "v1"),
    55  					),
    56  			},
    57  			want: map[string]*discoveryTypeInfo{
    58  				"bars.foo": {
    59  					typeMeta:           metav1.TypeMeta{Kind: "Bar", APIVersion: "foo/v2"},
    60  					forceMove:          false,
    61  					forceMoveHierarchy: false,
    62  					scope:              "Namespaced",
    63  				},
    64  				"bazs.foo": {
    65  					typeMeta:           metav1.TypeMeta{Kind: "Baz", APIVersion: "foo/v1"},
    66  					forceMove:          false,
    67  					forceMoveHierarchy: false,
    68  					scope:              "Namespaced",
    69  				},
    70  				"secrets.v1": {
    71  					typeMeta:           metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
    72  					forceMove:          false,
    73  					forceMoveHierarchy: false,
    74  					scope:              "",
    75  				},
    76  				"configmaps.v1": {
    77  					typeMeta:           metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
    78  					forceMove:          false,
    79  					forceMoveHierarchy: false,
    80  					scope:              "",
    81  				},
    82  			},
    83  			wantErr: false,
    84  		},
    85  		{
    86  			name: "Enforce force move for Cluster and ClusterResourceSet",
    87  			fields: fields{
    88  				proxy: test.NewFakeProxy().
    89  					WithObjs(
    90  						test.FakeNamespacedCustomResourceDefinition("cluster.x-k8s.io", "Cluster", "v1beta1"),
    91  						test.FakeNamespacedCustomResourceDefinition("addons.cluster.x-k8s.io", "ClusterResourceSet", "v1beta1"),
    92  					),
    93  			},
    94  			want: map[string]*discoveryTypeInfo{
    95  				"clusters.cluster.x-k8s.io": {
    96  					typeMeta:           metav1.TypeMeta{Kind: "Cluster", APIVersion: "cluster.x-k8s.io/v1beta1"},
    97  					forceMove:          true,
    98  					forceMoveHierarchy: true,
    99  					scope:              "Namespaced",
   100  				},
   101  				"clusterresourcesets.addons.cluster.x-k8s.io": {
   102  					typeMeta:           metav1.TypeMeta{Kind: "ClusterResourceSet", APIVersion: "addons.cluster.x-k8s.io/v1beta1"},
   103  					forceMove:          true,
   104  					forceMoveHierarchy: true,
   105  					scope:              "Namespaced",
   106  				},
   107  				"secrets.v1": {
   108  					typeMeta:           metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
   109  					forceMove:          false,
   110  					forceMoveHierarchy: false,
   111  					scope:              "",
   112  				},
   113  				"configmaps.v1": {
   114  					typeMeta:           metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
   115  					forceMove:          false,
   116  					forceMoveHierarchy: false,
   117  					scope:              "",
   118  				},
   119  			},
   120  			wantErr: false,
   121  		},
   122  		{
   123  			name: "Identified Cluster scoped types",
   124  			fields: fields{
   125  				proxy: test.NewFakeProxy().
   126  					WithObjs(
   127  						test.FakeClusterCustomResourceDefinition("infrastructure.cluster.x-k8s.io", "GenericClusterInfrastructureIdentity", "v1beta1"),
   128  					),
   129  			},
   130  			want: map[string]*discoveryTypeInfo{
   131  				"genericclusterinfrastructureidentitys.infrastructure.cluster.x-k8s.io": {
   132  					typeMeta:           metav1.TypeMeta{Kind: "GenericClusterInfrastructureIdentity", APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1"},
   133  					forceMove:          false,
   134  					forceMoveHierarchy: false,
   135  					scope:              "Cluster",
   136  				},
   137  				"secrets.v1": {
   138  					typeMeta:           metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
   139  					forceMove:          false,
   140  					forceMoveHierarchy: false,
   141  					scope:              "",
   142  				},
   143  				"configmaps.v1": {
   144  					typeMeta:           metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
   145  					forceMove:          false,
   146  					forceMoveHierarchy: false,
   147  					scope:              "",
   148  				},
   149  			},
   150  			wantErr: false,
   151  		},
   152  		{
   153  			name: "Identified force move label",
   154  			fields: fields{
   155  				proxy: test.NewFakeProxy().
   156  					WithObjs(
   157  						func() client.Object {
   158  							crd := test.FakeNamespacedCustomResourceDefinition("foo", "Bar", "v1")
   159  							crd.Labels[clusterctlv1.ClusterctlMoveLabel] = ""
   160  							return crd
   161  						}(),
   162  					),
   163  			},
   164  			want: map[string]*discoveryTypeInfo{
   165  				"bars.foo": {
   166  					typeMeta:           metav1.TypeMeta{Kind: "Bar", APIVersion: "foo/v1"},
   167  					forceMove:          true,
   168  					forceMoveHierarchy: false,
   169  					scope:              "Namespaced",
   170  				},
   171  				"secrets.v1": {
   172  					typeMeta:           metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
   173  					forceMove:          false,
   174  					forceMoveHierarchy: false,
   175  					scope:              "",
   176  				},
   177  				"configmaps.v1": {
   178  					typeMeta:           metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
   179  					forceMove:          false,
   180  					forceMoveHierarchy: false,
   181  					scope:              "",
   182  				},
   183  			},
   184  			wantErr: false,
   185  		},
   186  		{
   187  			name: "Identified force move hierarchy label",
   188  			fields: fields{
   189  				proxy: test.NewFakeProxy().
   190  					WithObjs(
   191  						func() client.Object {
   192  							crd := test.FakeNamespacedCustomResourceDefinition("foo", "Bar", "v1")
   193  							crd.Labels[clusterctlv1.ClusterctlMoveHierarchyLabel] = ""
   194  							return crd
   195  						}(),
   196  					),
   197  			},
   198  			want: map[string]*discoveryTypeInfo{
   199  				"bars.foo": {
   200  					typeMeta:           metav1.TypeMeta{Kind: "Bar", APIVersion: "foo/v1"},
   201  					forceMove:          true, // force move is implicit when there is forceMoveHierarchy
   202  					forceMoveHierarchy: true,
   203  					scope:              "Namespaced",
   204  				},
   205  				"secrets.v1": {
   206  					typeMeta:           metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
   207  					forceMove:          false,
   208  					forceMoveHierarchy: false,
   209  					scope:              "",
   210  				},
   211  				"configmaps.v1": {
   212  					typeMeta:           metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
   213  					forceMove:          false,
   214  					forceMoveHierarchy: false,
   215  					scope:              "",
   216  				},
   217  			},
   218  			wantErr: false,
   219  		},
   220  	}
   221  	for _, tt := range tests {
   222  		t.Run(tt.name, func(t *testing.T) {
   223  			g := NewWithT(t)
   224  
   225  			ctx := context.Background()
   226  
   227  			graph := newObjectGraph(tt.fields.proxy, nil)
   228  			err := graph.getDiscoveryTypes(ctx)
   229  			if tt.wantErr {
   230  				g.Expect(err).To(HaveOccurred())
   231  				return
   232  			}
   233  
   234  			g.Expect(err).ToNot(HaveOccurred())
   235  			g.Expect(graph.types).To(Equal(tt.want))
   236  		})
   237  	}
   238  }
   239  
   240  type wantGraphItem struct {
   241  	virtual            bool
   242  	isGlobal           bool
   243  	forceMove          bool
   244  	forceMoveHierarchy bool
   245  	owners             []string
   246  	softOwners         []string
   247  }
   248  
   249  type wantGraph struct {
   250  	nodes map[string]wantGraphItem
   251  }
   252  
   253  func assertGraph(t *testing.T, got *objectGraph, want wantGraph) {
   254  	t.Helper()
   255  
   256  	g := NewWithT(t)
   257  
   258  	g.Expect(got.uidToNode).To(HaveLen(len(want.nodes)), "the number of nodes in the objectGraph doesn't match the number of expected nodes - got: %d expected: %d", len(got.uidToNode), len(want.nodes))
   259  
   260  	for uid, wantNode := range want.nodes {
   261  		gotNode, ok := got.uidToNode[types.UID(uid)]
   262  		g.Expect(ok).To(BeTrue(), "node %q not found", uid)
   263  		g.Expect(gotNode.virtual).To(Equal(wantNode.virtual), "node %q.virtual does not have the expected value", uid)
   264  		g.Expect(gotNode.isGlobal).To(Equal(wantNode.isGlobal), "node %q.isGlobal does not have the expected value", uid)
   265  		g.Expect(gotNode.forceMove).To(Equal(wantNode.forceMove), "node %q.forceMove does not have the expected value", uid)
   266  		g.Expect(gotNode.forceMoveHierarchy).To(Equal(wantNode.forceMoveHierarchy), "node %q.forceMoveHierarchy does not have the expected value", uid)
   267  		g.Expect(gotNode.owners).To(HaveLen(len(wantNode.owners)), "node %q.owner does not have the expected length", uid)
   268  
   269  		for _, wantOwner := range wantNode.owners {
   270  			found := false
   271  			for k := range gotNode.owners {
   272  				if k.identity.UID == types.UID(wantOwner) {
   273  					found = true
   274  					break
   275  				}
   276  			}
   277  			g.Expect(found).To(BeTrue(), "node %q.owners does not contain %q", uid, wantOwner)
   278  		}
   279  
   280  		g.Expect(gotNode.softOwners).To(HaveLen(len(wantNode.softOwners)), "node %q.softOwners does not have the expected length", uid)
   281  
   282  		for _, wantOwner := range wantNode.softOwners {
   283  			found := false
   284  			for k := range gotNode.softOwners {
   285  				if k.identity.UID == types.UID(wantOwner) {
   286  					found = true
   287  					break
   288  				}
   289  			}
   290  			g.Expect(found).To(BeTrue(), "node %q.softOwners does not contain %q", uid, wantOwner)
   291  		}
   292  	}
   293  }
   294  
   295  func TestObjectGraph_addObj(t *testing.T) {
   296  	type args struct {
   297  		objs []*unstructured.Unstructured
   298  	}
   299  
   300  	tests := []struct {
   301  		name string
   302  		args args
   303  		want wantGraph
   304  	}{
   305  		{
   306  			name: "Add a single object",
   307  			args: args{
   308  				objs: []*unstructured.Unstructured{
   309  					{
   310  						Object: map[string]interface{}{
   311  							"apiVersion": "a/v1",
   312  							"kind":       "A",
   313  							"metadata": map[string]interface{}{
   314  								"namespace": "ns",
   315  								"name":      "foo",
   316  								"uid":       "1",
   317  							},
   318  						},
   319  					},
   320  				},
   321  			},
   322  			want: wantGraph{
   323  				nodes: map[string]wantGraphItem{
   324  					"1": { // the object: not virtual (observed), without owner ref
   325  						virtual: false,
   326  						owners:  nil,
   327  					},
   328  				},
   329  			},
   330  		},
   331  		{
   332  			name: "Add a single object with an owner ref",
   333  			args: args{
   334  				objs: []*unstructured.Unstructured{
   335  					{
   336  						Object: map[string]interface{}{
   337  							"apiVersion": "a/v1",
   338  							"kind":       "A",
   339  							"metadata": map[string]interface{}{
   340  								"namespace": "ns",
   341  								"name":      "foo",
   342  								"uid":       "1",
   343  								"ownerReferences": []interface{}{
   344  									map[string]interface{}{
   345  										"apiVersion": "b/v1",
   346  										"kind":       "B",
   347  										"name":       "bar",
   348  										"uid":        "2",
   349  									},
   350  								},
   351  							},
   352  						},
   353  					},
   354  				},
   355  			},
   356  			want: wantGraph{
   357  				nodes: map[string]wantGraphItem{
   358  					"1": { // the object: not virtual (observed), with 1 owner refs
   359  						virtual: false,
   360  						owners:  []string{"2"},
   361  					},
   362  					"2": { // the object owner: virtual (not yet observed), without owner refs
   363  						virtual: true,
   364  						owners:  nil,
   365  					},
   366  				},
   367  			},
   368  		},
   369  		{
   370  			name: "Add an object with an owner ref and its owner",
   371  			args: args{
   372  				objs: []*unstructured.Unstructured{
   373  					{
   374  						Object: map[string]interface{}{
   375  							"apiVersion": "a/v1",
   376  							"kind":       "A",
   377  							"metadata": map[string]interface{}{
   378  								"namespace": "ns",
   379  								"name":      "foo",
   380  								"uid":       "1",
   381  								"ownerReferences": []interface{}{
   382  									map[string]interface{}{
   383  										"apiVersion": "b/v1",
   384  										"kind":       "B",
   385  										"name":       "bar",
   386  										"uid":        "2",
   387  									},
   388  								},
   389  							},
   390  						},
   391  					},
   392  					{
   393  						Object: map[string]interface{}{
   394  							"apiVersion": "b/v1",
   395  							"kind":       "B",
   396  							"metadata": map[string]interface{}{
   397  								"namespace": "ns",
   398  								"name":      "bar",
   399  								"uid":       "2",
   400  							},
   401  						},
   402  					},
   403  				},
   404  			},
   405  			want: wantGraph{
   406  				nodes: map[string]wantGraphItem{
   407  					"1": { // the object: not virtual (observed), with 1 owner refs
   408  						virtual: false,
   409  						owners:  []string{"2"},
   410  					},
   411  					"2": { // the object owner: not virtual (observed), without owner refs
   412  						virtual: false,
   413  						owners:  nil,
   414  					},
   415  				},
   416  			},
   417  		},
   418  		{
   419  			name: "Add an object with an owner ref and its owner (reverse discovery order)",
   420  			args: args{
   421  				objs: []*unstructured.Unstructured{
   422  					{
   423  						Object: map[string]interface{}{
   424  							"apiVersion": "b/v1",
   425  							"kind":       "B",
   426  							"metadata": map[string]interface{}{
   427  								"namespace": "ns",
   428  								"name":      "bar",
   429  								"uid":       "2",
   430  							},
   431  						},
   432  					},
   433  					{
   434  						Object: map[string]interface{}{
   435  							"apiVersion": "a/v1",
   436  							"kind":       "A",
   437  							"metadata": map[string]interface{}{
   438  								"namespace": "ns",
   439  								"name":      "foo",
   440  								"uid":       "1",
   441  								"ownerReferences": []interface{}{
   442  									map[string]interface{}{
   443  										"apiVersion": "b/v1",
   444  										"kind":       "B",
   445  										"name":       "bar",
   446  										"uid":        "2",
   447  									},
   448  								},
   449  							},
   450  						},
   451  					},
   452  				},
   453  			},
   454  			want: wantGraph{
   455  				nodes: map[string]wantGraphItem{
   456  					"1": { // the object: not virtual (observed), with 1 owner refs
   457  						virtual: false,
   458  						owners:  []string{"2"},
   459  					},
   460  					"2": { // the object owner: not virtual (observed), without owner refs
   461  						virtual: false,
   462  						owners:  nil,
   463  					},
   464  				},
   465  			},
   466  		},
   467  	}
   468  	for _, tt := range tests {
   469  		t.Run(tt.name, func(t *testing.T) {
   470  			graph := newObjectGraph(nil, nil)
   471  			for _, o := range tt.args.objs {
   472  				if err := graph.addObj(o); err != nil {
   473  					panic(fmt.Sprintf("failed when adding object to graph: %v", o))
   474  				}
   475  			}
   476  
   477  			assertGraph(t, graph, tt.want)
   478  		})
   479  	}
   480  }
   481  
   482  type objectGraphTestArgs struct {
   483  	objs []client.Object
   484  }
   485  
   486  var objectGraphsTests = []struct {
   487  	name    string
   488  	args    objectGraphTestArgs
   489  	want    wantGraph
   490  	wantErr bool
   491  }{
   492  	{
   493  		name: "Cluster",
   494  		args: objectGraphTestArgs{
   495  			objs: test.NewFakeCluster("ns1", "cluster1").Objs(),
   496  		},
   497  		want: wantGraph{
   498  			nodes: map[string]wantGraphItem{
   499  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   500  					forceMove:          true,
   501  					forceMoveHierarchy: true,
   502  				},
   503  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   504  					owners: []string{
   505  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   506  					},
   507  				},
   508  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   509  					softOwners: []string{
   510  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   511  					},
   512  				},
   513  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   514  					owners: []string{
   515  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   516  					},
   517  				},
   518  			},
   519  		},
   520  	},
   521  	{
   522  		name: "Cluster with cloud config secret with the force move label",
   523  		args: objectGraphTestArgs{
   524  			objs: test.NewFakeCluster("ns1", "cluster1").
   525  				WithCloudConfigSecret().Objs(),
   526  		},
   527  		want: wantGraph{
   528  			nodes: map[string]wantGraphItem{
   529  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   530  					forceMove:          true,
   531  					forceMoveHierarchy: true,
   532  				},
   533  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   534  					owners: []string{
   535  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   536  					},
   537  				},
   538  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   539  					softOwners: []string{
   540  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   541  					},
   542  				},
   543  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   544  					owners: []string{
   545  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   546  					},
   547  				},
   548  				"/v1, Kind=Secret, ns1/cluster1-cloud-config": {
   549  					forceMove: true,
   550  				},
   551  			},
   552  		},
   553  	},
   554  	{
   555  		name: "Two clusters",
   556  		args: objectGraphTestArgs{
   557  			objs: func() []client.Object {
   558  				objs := []client.Object{}
   559  				objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
   560  				objs = append(objs, test.NewFakeCluster("ns1", "cluster2").Objs()...)
   561  				return objs
   562  			}(),
   563  		},
   564  		want: wantGraph{
   565  			nodes: map[string]wantGraphItem{
   566  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   567  					forceMove:          true,
   568  					forceMoveHierarchy: true,
   569  				},
   570  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   571  					owners: []string{
   572  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   573  					},
   574  				},
   575  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   576  					softOwners: []string{
   577  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   578  					},
   579  				},
   580  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   581  					owners: []string{
   582  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   583  					},
   584  				},
   585  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2": {
   586  					forceMove:          true,
   587  					forceMoveHierarchy: true,
   588  				},
   589  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster2": {
   590  					owners: []string{
   591  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
   592  					},
   593  				},
   594  				"/v1, Kind=Secret, ns1/cluster2-ca": {
   595  					softOwners: []string{
   596  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2", // NB. this secret is not linked to the cluster through owner ref
   597  					},
   598  				},
   599  				"/v1, Kind=Secret, ns1/cluster2-kubeconfig": {
   600  					owners: []string{
   601  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
   602  					},
   603  				},
   604  			},
   605  		},
   606  	},
   607  	{
   608  		name: "Cluster with machine",
   609  		args: objectGraphTestArgs{
   610  			objs: test.NewFakeCluster("ns1", "cluster1").
   611  				WithMachines(
   612  					test.NewFakeMachine("m1"),
   613  				).Objs(),
   614  		},
   615  		want: wantGraph{
   616  			nodes: map[string]wantGraphItem{
   617  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   618  					forceMove:          true,
   619  					forceMoveHierarchy: true,
   620  				},
   621  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   622  					owners: []string{
   623  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   624  					},
   625  				},
   626  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   627  					softOwners: []string{
   628  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   629  					},
   630  				},
   631  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   632  					owners: []string{
   633  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   634  					},
   635  				},
   636  
   637  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1": {
   638  					owners: []string{
   639  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   640  					},
   641  				},
   642  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/m1": {
   643  					owners: []string{
   644  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   645  					},
   646  				},
   647  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1": {
   648  					owners: []string{
   649  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   650  					},
   651  				},
   652  				"/v1, Kind=Secret, ns1/m1": {
   653  					owners: []string{
   654  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   655  					},
   656  				},
   657  				"/v1, Kind=Secret, ns1/cluster1-sa": {
   658  					owners: []string{
   659  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   660  					},
   661  				},
   662  			},
   663  		},
   664  	},
   665  	{
   666  		name: "Cluster with MachineSet",
   667  		args: objectGraphTestArgs{
   668  			objs: test.NewFakeCluster("ns1", "cluster1").
   669  				WithMachineSets(
   670  					test.NewFakeMachineSet("ms1").
   671  						WithMachines(test.NewFakeMachine("m1")),
   672  				).Objs(),
   673  		},
   674  		want: wantGraph{
   675  			nodes: map[string]wantGraphItem{
   676  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   677  					forceMove:          true,
   678  					forceMoveHierarchy: true,
   679  				},
   680  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   681  					owners: []string{
   682  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   683  					},
   684  				},
   685  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   686  					softOwners: []string{
   687  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   688  					},
   689  				},
   690  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   691  					owners: []string{
   692  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   693  					},
   694  				},
   695  
   696  				"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1": {
   697  					owners: []string{
   698  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   699  					},
   700  				},
   701  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/ms1": {
   702  					owners: []string{
   703  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   704  					},
   705  				},
   706  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/ms1": {
   707  					owners: []string{
   708  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   709  					},
   710  				},
   711  
   712  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1": {
   713  					owners: []string{
   714  						"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1",
   715  					},
   716  				},
   717  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/m1": {
   718  					owners: []string{
   719  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   720  					},
   721  				},
   722  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1": {
   723  					owners: []string{
   724  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   725  					},
   726  				},
   727  				"/v1, Kind=Secret, ns1/m1": {
   728  					owners: []string{
   729  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   730  					},
   731  				},
   732  			},
   733  		},
   734  	},
   735  	{
   736  		name: "Cluster with MachineDeployment",
   737  		args: objectGraphTestArgs{
   738  			objs: test.NewFakeCluster("ns1", "cluster1").
   739  				WithMachineDeployments(
   740  					test.NewFakeMachineDeployment("md1").
   741  						WithMachineSets(
   742  							test.NewFakeMachineSet("ms1").
   743  								WithMachines(
   744  									test.NewFakeMachine("m1"),
   745  								),
   746  						),
   747  				).Objs(),
   748  		},
   749  		want: wantGraph{
   750  			nodes: map[string]wantGraphItem{
   751  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   752  					forceMove:          true,
   753  					forceMoveHierarchy: true,
   754  				},
   755  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   756  					owners: []string{
   757  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   758  					},
   759  				},
   760  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   761  					softOwners: []string{
   762  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   763  					},
   764  				},
   765  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   766  					owners: []string{
   767  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   768  					},
   769  				},
   770  
   771  				"cluster.x-k8s.io/v1beta1, Kind=MachineDeployment, ns1/md1": {
   772  					owners: []string{
   773  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   774  					},
   775  				},
   776  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/md1": {
   777  					owners: []string{
   778  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   779  					},
   780  				},
   781  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/md1": {
   782  					owners: []string{
   783  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   784  					},
   785  				},
   786  
   787  				"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1": {
   788  					owners: []string{
   789  						"cluster.x-k8s.io/v1beta1, Kind=MachineDeployment, ns1/md1",
   790  					},
   791  				},
   792  
   793  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1": {
   794  					owners: []string{
   795  						"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1",
   796  					},
   797  				},
   798  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/m1": {
   799  					owners: []string{
   800  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   801  					},
   802  				},
   803  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1": {
   804  					owners: []string{
   805  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   806  					},
   807  				},
   808  				"/v1, Kind=Secret, ns1/m1": {
   809  					owners: []string{
   810  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   811  					},
   812  				},
   813  			},
   814  		},
   815  	},
   816  	{
   817  		name: "Cluster with MachineDeployment without a BootstrapConfigRef",
   818  		args: objectGraphTestArgs{
   819  			objs: test.NewFakeCluster("ns1", "cluster1").
   820  				WithMachineDeployments(
   821  					test.NewFakeMachineDeployment("md1").
   822  						WithStaticBootstrapConfig().
   823  						WithMachineSets(
   824  							test.NewFakeMachineSet("ms1").
   825  								WithMachines(
   826  									test.NewFakeMachine("m1"),
   827  								),
   828  						),
   829  				).Objs(),
   830  		},
   831  		want: wantGraph{
   832  			nodes: map[string]wantGraphItem{
   833  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   834  					forceMove:          true,
   835  					forceMoveHierarchy: true,
   836  				},
   837  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   838  					owners: []string{
   839  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   840  					},
   841  				},
   842  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   843  					softOwners: []string{
   844  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   845  					},
   846  				},
   847  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   848  					owners: []string{
   849  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   850  					},
   851  				},
   852  
   853  				"cluster.x-k8s.io/v1beta1, Kind=MachineDeployment, ns1/md1": {
   854  					owners: []string{
   855  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   856  					},
   857  				},
   858  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/md1": {
   859  					owners: []string{
   860  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   861  					},
   862  				},
   863  
   864  				"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1": {
   865  					owners: []string{
   866  						"cluster.x-k8s.io/v1beta1, Kind=MachineDeployment, ns1/md1",
   867  					},
   868  				},
   869  
   870  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1": {
   871  					owners: []string{
   872  						"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/ms1",
   873  					},
   874  				},
   875  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/m1": {
   876  					owners: []string{
   877  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   878  					},
   879  				},
   880  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1": {
   881  					owners: []string{
   882  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   883  					},
   884  				},
   885  				"/v1, Kind=Secret, ns1/m1": {
   886  					owners: []string{
   887  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   888  					},
   889  				},
   890  			},
   891  		},
   892  	},
   893  	{
   894  		name: "Cluster with Control Plane",
   895  		args: objectGraphTestArgs{
   896  			objs: test.NewFakeCluster("ns1", "cluster1").
   897  				WithControlPlane(
   898  					test.NewFakeControlPlane("cp1").
   899  						WithMachines(
   900  							test.NewFakeMachine("m1"),
   901  						),
   902  				).Objs(),
   903  		},
   904  		want: wantGraph{
   905  			nodes: map[string]wantGraphItem{
   906  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   907  					forceMove:          true,
   908  					forceMoveHierarchy: true,
   909  				},
   910  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   911  					owners: []string{
   912  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   913  					},
   914  				},
   915  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   916  					softOwners: []string{
   917  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   918  					},
   919  				},
   920  
   921  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlane, ns1/cp1": {
   922  					owners: []string{
   923  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   924  					},
   925  				},
   926  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/cp1": {
   927  					owners: []string{
   928  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   929  					},
   930  				},
   931  				"/v1, Kind=Secret, ns1/cluster1-sa": {
   932  					owners: []string{
   933  						"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlane, ns1/cp1",
   934  					},
   935  				},
   936  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   937  					owners: []string{
   938  						"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlane, ns1/cp1",
   939  					},
   940  				},
   941  
   942  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1": {
   943  					owners: []string{
   944  						"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlane, ns1/cp1",
   945  					},
   946  				},
   947  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/m1": {
   948  					owners: []string{
   949  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   950  					},
   951  				},
   952  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1": {
   953  					owners: []string{
   954  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/m1",
   955  					},
   956  				},
   957  				"/v1, Kind=Secret, ns1/m1": {
   958  					owners: []string{
   959  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/m1",
   960  					},
   961  				},
   962  			},
   963  		},
   964  	},
   965  	{
   966  		name: "Cluster with MachinePool",
   967  		args: objectGraphTestArgs{
   968  			objs: test.NewFakeCluster("ns1", "cluster1").
   969  				WithMachinePools(
   970  					test.NewFakeMachinePool("mp1"),
   971  				).Objs(),
   972  		},
   973  		want: wantGraph{
   974  			nodes: map[string]wantGraphItem{
   975  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
   976  					forceMove:          true,
   977  					forceMoveHierarchy: true,
   978  				},
   979  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
   980  					owners: []string{
   981  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   982  					},
   983  				},
   984  				"/v1, Kind=Secret, ns1/cluster1-ca": {
   985  					softOwners: []string{
   986  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
   987  					},
   988  				},
   989  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
   990  					owners: []string{
   991  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   992  					},
   993  				},
   994  
   995  				"cluster.x-k8s.io/v1beta1, Kind=MachinePool, ns1/mp1": {
   996  					owners: []string{
   997  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
   998  					},
   999  				},
  1000  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/mp1": {
  1001  					owners: []string{
  1002  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1003  					},
  1004  				},
  1005  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/mp1": {
  1006  					owners: []string{
  1007  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1008  					},
  1009  				},
  1010  			},
  1011  		},
  1012  	},
  1013  	{
  1014  		name: "Two clusters with shared objects",
  1015  		args: objectGraphTestArgs{
  1016  			objs: func() []client.Object {
  1017  				sharedInfrastructureTemplate := test.NewFakeInfrastructureTemplate("shared")
  1018  
  1019  				objs := []client.Object{
  1020  					sharedInfrastructureTemplate,
  1021  				}
  1022  
  1023  				objs = append(objs, test.NewFakeCluster("ns1", "cluster1").
  1024  					WithMachineSets(
  1025  						test.NewFakeMachineSet("cluster1-ms1").
  1026  							WithInfrastructureTemplate(sharedInfrastructureTemplate).
  1027  							WithMachines(
  1028  								test.NewFakeMachine("cluster1-m1"),
  1029  							),
  1030  					).Objs()...)
  1031  
  1032  				objs = append(objs, test.NewFakeCluster("ns1", "cluster2").
  1033  					WithMachineSets(
  1034  						test.NewFakeMachineSet("cluster2-ms1").
  1035  							WithInfrastructureTemplate(sharedInfrastructureTemplate).
  1036  							WithMachines(
  1037  								test.NewFakeMachine("cluster2-m1"),
  1038  							),
  1039  					).Objs()...)
  1040  
  1041  				return objs
  1042  			}(),
  1043  		},
  1044  		want: wantGraph{
  1045  			nodes: map[string]wantGraphItem{
  1046  
  1047  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/shared": {
  1048  					owners: []string{
  1049  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1050  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1051  					},
  1052  				},
  1053  
  1054  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1055  					forceMove:          true,
  1056  					forceMoveHierarchy: true,
  1057  				},
  1058  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1059  					owners: []string{
  1060  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1061  					},
  1062  				},
  1063  				"/v1, Kind=Secret, ns1/cluster1-ca": {
  1064  					softOwners: []string{
  1065  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1066  					},
  1067  				},
  1068  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1069  					owners: []string{
  1070  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1071  					},
  1072  				},
  1073  
  1074  				"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster1-ms1": {
  1075  					owners: []string{
  1076  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1077  					},
  1078  				},
  1079  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/cluster1-ms1": {
  1080  					owners: []string{
  1081  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1082  					},
  1083  				},
  1084  
  1085  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster1-m1": {
  1086  					owners: []string{
  1087  						"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster1-ms1",
  1088  					},
  1089  				},
  1090  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/cluster1-m1": {
  1091  					owners: []string{
  1092  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster1-m1",
  1093  					},
  1094  				},
  1095  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster1-m1": {
  1096  					owners: []string{
  1097  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster1-m1",
  1098  					},
  1099  				},
  1100  				"/v1, Kind=Secret, ns1/cluster1-m1": {
  1101  					owners: []string{
  1102  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster1-m1",
  1103  					},
  1104  				},
  1105  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2": {
  1106  					forceMove:          true,
  1107  					forceMoveHierarchy: true,
  1108  				},
  1109  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster2": {
  1110  					owners: []string{
  1111  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1112  					},
  1113  				},
  1114  				"/v1, Kind=Secret, ns1/cluster2-ca": {
  1115  					softOwners: []string{
  1116  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2", // NB. this secret is not linked to the cluster through owner ref
  1117  					},
  1118  				},
  1119  				"/v1, Kind=Secret, ns1/cluster2-kubeconfig": {
  1120  					owners: []string{
  1121  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1122  					},
  1123  				},
  1124  
  1125  				"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster2-ms1": {
  1126  					owners: []string{
  1127  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1128  					},
  1129  				},
  1130  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/cluster2-ms1": {
  1131  					owners: []string{
  1132  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1133  					},
  1134  				},
  1135  
  1136  				"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster2-m1": {
  1137  					owners: []string{
  1138  						"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster2-ms1",
  1139  					},
  1140  				},
  1141  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/cluster2-m1": {
  1142  					owners: []string{
  1143  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster2-m1",
  1144  					},
  1145  				},
  1146  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster2-m1": {
  1147  					owners: []string{
  1148  						"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster2-m1",
  1149  					},
  1150  				},
  1151  				"/v1, Kind=Secret, ns1/cluster2-m1": {
  1152  					owners: []string{
  1153  						"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster2-m1",
  1154  					},
  1155  				},
  1156  			},
  1157  		},
  1158  	},
  1159  	{
  1160  		name: "A ClusterResourceSet applied to a cluster",
  1161  		args: objectGraphTestArgs{
  1162  			objs: func() []client.Object {
  1163  				objs := []client.Object{}
  1164  				objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  1165  
  1166  				objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  1167  					WithSecret("resource-s1").
  1168  					WithConfigMap("resource-c1").
  1169  					ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  1170  					Objs()...)
  1171  
  1172  				return objs
  1173  			}(),
  1174  		},
  1175  		want: wantGraph{
  1176  			nodes: map[string]wantGraphItem{
  1177  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1178  					forceMove:          true,
  1179  					forceMoveHierarchy: true,
  1180  				},
  1181  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1182  					owners: []string{
  1183  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1184  					},
  1185  				},
  1186  				"/v1, Kind=Secret, ns1/cluster1-ca": {
  1187  					softOwners: []string{
  1188  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1189  					},
  1190  				},
  1191  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1192  					owners: []string{
  1193  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1194  					},
  1195  				},
  1196  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1": {
  1197  					forceMove:          true,
  1198  					forceMoveHierarchy: true,
  1199  				},
  1200  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1": {
  1201  					owners: []string{
  1202  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1203  					},
  1204  					softOwners: []string{
  1205  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1206  					},
  1207  				},
  1208  				"/v1, Kind=Secret, ns1/resource-s1": {
  1209  					owners: []string{
  1210  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1211  					},
  1212  				},
  1213  				"/v1, Kind=ConfigMap, ns1/resource-c1": {
  1214  					owners: []string{
  1215  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1216  					},
  1217  				},
  1218  			},
  1219  		},
  1220  	},
  1221  	{
  1222  		name: "A ClusterResourceSet applied to two clusters",
  1223  		args: objectGraphTestArgs{
  1224  			objs: func() []client.Object {
  1225  				objs := []client.Object{}
  1226  				objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  1227  				objs = append(objs, test.NewFakeCluster("ns1", "cluster2").Objs()...)
  1228  
  1229  				objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  1230  					WithSecret("resource-s1").
  1231  					WithConfigMap("resource-c1").
  1232  					ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  1233  					ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster2")).
  1234  					Objs()...)
  1235  
  1236  				return objs
  1237  			}(),
  1238  		},
  1239  		want: wantGraph{
  1240  			nodes: map[string]wantGraphItem{
  1241  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1242  					forceMove:          true,
  1243  					forceMoveHierarchy: true,
  1244  				},
  1245  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1246  					owners: []string{
  1247  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1248  					},
  1249  				},
  1250  				"/v1, Kind=Secret, ns1/cluster1-ca": {
  1251  					softOwners: []string{
  1252  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1253  					},
  1254  				},
  1255  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1256  					owners: []string{
  1257  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1258  					},
  1259  				},
  1260  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2": {
  1261  					forceMove:          true,
  1262  					forceMoveHierarchy: true,
  1263  				},
  1264  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster2": {
  1265  					owners: []string{
  1266  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1267  					},
  1268  				},
  1269  				"/v1, Kind=Secret, ns1/cluster2-ca": {
  1270  					softOwners: []string{
  1271  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2", // NB. this secret is not linked to the cluster through owner ref
  1272  					},
  1273  				},
  1274  				"/v1, Kind=Secret, ns1/cluster2-kubeconfig": {
  1275  					owners: []string{
  1276  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1277  					},
  1278  				},
  1279  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1": {
  1280  					forceMove:          true,
  1281  					forceMoveHierarchy: true,
  1282  				},
  1283  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1": {
  1284  					owners: []string{
  1285  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1286  					},
  1287  					softOwners: []string{
  1288  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1289  					},
  1290  				},
  1291  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster2": {
  1292  					owners: []string{
  1293  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1294  					},
  1295  					softOwners: []string{
  1296  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",
  1297  					},
  1298  				},
  1299  				"/v1, Kind=Secret, ns1/resource-s1": {
  1300  					owners: []string{
  1301  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1302  					},
  1303  				},
  1304  				"/v1, Kind=ConfigMap, ns1/resource-c1": {
  1305  					owners: []string{
  1306  						"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  1307  					},
  1308  				},
  1309  			},
  1310  		},
  1311  	},
  1312  	{
  1313  		// NOTE: External objects are CRD types installed by clusterctl, but not directly related with the CAPI hierarchy of objects. e.g. IPAM claims.
  1314  		name: "Namespaced External Objects with force move label",
  1315  		args: objectGraphTestArgs{
  1316  			objs: test.NewFakeExternalObject("ns1", "externalObject1").Objs(),
  1317  		},
  1318  		want: wantGraph{
  1319  			nodes: map[string]wantGraphItem{
  1320  				"external.cluster.x-k8s.io/v1beta1, Kind=GenericExternalObject, ns1/externalObject1": {
  1321  					forceMove: true,
  1322  				},
  1323  			},
  1324  		},
  1325  	},
  1326  	{
  1327  		// NOTE: External objects are CRD types installed by clusterctl, but not directly related with the CAPI hierarchy of objects. e.g. IPAM claims.
  1328  		name: "Global External Objects with force move label",
  1329  		args: objectGraphTestArgs{
  1330  			objs: test.NewFakeClusterExternalObject("externalObject1").Objs(),
  1331  		},
  1332  		want: wantGraph{
  1333  			nodes: map[string]wantGraphItem{
  1334  				"external.cluster.x-k8s.io/v1beta1, Kind=GenericClusterExternalObject, externalObject1": {
  1335  					forceMove: true,
  1336  					isGlobal:  true,
  1337  				},
  1338  			},
  1339  		},
  1340  	},
  1341  	{
  1342  		// NOTE: Infrastructure providers global credentials are going to be stored in Secrets in the provider's namespaces.
  1343  		name: "Secrets from provider's namespace",
  1344  		args: objectGraphTestArgs{
  1345  			objs: []client.Object{
  1346  				test.NewSecret("infra-system", "credentials"),
  1347  			},
  1348  		},
  1349  		want: wantGraph{
  1350  			nodes: map[string]wantGraphItem{
  1351  				"/v1, Kind=Secret, infra-system/credentials": {},
  1352  			},
  1353  		},
  1354  	},
  1355  	{
  1356  		name: "Cluster owning a secret with infrastructure credentials",
  1357  		args: objectGraphTestArgs{
  1358  			objs: test.NewFakeCluster("ns1", "cluster1").
  1359  				WithCredentialSecret().Objs(),
  1360  		},
  1361  		want: wantGraph{
  1362  			nodes: map[string]wantGraphItem{
  1363  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1364  					forceMove:          true,
  1365  					forceMoveHierarchy: true,
  1366  				},
  1367  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1368  					owners: []string{
  1369  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1370  					},
  1371  				},
  1372  				"/v1, Kind=Secret, ns1/cluster1-ca": {
  1373  					softOwners: []string{
  1374  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1375  					},
  1376  				},
  1377  				"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1378  					owners: []string{
  1379  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1380  					},
  1381  				},
  1382  				"/v1, Kind=Secret, ns1/cluster1-credentials": {
  1383  					owners: []string{
  1384  						"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1385  					},
  1386  				},
  1387  			},
  1388  		},
  1389  	},
  1390  	{
  1391  		name: "A global identity for an infrastructure provider owning a Secret with credentials in the provider's namespace",
  1392  		args: objectGraphTestArgs{
  1393  			objs: test.NewFakeClusterInfrastructureIdentity("infra1-identity").
  1394  				WithSecretIn("infra1-system"). // a secret in infra1-system namespace, where an infrastructure provider is installed
  1395  				Objs(),
  1396  		},
  1397  		want: wantGraph{
  1398  			nodes: map[string]wantGraphItem{
  1399  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericClusterInfrastructureIdentity, infra1-identity": {
  1400  					isGlobal:           true,
  1401  					forceMove:          true,
  1402  					forceMoveHierarchy: true,
  1403  				},
  1404  				"/v1, Kind=Secret, infra1-system/infra1-identity-credentials": {
  1405  					owners: []string{
  1406  						"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericClusterInfrastructureIdentity, infra1-identity",
  1407  					},
  1408  				},
  1409  			},
  1410  		},
  1411  	},
  1412  	{
  1413  		name: "ClusterClass",
  1414  		args: objectGraphTestArgs{
  1415  			objs: test.NewFakeClusterClass("ns1", "clusterclass1").Objs(),
  1416  		},
  1417  		want: wantGraph{
  1418  			nodes: map[string]wantGraphItem{
  1419  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1420  					forceMove:          true,
  1421  					forceMoveHierarchy: true,
  1422  				},
  1423  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1424  					owners: []string{
  1425  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1426  					},
  1427  				},
  1428  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass1": {
  1429  					owners: []string{
  1430  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1431  					},
  1432  				},
  1433  			},
  1434  		},
  1435  	},
  1436  	{
  1437  		name: "ClusterClass with control plane infrastructure template",
  1438  		args: objectGraphTestArgs{
  1439  			objs: func() []client.Object {
  1440  				infrastructureMachineTemplate := builder.InfrastructureMachineTemplate("ns1", "inframachinetemplate1").Build()
  1441  				return test.NewFakeClusterClass("ns1", "clusterclass1").
  1442  					WithControlPlaneInfrastructureTemplate(infrastructureMachineTemplate).
  1443  					Objs()
  1444  			}(),
  1445  		},
  1446  		want: wantGraph{
  1447  			nodes: map[string]wantGraphItem{
  1448  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1449  					forceMove:          true,
  1450  					forceMoveHierarchy: true,
  1451  				},
  1452  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1453  					owners: []string{
  1454  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1455  					},
  1456  				},
  1457  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass1": {
  1458  					owners: []string{
  1459  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1460  					},
  1461  				},
  1462  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/inframachinetemplate1": {
  1463  					owners: []string{
  1464  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1465  					},
  1466  				},
  1467  			},
  1468  		},
  1469  	},
  1470  	{
  1471  		name: "ClusterClass with worker machine deployment class",
  1472  		args: objectGraphTestArgs{
  1473  			objs: func() []client.Object {
  1474  				mdClass := test.NewFakeMachineDeploymentClass("ns1", "mdclass1")
  1475  				return test.NewFakeClusterClass("ns1", "clusterclass1").
  1476  					WithWorkerMachineDeploymentClasses([]*test.FakeMachineDeploymentClass{mdClass}).
  1477  					Objs()
  1478  			}(),
  1479  		},
  1480  		want: wantGraph{
  1481  			nodes: map[string]wantGraphItem{
  1482  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1483  					forceMove:          true,
  1484  					forceMoveHierarchy: true,
  1485  				},
  1486  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1487  					owners: []string{
  1488  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1489  					},
  1490  				},
  1491  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass1": {
  1492  					owners: []string{
  1493  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1494  					},
  1495  				},
  1496  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/mdclass1": {
  1497  					owners: []string{
  1498  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1499  					},
  1500  				},
  1501  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/mdclass1": {
  1502  					owners: []string{
  1503  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1504  					},
  1505  				},
  1506  			},
  1507  		},
  1508  	},
  1509  	{
  1510  		name: "ClusterClass with 2 worker machine deployment classes with shared templates",
  1511  		args: objectGraphTestArgs{
  1512  			objs: func() []client.Object {
  1513  				infraMachineTemplate := builder.InfrastructureMachineTemplate("ns1", "infamachinetemplate1").Build()
  1514  				bootstrapTemplate := builder.BootstrapTemplate("ns1", "bootstraptemplate1").Build()
  1515  				mdClass1 := test.NewFakeMachineDeploymentClass("ns1", "mdclass1").
  1516  					WithInfrastructureMachineTemplate(infraMachineTemplate).
  1517  					WithBootstrapTemplate(bootstrapTemplate)
  1518  				mdClass2 := test.NewFakeMachineDeploymentClass("ns1", "mdclass2").
  1519  					WithInfrastructureMachineTemplate(infraMachineTemplate).
  1520  					WithBootstrapTemplate(bootstrapTemplate)
  1521  				return test.NewFakeClusterClass("ns1", "clusterclass1").
  1522  					WithWorkerMachineDeploymentClasses([]*test.FakeMachineDeploymentClass{mdClass1, mdClass2}).
  1523  					Objs()
  1524  			}(),
  1525  		},
  1526  		want: wantGraph{
  1527  			nodes: map[string]wantGraphItem{
  1528  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1529  					forceMove:          true,
  1530  					forceMoveHierarchy: true,
  1531  				},
  1532  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1533  					owners: []string{
  1534  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1535  					},
  1536  				},
  1537  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass1": {
  1538  					owners: []string{
  1539  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1540  					},
  1541  				},
  1542  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/infamachinetemplate1": {
  1543  					owners: []string{
  1544  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1545  					},
  1546  				},
  1547  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/bootstraptemplate1": {
  1548  					owners: []string{
  1549  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1550  					},
  1551  				},
  1552  			},
  1553  		},
  1554  	},
  1555  	{
  1556  		name: "Two ClusterClasses",
  1557  		args: objectGraphTestArgs{
  1558  			objs: func() []client.Object {
  1559  				objs := []client.Object{}
  1560  				objs = append(objs, test.NewFakeClusterClass("ns1", "clusterclass1").Objs()...)
  1561  				objs = append(objs, test.NewFakeClusterClass("ns1", "clusterclass2").Objs()...)
  1562  				return objs
  1563  			}(),
  1564  		},
  1565  		want: wantGraph{
  1566  			nodes: map[string]wantGraphItem{
  1567  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1568  					forceMove:          true,
  1569  					forceMoveHierarchy: true,
  1570  				},
  1571  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1572  					owners: []string{
  1573  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1574  					},
  1575  				},
  1576  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass1": {
  1577  					owners: []string{
  1578  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1579  					},
  1580  				},
  1581  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2": {
  1582  					forceMove:          true,
  1583  					forceMoveHierarchy: true,
  1584  				},
  1585  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass2": {
  1586  					owners: []string{
  1587  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1588  					},
  1589  				},
  1590  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/clusterclass2": {
  1591  					owners: []string{
  1592  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1593  					},
  1594  				},
  1595  			},
  1596  		},
  1597  	},
  1598  	{
  1599  		name: "Two ClusterClasses with shared templates",
  1600  		args: objectGraphTestArgs{
  1601  			objs: func() []client.Object {
  1602  				objs := []client.Object{}
  1603  
  1604  				infraMachineTemplate1 := builder.InfrastructureMachineTemplate("ns1", "infamachinetemplate1").Build()
  1605  				bootstrapTemplate1 := builder.BootstrapTemplate("ns1", "bootstraptemplate1").Build()
  1606  
  1607  				infraMachineTemplate2 := builder.InfrastructureMachineTemplate("ns1", "infamachinetemplate2").Build()
  1608  				bootstrapTemplate2 := builder.BootstrapTemplate("ns1", "bootstraptemplate2").Build()
  1609  
  1610  				controlPlaneTemplate := builder.ControlPlaneTemplate("ns1", "controlplanetemplate1").Build()
  1611  
  1612  				// mdClass1 and mdClass2 share templates.
  1613  				// mdClass3 does not share templates with any.
  1614  				mdClass1 := test.NewFakeMachineDeploymentClass("ns1", "mdclass1").
  1615  					WithInfrastructureMachineTemplate(infraMachineTemplate1).
  1616  					WithBootstrapTemplate(bootstrapTemplate1)
  1617  				mdClass2 := test.NewFakeMachineDeploymentClass("ns1", "mdclass2").
  1618  					WithInfrastructureMachineTemplate(infraMachineTemplate1).
  1619  					WithBootstrapTemplate(bootstrapTemplate1)
  1620  				mdClass3 := test.NewFakeMachineDeploymentClass("ns1", "mdclass2").
  1621  					WithInfrastructureMachineTemplate(infraMachineTemplate2).
  1622  					WithBootstrapTemplate(bootstrapTemplate2)
  1623  
  1624  				// clusterclass1 and clusterclass2 share the same control plane template but have different
  1625  				// infrastructure cluster templates.
  1626  				// clusterclass1 and clusterclass2 share the templates defined in mdclass1.
  1627  				// clusterclass1 and clusterclass2 do not share the templates in mdclass3.
  1628  				objs = append(objs, test.NewFakeClusterClass("ns1", "clusterclass1").
  1629  					WithControlPlaneTemplate(controlPlaneTemplate).
  1630  					WithWorkerMachineDeploymentClasses([]*test.FakeMachineDeploymentClass{mdClass1, mdClass2}).
  1631  					Objs()...)
  1632  
  1633  				objs = append(objs, test.NewFakeClusterClass("ns1", "clusterclass2").
  1634  					WithControlPlaneTemplate(controlPlaneTemplate).
  1635  					WithWorkerMachineDeploymentClasses([]*test.FakeMachineDeploymentClass{mdClass1, mdClass3}).
  1636  					Objs()...)
  1637  
  1638  				// We need to deduplicate objects here as the clusterclasses share objects and
  1639  				// setting up the test server panics if we try to create it with duplicate objects.
  1640  				return deduplicateObjects(objs)
  1641  
  1642  			}(),
  1643  		},
  1644  		want: wantGraph{
  1645  			nodes: map[string]wantGraphItem{
  1646  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1": {
  1647  					forceMove:          true,
  1648  					forceMoveHierarchy: true,
  1649  				},
  1650  				"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2": {
  1651  					forceMove:          true,
  1652  					forceMoveHierarchy: true,
  1653  				},
  1654  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass1": {
  1655  					owners: []string{
  1656  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1657  					},
  1658  				},
  1659  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/clusterclass2": {
  1660  					owners: []string{
  1661  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1662  					},
  1663  				},
  1664  				"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/controlplanetemplate1": {
  1665  					owners: []string{
  1666  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1667  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1668  					},
  1669  				},
  1670  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/infamachinetemplate1": {
  1671  					owners: []string{
  1672  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1673  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1674  					},
  1675  				},
  1676  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/bootstraptemplate1": {
  1677  					owners: []string{
  1678  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass1",
  1679  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1680  					},
  1681  				},
  1682  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/infamachinetemplate2": {
  1683  					owners: []string{
  1684  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1685  					},
  1686  				},
  1687  				"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/bootstraptemplate2": {
  1688  					owners: []string{
  1689  						"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/clusterclass2",
  1690  					},
  1691  				},
  1692  			},
  1693  		},
  1694  	},
  1695  }
  1696  
  1697  func getDetachedObjectGraphWihObjs(objs []client.Object) (*objectGraph, error) {
  1698  	graph := newObjectGraph(nil, nil) // detached from any cluster
  1699  	for _, o := range objs {
  1700  		u := &unstructured.Unstructured{}
  1701  		if err := test.FakeScheme.Convert(o, u, nil); err != nil {
  1702  			return nil, errors.Wrap(err, "failed to convert object in unstructured")
  1703  		}
  1704  		if err := graph.addObj(u); err != nil {
  1705  			return nil, err
  1706  		}
  1707  	}
  1708  
  1709  	// given that we are not relying on discovery while testing in "detached mode (without a fake client)" it is required to:
  1710  	for _, node := range graph.getNodes() {
  1711  		// enforce forceMoveHierarchy for Clusters, ClusterResourceSets, GenericClusterInfrastructureIdentity
  1712  		if node.identity.Kind == "Cluster" || node.identity.Kind == "ClusterClass" || node.identity.Kind == "ClusterResourceSet" || node.identity.Kind == "GenericClusterInfrastructureIdentity" {
  1713  			node.forceMove = true
  1714  			node.forceMoveHierarchy = true
  1715  		}
  1716  		// enforce forceMove for GenericExternalObject, GenericClusterExternalObject
  1717  		if node.identity.Kind == "GenericExternalObject" || node.identity.Kind == "GenericClusterExternalObject" {
  1718  			node.forceMove = true
  1719  		}
  1720  		// enforce isGlobal for GenericClusterInfrastructureIdentity and GenericClusterExternalObject
  1721  		if node.identity.Kind == "GenericClusterInfrastructureIdentity" || node.identity.Kind == "GenericClusterExternalObject" {
  1722  			node.isGlobal = true
  1723  		}
  1724  	}
  1725  
  1726  	return graph, nil
  1727  }
  1728  
  1729  func TestObjectGraph_addObj_WithFakeObjects(t *testing.T) {
  1730  	// NB. we are testing the graph is properly built starting from objects (this test) or from the same objects read from the cluster (TestGraphBuilder_Discovery)
  1731  	for _, tt := range objectGraphsTests {
  1732  		t.Run(tt.name, func(t *testing.T) {
  1733  			g := NewWithT(t)
  1734  
  1735  			graph, err := getDetachedObjectGraphWihObjs(tt.args.objs)
  1736  			g.Expect(err).ToNot(HaveOccurred())
  1737  
  1738  			// call setSoftOwnership so there is functional parity with discovery
  1739  			graph.setSoftOwnership()
  1740  
  1741  			assertGraph(t, graph, tt.want)
  1742  		})
  1743  	}
  1744  }
  1745  
  1746  func getObjectGraphWithObjs(objs []client.Object) *objectGraph {
  1747  	fromProxy := getFakeProxyWithCRDs()
  1748  
  1749  	for _, o := range objs {
  1750  		fromProxy.WithObjs(o)
  1751  	}
  1752  
  1753  	fromProxy.WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.2.3", "infra1-system")
  1754  	inventory := newInventoryClient(fromProxy, fakePollImmediateWaiter)
  1755  
  1756  	return newObjectGraph(fromProxy, inventory)
  1757  }
  1758  
  1759  func getObjectGraph() *objectGraph {
  1760  	// build object graph from file
  1761  	fromProxy := getFakeProxyWithCRDs()
  1762  
  1763  	fromProxy.WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.2.3", "infra1-system")
  1764  	inventory := newInventoryClient(fromProxy, fakePollImmediateWaiter)
  1765  
  1766  	return newObjectGraph(fromProxy, inventory)
  1767  }
  1768  
  1769  func getFakeProxyWithCRDs() *test.FakeProxy {
  1770  	proxy := test.NewFakeProxy()
  1771  	for _, o := range test.FakeCRDList() {
  1772  		proxy.WithObjs(o)
  1773  	}
  1774  	return proxy
  1775  }
  1776  
  1777  func TestObjectGraph_Discovery(t *testing.T) {
  1778  	// NB. we are testing the graph is properly built starting from objects (TestGraphBuilder_addObj_WithFakeObjects) or from the same objects read from the cluster (this test).
  1779  	for _, tt := range objectGraphsTests {
  1780  		t.Run(tt.name, func(t *testing.T) {
  1781  			g := NewWithT(t)
  1782  
  1783  			ctx := context.Background()
  1784  
  1785  			// Create an objectGraph bound to a source cluster with all the CRDs for the types involved in the test.
  1786  			graph := getObjectGraphWithObjs(tt.args.objs)
  1787  
  1788  			// Get all the types to be considered for discovery
  1789  			err := graph.getDiscoveryTypes(ctx)
  1790  			g.Expect(err).ToNot(HaveOccurred())
  1791  
  1792  			// finally test discovery
  1793  			err = graph.Discovery(ctx, "")
  1794  			if tt.wantErr {
  1795  				g.Expect(err).To(HaveOccurred())
  1796  				return
  1797  			}
  1798  
  1799  			g.Expect(err).ToNot(HaveOccurred())
  1800  			assertGraph(t, graph, tt.want)
  1801  		})
  1802  	}
  1803  }
  1804  
  1805  func TestObjectGraph_DiscoveryByNamespace(t *testing.T) {
  1806  	type args struct {
  1807  		namespace string
  1808  		objs      []client.Object
  1809  	}
  1810  	var tests = []struct {
  1811  		name    string
  1812  		args    args
  1813  		want    wantGraph
  1814  		wantErr bool
  1815  	}{
  1816  		{
  1817  			name: "two clusters, in different namespaces, read both",
  1818  			args: args{
  1819  				namespace: "", // read all the namespaces
  1820  				objs: func() []client.Object {
  1821  					objs := []client.Object{}
  1822  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  1823  					objs = append(objs, test.NewFakeCluster("ns2", "cluster1").Objs()...)
  1824  					return objs
  1825  				}(),
  1826  			},
  1827  			want: wantGraph{
  1828  				nodes: map[string]wantGraphItem{
  1829  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1830  						forceMove:          true,
  1831  						forceMoveHierarchy: true,
  1832  					},
  1833  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1834  						owners: []string{
  1835  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1836  						},
  1837  					},
  1838  					"/v1, Kind=Secret, ns1/cluster1-ca": {
  1839  						softOwners: []string{
  1840  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1841  						},
  1842  					},
  1843  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1844  						owners: []string{
  1845  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1846  						},
  1847  					},
  1848  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1": {
  1849  						forceMove:          true,
  1850  						forceMoveHierarchy: true,
  1851  					},
  1852  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns2/cluster1": {
  1853  						owners: []string{
  1854  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1",
  1855  						},
  1856  					},
  1857  					"/v1, Kind=Secret, ns2/cluster1-ca": {
  1858  						softOwners: []string{
  1859  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1860  						},
  1861  					},
  1862  					"/v1, Kind=Secret, ns2/cluster1-kubeconfig": {
  1863  						owners: []string{
  1864  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns2/cluster1",
  1865  						},
  1866  					},
  1867  				},
  1868  			},
  1869  		},
  1870  		{
  1871  			name: "two clusters, in different namespaces, read only 1",
  1872  			args: args{
  1873  				namespace: "ns1", // read only from ns1
  1874  				objs: func() []client.Object {
  1875  					objs := []client.Object{}
  1876  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  1877  					objs = append(objs, test.NewFakeCluster("ns2", "cluster1").Objs()...)
  1878  					return objs
  1879  				}(),
  1880  			},
  1881  			want: wantGraph{
  1882  				nodes: map[string]wantGraphItem{
  1883  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1884  						forceMove:          true,
  1885  						forceMoveHierarchy: true,
  1886  					},
  1887  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1888  						owners: []string{
  1889  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1890  						},
  1891  					},
  1892  					"/v1, Kind=Secret, ns1/cluster1-ca": {
  1893  						softOwners: []string{
  1894  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref
  1895  						},
  1896  					},
  1897  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  1898  						owners: []string{
  1899  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1900  						},
  1901  					},
  1902  				},
  1903  			},
  1904  		},
  1905  		{
  1906  			// NOTE: External objects are CRD types installed by clusterctl, but not directly related with the CAPI hierarchy of objects. e.g. IPAM claims.
  1907  			name: "Namespaced External Objects with force move label",
  1908  			args: args{
  1909  				namespace: "ns1",                                                       // read only from ns1
  1910  				objs:      test.NewFakeExternalObject("ns1", "externalObject1").Objs(), // Fake external object with
  1911  			},
  1912  			want: wantGraph{
  1913  				nodes: map[string]wantGraphItem{
  1914  					"external.cluster.x-k8s.io/v1beta1, Kind=GenericExternalObject, ns1/externalObject1": {
  1915  						forceMove: true,
  1916  					},
  1917  				},
  1918  			},
  1919  		},
  1920  		{
  1921  			// NOTE: Infrastructure providers global credentials are going to be stored in Secrets in the provider's namespaces.
  1922  			name: "Secrets from provider's namespace (e.g. credentials) should always be read",
  1923  			args: args{
  1924  				namespace: "ns1", // read only from ns1
  1925  				objs: []client.Object{
  1926  					test.NewSecret("infra1-system", "infra1-credentials"), // a secret in infra1-system namespace, where an infrastructure provider is installed
  1927  				},
  1928  			},
  1929  			want: wantGraph{
  1930  				nodes: map[string]wantGraphItem{
  1931  					"/v1, Kind=Secret, infra1-system/infra1-credentials": {},
  1932  				},
  1933  			},
  1934  		},
  1935  	}
  1936  
  1937  	for _, tt := range tests {
  1938  		t.Run(tt.name, func(t *testing.T) {
  1939  			g := NewWithT(t)
  1940  
  1941  			ctx := context.Background()
  1942  
  1943  			// Create an objectGraph bound to a source cluster with all the CRDs for the types involved in the test.
  1944  			graph := getObjectGraphWithObjs(tt.args.objs)
  1945  
  1946  			// Get all the types to be considered for discovery
  1947  			err := graph.getDiscoveryTypes(ctx)
  1948  			g.Expect(err).ToNot(HaveOccurred())
  1949  
  1950  			// finally test discovery
  1951  			err = graph.Discovery(ctx, tt.args.namespace)
  1952  			if tt.wantErr {
  1953  				g.Expect(err).To(HaveOccurred())
  1954  				return
  1955  			}
  1956  
  1957  			g.Expect(err).ToNot(HaveOccurred())
  1958  			assertGraph(t, graph, tt.want)
  1959  		})
  1960  	}
  1961  }
  1962  
  1963  func Test_objectGraph_setSoftOwnership(t *testing.T) {
  1964  	type fields struct {
  1965  		objs []client.Object
  1966  	}
  1967  	tests := []struct {
  1968  		name   string
  1969  		fields fields
  1970  		want   wantGraph
  1971  	}{
  1972  		{
  1973  			name: "A cluster with a soft owned secret",
  1974  			fields: fields{
  1975  				objs: test.NewFakeCluster("ns1", "cluster1").Objs(),
  1976  			},
  1977  			want: wantGraph{
  1978  				nodes: map[string]wantGraphItem{
  1979  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  1980  						forceMove:          true,
  1981  						forceMoveHierarchy: true,
  1982  					},
  1983  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  1984  						owners: []string{
  1985  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1986  						},
  1987  					},
  1988  					"/v1, Kind=Secret, ns1/cluster1-ca": { // the ca secret has no explicit OwnerRef to the cluster, so it should be identified as a soft ownership
  1989  						softOwners: []string{
  1990  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1991  						},
  1992  					},
  1993  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig": { // the kubeconfig secret has explicit OwnerRef to the cluster, so it should NOT be identified as a soft ownership
  1994  						owners: []string{
  1995  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  1996  						},
  1997  					},
  1998  				},
  1999  			},
  2000  		},
  2001  		{
  2002  			name: "A ClusterClass with a soft owned Cluster",
  2003  			fields: fields{
  2004  				objs: func() []client.Object {
  2005  					objs := test.NewFakeClusterClass("ns1", "class1").Objs()
  2006  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").WithTopologyClass("class1").Objs()...)
  2007  
  2008  					return objs
  2009  				}(),
  2010  			},
  2011  			want: wantGraph{
  2012  				nodes: map[string]wantGraphItem{
  2013  					"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1": {
  2014  						forceMove:          true,
  2015  						forceMoveHierarchy: true,
  2016  					},
  2017  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureClusterTemplate, ns1/class1": {
  2018  						owners: []string{
  2019  							"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1",
  2020  						},
  2021  					},
  2022  					"controlplane.cluster.x-k8s.io/v1beta1, Kind=GenericControlPlaneTemplate, ns1/class1": {
  2023  						owners: []string{
  2024  							"cluster.x-k8s.io/v1beta1, Kind=ClusterClass, ns1/class1",
  2025  						},
  2026  					},
  2027  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  2028  						forceMove:          true,
  2029  						forceMoveHierarchy: true,
  2030  						softOwners: []string{
  2031  							"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
  2032  						},
  2033  					},
  2034  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  2035  						owners: []string{
  2036  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  2037  						},
  2038  					},
  2039  					"/v1, Kind=Secret, ns1/cluster1-ca": {
  2040  						softOwners: []string{
  2041  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref, but it is detected as soft ownership
  2042  						},
  2043  					},
  2044  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  2045  						owners: []string{
  2046  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  2047  						},
  2048  					},
  2049  				},
  2050  			},
  2051  		},
  2052  		{
  2053  			name: "A Cluster with a soft owned ClusterResourceSetBinding",
  2054  			fields: fields{
  2055  				objs: func() []client.Object {
  2056  					objs := test.NewFakeCluster("ns1", "cluster1").Objs()
  2057  					objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  2058  						WithSecret("resource-s1").
  2059  						WithConfigMap("resource-c1").
  2060  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  2061  						Objs()...)
  2062  
  2063  					return objs
  2064  				}(),
  2065  			},
  2066  			want: wantGraph{
  2067  				nodes: map[string]wantGraphItem{
  2068  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  2069  						forceMove:          true,
  2070  						forceMoveHierarchy: true,
  2071  					},
  2072  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1": {
  2073  						owners: []string{
  2074  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  2075  						},
  2076  					},
  2077  					"/v1, Kind=Secret, ns1/cluster1-ca": {
  2078  						softOwners: []string{
  2079  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this secret is not linked to the cluster through owner ref, but it is detected as soft ownership
  2080  						},
  2081  					},
  2082  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig": {
  2083  						owners: []string{
  2084  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",
  2085  						},
  2086  					},
  2087  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1": {
  2088  						forceMove:          true,
  2089  						forceMoveHierarchy: true,
  2090  					},
  2091  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1": {
  2092  						owners: []string{
  2093  							"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  2094  						},
  2095  						softOwners: []string{
  2096  							"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // NB. this ClusterResourceSetBinding is not linked to the cluster through owner ref, but it is detected as soft ownership
  2097  						},
  2098  					},
  2099  					"/v1, Kind=Secret, ns1/resource-s1": {
  2100  						owners: []string{
  2101  							"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  2102  						},
  2103  					},
  2104  					"/v1, Kind=ConfigMap, ns1/resource-c1": {
  2105  						owners: []string{
  2106  							"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",
  2107  						},
  2108  					},
  2109  				},
  2110  			},
  2111  		},
  2112  	}
  2113  	for _, tt := range tests {
  2114  		t.Run(tt.name, func(t *testing.T) {
  2115  			g := NewWithT(t)
  2116  
  2117  			graph, err := getDetachedObjectGraphWihObjs(tt.fields.objs)
  2118  			g.Expect(err).ToNot(HaveOccurred())
  2119  
  2120  			graph.setSoftOwnership()
  2121  
  2122  			assertGraph(t, graph, tt.want)
  2123  		})
  2124  	}
  2125  }
  2126  
  2127  func Test_objectGraph_setClusterTenants(t *testing.T) {
  2128  	type fields struct {
  2129  		objs []client.Object
  2130  	}
  2131  	tests := []struct {
  2132  		name         string
  2133  		fields       fields
  2134  		wantClusters map[string][]string
  2135  	}{
  2136  		{
  2137  			name: "One cluster",
  2138  			fields: fields{
  2139  				objs: test.NewFakeCluster("ns1", "foo").Objs(),
  2140  			},
  2141  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2142  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo": {
  2143  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo", // the cluster should be tenant of itself
  2144  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/foo",
  2145  					"/v1, Kind=Secret, ns1/foo-ca", // the ca secret is a soft owned
  2146  					"/v1, Kind=Secret, ns1/foo-kubeconfig",
  2147  				},
  2148  			},
  2149  		},
  2150  		{
  2151  			name: "Object not owned by a cluster should be ignored",
  2152  			fields: fields{
  2153  				objs: func() []client.Object {
  2154  					objs := []client.Object{}
  2155  					objs = append(objs, test.NewFakeCluster("ns1", "foo").Objs()...)
  2156  					objs = append(objs, test.NewFakeInfrastructureTemplate("orphan")) // orphan object, not owned by  any cluster
  2157  					return objs
  2158  				}(),
  2159  			},
  2160  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2161  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo": {
  2162  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo", // the cluster should be tenant of itself
  2163  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/foo",
  2164  					"/v1, Kind=Secret, ns1/foo-ca", // the ca secret is a soft owned
  2165  					"/v1, Kind=Secret, ns1/foo-kubeconfig",
  2166  				},
  2167  			},
  2168  		},
  2169  		{
  2170  			name: "Two clusters",
  2171  			fields: fields{
  2172  				objs: func() []client.Object {
  2173  					objs := []client.Object{}
  2174  					objs = append(objs, test.NewFakeCluster("ns1", "foo").Objs()...)
  2175  					objs = append(objs, test.NewFakeCluster("ns1", "bar").Objs()...)
  2176  					return objs
  2177  				}(),
  2178  			},
  2179  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2180  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo": {
  2181  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/foo", // the cluster should be tenant of itself
  2182  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/foo",
  2183  					"/v1, Kind=Secret, ns1/foo-ca", // the ca secret is a soft owned
  2184  					"/v1, Kind=Secret, ns1/foo-kubeconfig",
  2185  				},
  2186  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/bar": {
  2187  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/bar", // the cluster should be tenant of itself
  2188  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/bar",
  2189  					"/v1, Kind=Secret, ns1/bar-ca", // the ca secret is a soft owned
  2190  					"/v1, Kind=Secret, ns1/bar-kubeconfig",
  2191  				},
  2192  			},
  2193  		},
  2194  		{
  2195  			name: "Two clusters with a shared object",
  2196  			fields: fields{
  2197  				objs: func() []client.Object {
  2198  					sharedInfrastructureTemplate := test.NewFakeInfrastructureTemplate("shared")
  2199  
  2200  					objs := []client.Object{
  2201  						sharedInfrastructureTemplate,
  2202  					}
  2203  
  2204  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").
  2205  						WithMachineSets(
  2206  							test.NewFakeMachineSet("cluster1-ms1").
  2207  								WithInfrastructureTemplate(sharedInfrastructureTemplate).
  2208  								WithMachines(
  2209  									test.NewFakeMachine("cluster1-m1"),
  2210  								),
  2211  						).Objs()...)
  2212  
  2213  					objs = append(objs, test.NewFakeCluster("ns1", "cluster2").
  2214  						WithMachineSets(
  2215  							test.NewFakeMachineSet("cluster2-ms1").
  2216  								WithInfrastructureTemplate(sharedInfrastructureTemplate).
  2217  								WithMachines(
  2218  									test.NewFakeMachine("cluster2-m1"),
  2219  								),
  2220  						).Objs()...)
  2221  
  2222  					return objs
  2223  				}(),
  2224  			},
  2225  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2226  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  2227  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/shared", // the shared object should be in both lists
  2228  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1",                                           // the cluster should be tenant of itself
  2229  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1",
  2230  					"/v1, Kind=Secret, ns1/cluster1-ca", // the ca secret is a soft owned
  2231  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig",
  2232  					"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster1-ms1",
  2233  					"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/cluster1-ms1",
  2234  					"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster1-m1",
  2235  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/cluster1-m1",
  2236  					"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster1-m1",
  2237  					"/v1, Kind=Secret, ns1/cluster1-m1",
  2238  				},
  2239  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2": {
  2240  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachineTemplate, ns1/shared", // the shared object should be in both lists
  2241  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2",                                           // the cluster should be tenant of itself
  2242  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster2",
  2243  					"/v1, Kind=Secret, ns1/cluster2-ca", // the ca secret is a soft owned
  2244  					"/v1, Kind=Secret, ns1/cluster2-kubeconfig",
  2245  					"cluster.x-k8s.io/v1beta1, Kind=MachineSet, ns1/cluster2-ms1",
  2246  					"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfigTemplate, ns1/cluster2-ms1",
  2247  					"cluster.x-k8s.io/v1beta1, Kind=Machine, ns1/cluster2-m1",
  2248  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureMachine, ns1/cluster2-m1",
  2249  					"bootstrap.cluster.x-k8s.io/v1beta1, Kind=GenericBootstrapConfig, ns1/cluster2-m1",
  2250  					"/v1, Kind=Secret, ns1/cluster2-m1",
  2251  				},
  2252  			},
  2253  		},
  2254  		{
  2255  			name: "A ClusterResourceSet applied to a cluster",
  2256  			fields: fields{
  2257  				objs: func() []client.Object {
  2258  					objs := []client.Object{}
  2259  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  2260  
  2261  					objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  2262  						WithSecret("resource-s1").
  2263  						WithConfigMap("resource-c1").
  2264  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  2265  						Objs()...)
  2266  
  2267  					return objs
  2268  				}(),
  2269  			},
  2270  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2271  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  2272  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // the cluster should be tenant of itself
  2273  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1",
  2274  					"/v1, Kind=Secret, ns1/cluster1-ca", // the ca secret is a soft owned
  2275  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig",
  2276  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1", // ClusterResourceSetBinding are owned by the cluster
  2277  				},
  2278  			},
  2279  		},
  2280  		{
  2281  			name: "A ClusterResourceSet applied to two clusters",
  2282  			fields: fields{
  2283  				objs: func() []client.Object {
  2284  					objs := []client.Object{}
  2285  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  2286  					objs = append(objs, test.NewFakeCluster("ns1", "cluster2").Objs()...)
  2287  
  2288  					objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  2289  						WithSecret("resource-s1").
  2290  						WithConfigMap("resource-c1").
  2291  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  2292  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster2")).
  2293  						Objs()...)
  2294  
  2295  					return objs
  2296  				}(),
  2297  			},
  2298  			wantClusters: map[string][]string{ // wantClusters is a map[Cluster.UID] --> list of UIDs
  2299  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1": {
  2300  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster1", // the cluster should be tenant of itself
  2301  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster1",
  2302  					"/v1, Kind=Secret, ns1/cluster1-ca", // the ca secret is a soft owned
  2303  					"/v1, Kind=Secret, ns1/cluster1-kubeconfig",
  2304  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1", // ClusterResourceSetBinding are owned by the cluster
  2305  				},
  2306  				"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2": {
  2307  					"cluster.x-k8s.io/v1beta1, Kind=Cluster, ns1/cluster2", // the cluster should be tenant of itself
  2308  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericInfrastructureCluster, ns1/cluster2",
  2309  					"/v1, Kind=Secret, ns1/cluster2-ca", // the ca secret is a soft owned
  2310  					"/v1, Kind=Secret, ns1/cluster2-kubeconfig",
  2311  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster2", // ClusterResourceSetBinding are owned by the cluster
  2312  				},
  2313  			},
  2314  		},
  2315  	}
  2316  	for _, tt := range tests {
  2317  		t.Run(tt.name, func(t *testing.T) {
  2318  			g := NewWithT(t)
  2319  
  2320  			gb, err := getDetachedObjectGraphWihObjs(tt.fields.objs)
  2321  			g.Expect(err).ToNot(HaveOccurred())
  2322  
  2323  			// we want to check that soft dependent nodes are considered part of the cluster, so we make sure to call SetSoftDependants before SetClusterTenants
  2324  			gb.setSoftOwnership()
  2325  
  2326  			// finally test SetTenants
  2327  			gb.setTenants()
  2328  
  2329  			gotClusters := gb.getClusters()
  2330  			sort.Slice(gotClusters, func(i, j int) bool {
  2331  				return gotClusters[i].identity.UID < gotClusters[j].identity.UID
  2332  			})
  2333  
  2334  			g.Expect(gotClusters).To(HaveLen(len(tt.wantClusters)))
  2335  
  2336  			for _, cluster := range gotClusters {
  2337  				wantTenants, ok := tt.wantClusters[string(cluster.identity.UID)]
  2338  				g.Expect(ok).To(BeTrue())
  2339  
  2340  				gotTenants := []string{}
  2341  				for _, node := range gb.uidToNode {
  2342  					for c := range node.tenant {
  2343  						if c.identity.UID == cluster.identity.UID {
  2344  							gotTenants = append(gotTenants, string(node.identity.UID))
  2345  							g.Expect(node.isGlobalHierarchy).To(BeFalse()) // We should make sure that everything below a Cluster is not considered global
  2346  						}
  2347  					}
  2348  				}
  2349  
  2350  				g.Expect(gotTenants).To(ConsistOf(wantTenants))
  2351  			}
  2352  		})
  2353  	}
  2354  }
  2355  
  2356  func Test_objectGraph_setCRSTenants(t *testing.T) {
  2357  	type fields struct {
  2358  		objs []client.Object
  2359  	}
  2360  	tests := []struct {
  2361  		name     string
  2362  		fields   fields
  2363  		wantCRSs map[string][]string
  2364  	}{
  2365  		{
  2366  			name: "A ClusterResourceSet applied to a cluster",
  2367  			fields: fields{
  2368  				objs: func() []client.Object {
  2369  					objs := []client.Object{}
  2370  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  2371  
  2372  					objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  2373  						WithSecret("resource-s1").
  2374  						WithConfigMap("resource-c1").
  2375  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  2376  						Objs()...)
  2377  
  2378  					return objs
  2379  				}(),
  2380  			},
  2381  			wantCRSs: map[string][]string{ // wantCRDs is a map[ClusterResourceSet.UID] --> list of UIDs
  2382  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1": {
  2383  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",            // the ClusterResourceSet should be tenant of itself
  2384  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1", // ClusterResourceSetBinding are owned by ClusterResourceSet
  2385  					"/v1, Kind=Secret, ns1/resource-s1",                                             // resource are owned by ClusterResourceSet
  2386  					"/v1, Kind=ConfigMap, ns1/resource-c1",                                          // resource are owned by ClusterResourceSet
  2387  				},
  2388  			},
  2389  		},
  2390  		{
  2391  			name: "A ClusterResourceSet applied to two clusters",
  2392  			fields: fields{
  2393  				objs: func() []client.Object {
  2394  					objs := []client.Object{}
  2395  					objs = append(objs, test.NewFakeCluster("ns1", "cluster1").Objs()...)
  2396  					objs = append(objs, test.NewFakeCluster("ns1", "cluster2").Objs()...)
  2397  
  2398  					objs = append(objs, test.NewFakeClusterResourceSet("ns1", "crs1").
  2399  						WithSecret("resource-s1").
  2400  						WithConfigMap("resource-c1").
  2401  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster1")).
  2402  						ApplyToCluster(test.SelectClusterObj(objs, "ns1", "cluster2")).
  2403  						Objs()...)
  2404  
  2405  					return objs
  2406  				}(),
  2407  			},
  2408  			wantCRSs: map[string][]string{ // wantCRDs is a map[ClusterResourceSet.UID] --> list of UIDs
  2409  				"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1": {
  2410  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSet, ns1/crs1",            // the ClusterResourceSet should be tenant of itself
  2411  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster1", // ClusterResourceSetBinding are owned by ClusterResourceSet
  2412  					"addons.cluster.x-k8s.io/v1beta1, Kind=ClusterResourceSetBinding, ns1/cluster2", // ClusterResourceSetBinding are owned by ClusterResourceSet
  2413  					"/v1, Kind=Secret, ns1/resource-s1",                                             // resource are owned by ClusterResourceSet
  2414  					"/v1, Kind=ConfigMap, ns1/resource-c1",                                          // resource are owned by ClusterResourceSet
  2415  				},
  2416  			},
  2417  		},
  2418  	}
  2419  	for _, tt := range tests {
  2420  		t.Run(tt.name, func(t *testing.T) {
  2421  			g := NewWithT(t)
  2422  
  2423  			gb, err := getDetachedObjectGraphWihObjs(tt.fields.objs)
  2424  			g.Expect(err).ToNot(HaveOccurred())
  2425  
  2426  			gb.setTenants()
  2427  
  2428  			gotCRSs := gb.getCRSs()
  2429  			sort.Slice(gotCRSs, func(i, j int) bool {
  2430  				return gotCRSs[i].identity.UID < gotCRSs[j].identity.UID
  2431  			})
  2432  
  2433  			g.Expect(gotCRSs).To(HaveLen(len(tt.wantCRSs)))
  2434  
  2435  			for _, crs := range gotCRSs {
  2436  				wantTenants, ok := tt.wantCRSs[string(crs.identity.UID)]
  2437  				g.Expect(ok).To(BeTrue())
  2438  
  2439  				gotTenants := []string{}
  2440  				for _, node := range gb.uidToNode {
  2441  					for c := range node.tenant {
  2442  						if c.identity.UID == crs.identity.UID {
  2443  							gotTenants = append(gotTenants, string(node.identity.UID))
  2444  							g.Expect(node.isGlobalHierarchy).To(BeFalse()) // We should make sure that everything below a CRS is not considered global
  2445  						}
  2446  					}
  2447  				}
  2448  
  2449  				g.Expect(gotTenants).To(ConsistOf(wantTenants))
  2450  			}
  2451  		})
  2452  	}
  2453  }
  2454  
  2455  func Test_objectGraph_setGlobalIdentityTenants(t *testing.T) {
  2456  	type fields struct {
  2457  		objs []client.Object
  2458  	}
  2459  	tests := []struct {
  2460  		name         string
  2461  		fields       fields
  2462  		wantIdentity map[string][]string
  2463  	}{
  2464  		{
  2465  			name: "A global identity for an infrastructure provider owning a Secret with credentials in the provider's namespace",
  2466  			fields: fields{
  2467  				objs: test.NewFakeClusterInfrastructureIdentity("infra1-identity").
  2468  					WithSecretIn("infra1-system"). // a secret in infra1-system namespace, where an infrastructure provider is installed
  2469  					Objs(),
  2470  			},
  2471  			wantIdentity: map[string][]string{ // wantCRDs is a map[ClusterResourceSet.UID] --> list of UIDs
  2472  				"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericClusterInfrastructureIdentity, infra1-identity": {
  2473  					"infrastructure.cluster.x-k8s.io/v1beta1, Kind=GenericClusterInfrastructureIdentity, infra1-identity", // the global identity should be tenant of itself
  2474  					"/v1, Kind=Secret, infra1-system/infra1-identity-credentials",
  2475  				},
  2476  			},
  2477  		},
  2478  	}
  2479  	for _, tt := range tests {
  2480  		t.Run(tt.name, func(t *testing.T) {
  2481  			g := NewWithT(t)
  2482  
  2483  			gb, err := getDetachedObjectGraphWihObjs(tt.fields.objs)
  2484  			g.Expect(err).ToNot(HaveOccurred())
  2485  
  2486  			gb.setTenants()
  2487  
  2488  			gotIdentity := []*node{}
  2489  			for _, n := range gb.getNodes() {
  2490  				if n.forceMoveHierarchy {
  2491  					gotIdentity = append(gotIdentity, n)
  2492  				}
  2493  			}
  2494  			sort.Slice(gotIdentity, func(i, j int) bool {
  2495  				return gotIdentity[i].identity.UID < gotIdentity[j].identity.UID
  2496  			})
  2497  			g.Expect(gotIdentity).To(HaveLen(len(tt.wantIdentity)))
  2498  
  2499  			for _, i := range gotIdentity {
  2500  				wantTenants, ok := tt.wantIdentity[string(i.identity.UID)]
  2501  				g.Expect(ok).To(BeTrue())
  2502  
  2503  				gotTenants := []string{}
  2504  				for _, node := range gb.uidToNode {
  2505  					for c := range node.tenant {
  2506  						if c.identity.UID == i.identity.UID {
  2507  							gotTenants = append(gotTenants, string(node.identity.UID))
  2508  							g.Expect(node.isGlobalHierarchy).To(BeTrue()) // We should make sure that everything below a global object is considered global
  2509  						}
  2510  					}
  2511  				}
  2512  
  2513  				g.Expect(gotTenants).To(ConsistOf(wantTenants))
  2514  			}
  2515  		})
  2516  	}
  2517  }
  2518  
  2519  func deduplicateObjects(objs []client.Object) []client.Object {
  2520  	res := []client.Object{}
  2521  	uniqueObjectKeys := sets.Set[string]{}
  2522  	for _, o := range objs {
  2523  		if !uniqueObjectKeys.Has(string(o.GetUID())) {
  2524  			res = append(res, o)
  2525  			uniqueObjectKeys.Insert(string(o.GetUID()))
  2526  		}
  2527  	}
  2528  	return res
  2529  }