Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] 🌱 Clusterctl move Cluster Principal #3254

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cmd/clusterctl/client/cluster/mover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,35 @@ var moveTests = []struct {
},
},
},
{
name: "Two cluster with the same principal",
fields: moveTestsFields{
objs: func() []runtime.Object {
principal := test.NewInfrastructurePrincipal("principal")
objs := []runtime.Object{principal}
objs = append(objs, test.NewFakeCluster("ns1", "cluster1").WithPrincipal(principal).Objs()...)
objs = append(objs, test.NewFakeCluster("ns1", "cluster2").WithPrincipal(principal).Objs()...)
return objs
}(),
},
wantMoveGroups: [][]string{
{ //group 1
"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/cluster1",
"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/cluster2",
"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=DummyInfrastructurePrincipal, /principal", // the principal should be moved before the infrastructure clusters
},
{ //group 2 (objects with ownerReferences in group 1)
// owned by Clusters
"/v1, Kind=Secret, ns1/cluster1-ca",
"/v1, Kind=Secret, ns1/cluster1-kubeconfig",
"/v1, Kind=Secret, ns1/cluster2-ca",
"/v1, Kind=Secret, ns1/cluster2-kubeconfig",
"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=DummyInfrastructureCluster, ns1/cluster1",
"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=DummyInfrastructureCluster, ns1/cluster2",
},
},
wantErr: false,
},
}

func Test_getMoveSequence(t *testing.T) {
Expand Down
111 changes: 96 additions & 15 deletions cmd/clusterctl/client/cluster/objectgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package cluster

import (
"strings"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand Down Expand Up @@ -83,6 +85,10 @@ func (n *node) isSoftOwnedBy(other *node) bool {
return ok
}

func (n *node) isGlobal() bool {
return n.identity.Namespace == ""
}

// objectGraph manages the Kubernetes object graph that is generated during the discovery phase for the move operation.
type objectGraph struct {
proxy Proxy
Expand All @@ -98,15 +104,21 @@ func newObjectGraph(proxy Proxy) *objectGraph {

// addObj adds a Kubernetes object to the object graph that is generated during the move discovery phase.
// During add, OwnerReferences are processed in order to create the dependency graph.
func (o *objectGraph) addObj(obj *unstructured.Unstructured) {
func (o *objectGraph) addObj(obj *unstructured.Unstructured, types discoveryTypeMetas) {
// Adds the node to the Graph.
newNode := o.objToNode(obj)

// Process OwnerReferences; if the owner object doe not exists yet, create a virtual node as a placeholder for it.
for _, ownerReference := range obj.GetOwnerReferences() {
ownerNode, ok := o.uidToNode[ownerReference.UID]
if !ok {
ownerNode = o.ownerToVirtualNode(ownerReference, newNode.identity.Namespace)
// When creating a virtual node, we are assuming it is the same namespace of the current obj
// unless the GVK of the virtual node is globally scoped.
namespace := newNode.identity.Namespace
if types.isTypeGlobal(ownerReference.APIVersion, ownerReference.Kind) {
namespace = ""
}
ownerNode = o.ownerToVirtualNode(ownerReference, namespace)
}

newNode.addOwner(ownerNode, ownerReferenceAttributes{
Expand Down Expand Up @@ -165,10 +177,28 @@ func (o *objectGraph) objToNode(obj *unstructured.Unstructured) *node {
return newNode
}

// discoveryTypeMeta holds information about types to be considered during discovery
type discoveryTypeMeta struct {
Kind string
APIVersion string
Scope apiextensionsv1.ResourceScope
}

type discoveryTypeMetas []discoveryTypeMeta

func (t discoveryTypeMetas) isTypeGlobal(apiVersion, kind string) bool {
for _, t := range t {
if t.APIVersion == apiVersion && strings.TrimSuffix(t.Kind, "List") == kind {
return t.Scope == apiextensionsv1.ClusterScoped
}
}
return false
}

// getDiscoveryTypes returns the list of TypeMeta to be considered for the the move discovery phase.
// This list includes all the types defines by the CRDs installed by clusterctl and the ConfigMap/Secret core types.
func (o *objectGraph) getDiscoveryTypes() ([]metav1.TypeMeta, error) {
discoveredTypes := []metav1.TypeMeta{}
func (o *objectGraph) getDiscoveryTypes() (discoveryTypeMetas, error) {
discoveredTypes := discoveryTypeMetas{}

crdList := &apiextensionsv1.CustomResourceDefinitionList{}
getDiscoveryTypesBackoff := newReadBackoff()
Expand All @@ -184,18 +214,19 @@ func (o *objectGraph) getDiscoveryTypes() ([]metav1.TypeMeta, error) {
continue
}

discoveredTypes = append(discoveredTypes, metav1.TypeMeta{
discoveredTypes = append(discoveredTypes, discoveryTypeMeta{
Kind: crd.Spec.Names.Kind,
APIVersion: metav1.GroupVersion{
Group: crd.Spec.Group,
Version: version.Name,
}.String(),
Scope: crd.Spec.Scope,
})
}
}

discoveredTypes = append(discoveredTypes, metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"})
discoveredTypes = append(discoveredTypes, metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"})
discoveredTypes = append(discoveredTypes, discoveryTypeMeta{Kind: "Secret", APIVersion: "v1", Scope: apiextensionsv1.NamespaceScoped})
discoveredTypes = append(discoveredTypes, discoveryTypeMeta{Kind: "ConfigMap", APIVersion: "v1", Scope: apiextensionsv1.NamespaceScoped})

return discoveredTypes, nil
}
Expand All @@ -214,20 +245,21 @@ func getCRDList(proxy Proxy, crdList *apiextensionsv1.CustomResourceDefinitionLi

// Discovery reads all the Kubernetes objects existing in a namespace (or in all namespaces if empty) for the types received in input, and then adds
// everything to the objects graph.
func (o *objectGraph) Discovery(namespace string, types []metav1.TypeMeta) error {
func (o *objectGraph) Discovery(namespace string, types discoveryTypeMetas) error {
log := logf.Log
log.Info("Discovering Cluster API objects")

selectors := []client.ListOption{}
if namespace != "" {
selectors = append(selectors, client.InNamespace(namespace))
}

discoveryBackoff := newReadBackoff()
for i := range types {
typeMeta := types[i]
objList := new(unstructured.UnstructuredList)

selectors := []client.ListOption{}
// adds namespace selector if the resource is NamespaceScoped and namespace value is not empty.
if typeMeta.Scope == apiextensionsv1.NamespaceScoped && namespace != "" {
selectors = append(selectors, client.InNamespace(namespace))
}

if err := retryWithExponentialBackoff(discoveryBackoff, func() error {
return getObjList(o.proxy, typeMeta, selectors, objList)
}); err != nil {
Expand All @@ -241,7 +273,7 @@ func (o *objectGraph) Discovery(namespace string, types []metav1.TypeMeta) error
log.V(5).Info(typeMeta.Kind, "Count", len(objList.Items))
for i := range objList.Items {
obj := objList.Items[i]
o.addObj(&obj)
o.addObj(&obj, types)
}
}

Expand All @@ -254,10 +286,13 @@ func (o *objectGraph) Discovery(namespace string, types []metav1.TypeMeta) error
// Completes the graph by setting for each node the list of Clusters the node belong to.
o.setClusterTenants()

// Completes the graph by identifying cluster principals.
o.setClusterPrincipalsTenants()

return nil
}

func getObjList(proxy Proxy, typeMeta metav1.TypeMeta, selectors []client.ListOption, objList *unstructured.UnstructuredList) error {
func getObjList(proxy Proxy, typeMeta discoveryTypeMeta, selectors []client.ListOption, objList *unstructured.UnstructuredList) error {
c, err := proxy.NewClient()
if err != nil {
return err
Expand Down Expand Up @@ -369,3 +404,49 @@ func (o *objectGraph) setClusterTenant(node, tenant *node) {
}
}
}

// setClusterPrincipalsTenants sets the cluster tenants for the clusters principal.
// NOTE: Cluster principals are defined as infrastructure specific objects, globally scoped;
// In case a cluster principal is used, clusterctl discovery can rely on a OwnerReference on the
// infrastructure cluster object for identifying the cluster principal
func (o *objectGraph) setClusterPrincipalsTenants() {
for _, cluster := range o.getClusters() {
if infrastructureCluster := o.getInfrastructureCluster(cluster); infrastructureCluster != nil {
if principal := o.getPrincipal(infrastructureCluster); principal != nil {
principal.tenantClusters[cluster] = empty{}
}
}
}
}

// getInfrastructureCluster identifies the InfrastructureCluster object among all the nodes owned by a cluster.
// NOTE: This assumes the infrastructure provider following a common naming convention.
func (o *objectGraph) getInfrastructureCluster(cluster *node) *node {
for _, n := range o.getNodes() {
if !n.isOwnedBy(cluster) {
continue
}
if strings.HasSuffix(n.identity.Kind, "Cluster") {
return n
}
}
return nil
}

// getPrincipal identifies the Principal among all the nodes owned by a InfrastructureCluster.
// NOTE: This assumes the infrastructure provider following a common naming convention.
func (o *objectGraph) getPrincipal(infrastructureCluster *node) *node {
for _, n := range o.getNodes() {
if !n.isGlobal() {
continue
}
if !infrastructureCluster.isOwnedBy(n) {
continue
}

if strings.HasSuffix(n.identity.Kind, "Principal") {
return n
}
}
return nil
}
Loading