From 3f220870c44108f1f63c50d3bf1afdabcfe14202 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Tue, 3 Aug 2021 10:34:25 +0200 Subject: [PATCH] Generate desired Cluster and its referenced objects for a managed topology --- api/v1alpha4/common_types.go | 3 + .../clustertopology_controller_compute.go | 274 +++++++- ...clustertopology_controller_compute_test.go | 632 +++++++++++++++++- ...56-cluster-class-and-managed-topologies.md | 4 +- 4 files changed, 889 insertions(+), 24 deletions(-) diff --git a/api/v1alpha4/common_types.go b/api/v1alpha4/common_types.go index 60c86f55548d..a51cb5ebd4b8 100644 --- a/api/v1alpha4/common_types.go +++ b/api/v1alpha4/common_types.go @@ -26,6 +26,9 @@ const ( // external objects(bootstrap and infrastructure providers). ClusterLabelName = "cluster.x-k8s.io/cluster-name" + // ClusterTopologyLabelName is the label set on all the object which are managed as part of a ClusterTopology. + ClusterTopologyLabelName = "cluster.x-k8s.io/topology" + // ProviderLabelName is the label set on components in the provider manifest. // This label allows to easily identify all the components belonging to a provider; the clusterctl // tool uses this label for implementing provider's lifecycle operations. diff --git a/controllers/clustertopology_controller_compute.go b/controllers/clustertopology_controller_compute.go index a8c0d803e30c..92bd74325a6b 100644 --- a/controllers/clustertopology_controller_compute.go +++ b/controllers/clustertopology_controller_compute.go @@ -18,11 +18,14 @@ package controllers import ( "context" + "fmt" + "strings" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/storage/names" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" "sigs.k8s.io/cluster-api/controllers/external" utilconversion "sigs.k8s.io/cluster-api/util/conversion" @@ -54,16 +57,16 @@ type machineDeploymentTopologyClass struct { // NOTE: please note that we are going to deal with two different type state, the current state as read from the API server, // and the desired state resulting from processing the clusterTopologyClass. type clusterTopologyState struct { - cluster *clusterv1.Cluster //nolint:structcheck - infrastructureCluster *unstructured.Unstructured //nolint:structcheck - controlPlane controlPlaneTopologyState //nolint:structcheck + cluster *clusterv1.Cluster + infrastructureCluster *unstructured.Unstructured + controlPlane controlPlaneTopologyState machineDeployments []machineDeploymentTopologyState //nolint:structcheck } // controlPlaneTopologyState all the objects representing the state of a managed control plane. type controlPlaneTopologyState struct { - object *unstructured.Unstructured //nolint:structcheck - infrastructureMachineTemplate *unstructured.Unstructured //nolint:structcheck + object *unstructured.Unstructured + infrastructureMachineTemplate *unstructured.Unstructured } // machineDeploymentTopologyState all the objects representing the state of a managed deployment. @@ -163,10 +166,242 @@ func (r *ClusterTopologyReconciler) getCurrentState(ctx context.Context, cluster return nil, nil } -// Computes the desired state of the Cluster topology. -func (r *ClusterTopologyReconciler) computeDesiredState(ctx context.Context, input *clusterTopologyClass, current *clusterTopologyState) (*clusterTopologyState, error) { - // TODO: add compute logic - return nil, nil +// computeDesiredState computes the desired state of the cluster topology. +// NOTE: We are assuming all the required objects are provided as input; also, in case of any error, +// the entire compute operation operation will fail. This might be improved in the future if support for reconciling +// subset of a topology will be implemented. +func (r *ClusterTopologyReconciler) computeDesiredState(_ context.Context, class *clusterTopologyClass, current *clusterTopologyState) (*clusterTopologyState, error) { + var err error + desiredState := &clusterTopologyState{} + + // Compute the desired state of the InfrastructureCluster object. + if desiredState.infrastructureCluster, err = computeInfrastructureCluster(class, current); err != nil { + return nil, err + } + + // If the ControlPlane object requires it, compute the InfrastructureMachineTemplate for the ControlPlane. + if class.clusterClass.Spec.ControlPlane.MachineInfrastructure != nil { + if desiredState.controlPlane.infrastructureMachineTemplate, err = computeControlPlaneInfrastructureMachineTemplate(class, current); err != nil { + return nil, err + } + } + + // Compute the desired state of the ControlPlane object, eventually adding a reference to the + // InfrastructureMachineTemplate generated by the previous step. + if desiredState.controlPlane.object, err = computeControlPlane(class, current, desiredState.controlPlane.infrastructureMachineTemplate); err != nil { + return nil, err + } + + // Compute the desired state for the Cluster object adding a reference to the + // InfrastructureCluster and the ControlPlane objects generated by the previous step. + desiredState.cluster = computeCluster(current, desiredState.infrastructureCluster, desiredState.controlPlane.object) + + // TODO: implement generate desired state for machine deployments + + return desiredState, nil +} + +// computeInfrastructureCluster computes the desired state for the InfrastructureCluster object starting from the +// corresponding template defined in ClusterClass. +func computeInfrastructureCluster(class *clusterTopologyClass, current *clusterTopologyState) (*unstructured.Unstructured, error) { + infrastructureCluster, err := templateToObject(templateToInput{ + template: class.infrastructureClusterTemplate, + templateClonedFromRef: class.clusterClass.Spec.Infrastructure.Ref, + cluster: current.cluster, + namePrefix: fmt.Sprintf("%s-", current.cluster.Name), + currentObjectRef: current.cluster.Spec.InfrastructureRef, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to generate the InfrastructureCluster object from the %s", class.infrastructureClusterTemplate.GetKind()) + } + return infrastructureCluster, nil +} + +// computeControlPlaneInfrastructureMachineTemplate computes the desired state for InfrastructureMachineTemplate +// that should be referenced by the ControlPlane object. +func computeControlPlaneInfrastructureMachineTemplate(class *clusterTopologyClass, current *clusterTopologyState) (*unstructured.Unstructured, error) { + var currentInfrastructureMachineTemplate *corev1.ObjectReference + if current.controlPlane.object != nil { + var err error + if currentInfrastructureMachineTemplate, err = getNestedRef(current.controlPlane.object, "spec", "machineTemplate", "infrastructureRef"); err != nil { + return nil, errors.Wrap(err, "failed to get spec.machineTemplate.infrastructureRef for the current ControlPlane object") + } + } + + controlPlaneInfrastructureMachineTemplate := templateToTemplate(templateToInput{ + template: class.controlPlane.infrastructureMachineTemplate, + templateClonedFromRef: objToRef(class.controlPlane.infrastructureMachineTemplate), + cluster: current.cluster, + namePrefix: fmt.Sprintf("%s-controlplane-", current.cluster.Name), + currentObjectRef: currentInfrastructureMachineTemplate, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, class.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, class.clusterClass.Spec.ControlPlane.Metadata.Annotations), + }) + return controlPlaneInfrastructureMachineTemplate, nil +} + +// computeControlPlane computes the desired state for the ControlPlane object starting from the +// corresponding template defined in ClusterClass. +func computeControlPlane(class *clusterTopologyClass, current *clusterTopologyState, infrastructureMachineTemplate *unstructured.Unstructured) (*unstructured.Unstructured, error) { + controlPlane, err := templateToObject(templateToInput{ + template: class.controlPlane.template, + templateClonedFromRef: class.clusterClass.Spec.ControlPlane.Ref, + cluster: current.cluster, + namePrefix: fmt.Sprintf("%s-", current.cluster.Name), + currentObjectRef: current.cluster.Spec.ControlPlaneRef, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, class.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, class.clusterClass.Spec.ControlPlane.Metadata.Annotations), + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to generate the ControlPlane object from the %s", class.controlPlane.template.GetKind()) + } + + // If the ControlPlane object requires it, add a reference to InfrastructureMachine template to be used for the control plane machines. + // NOTE: Once set for the first time, the reference name is not expected to changed in this step + // (instead it could change later during the reconciliation if template rotation is triggered) + if class.clusterClass.Spec.ControlPlane.MachineInfrastructure != nil { + if err := setNestedRef(controlPlane, infrastructureMachineTemplate, "spec", "machineTemplate", "infrastructureRef"); err != nil { + return nil, errors.Wrap(err, "failed to spec.machineTemplate.infrastructureRef in the ControlPlane object") + } + } + + // If it is required to manage the number of replicas for the control plane, set the corresponding field. + // NOTE: If the Topology.controlPlane.replicas value is nil, it is assumed that the control plane controller + // does not implement support for this field and the ControlPlane object is generated without the number of Replicas. + if current.cluster.Spec.Topology.ControlPlane.Replicas != nil { + if err := unstructured.SetNestedField(controlPlane.UnstructuredContent(), int64(*current.cluster.Spec.Topology.ControlPlane.Replicas), "spec", "replicas"); err != nil { + return nil, errors.Wrap(err, "failed to set spec.replicas in the ControlPlane object") + } + } + + // Sets the desired Kubernetes version for the control plane. + // TODO: improve this logic by adding support for version upgrade component by component + if err := unstructured.SetNestedField(controlPlane.UnstructuredContent(), current.cluster.Spec.Topology.Version, "spec", "version"); err != nil { + return nil, errors.Wrap(err, "failed to set spec.version in the ControlPlane object") + } + + return controlPlane, nil +} + +// computeCluster computes the desired state for the Cluster object. +// NOTE: Some fields of the Cluster’s fields contribute to defining how a Cluster should look like (e.g. Cluster.Spec.Topology), +// while some other fields should be managed as part of the actual Cluster (e.g. Cluster.Spec.ControlPlaneRef); in this func +// we are concerned only about the latest group of fields. +func computeCluster(current *clusterTopologyState, infrastructureCluster, controlPlane *unstructured.Unstructured) *clusterv1.Cluster { + cluster := &clusterv1.Cluster{} + current.cluster.DeepCopyInto(cluster) + + // Enforce the topology labels. + // NOTE: The cluster label is added at creation time so this object could be read by the ClusterTopology + // controller immediately after creation, even before other controllers are going to add the label (if missing). + if cluster.Labels == nil { + cluster.Labels = map[string]string{} + } + cluster.Labels[clusterv1.ClusterLabelName] = cluster.Name + cluster.Labels[clusterv1.ClusterTopologyLabelName] = "" + + // Set the references to the infrastructureCluster and controlPlane objects. + // NOTE: Once set for the first time, the references are not expected to change. + cluster.Spec.InfrastructureRef = objToRef(infrastructureCluster) + cluster.Spec.ControlPlaneRef = objToRef(controlPlane) + + return cluster +} + +type templateToInput struct { + template *unstructured.Unstructured + templateClonedFromRef *corev1.ObjectReference + cluster *clusterv1.Cluster + namePrefix string + currentObjectRef *corev1.ObjectReference + labels map[string]string + annotations map[string]string +} + +// templateToObject generates an object from a template, taking care +// of adding required labels (cluster, topology), annotations (clonedFrom) +// and assigning a meaningful name (or reusing current reference name). +func templateToObject(in templateToInput) (*unstructured.Unstructured, error) { + // Enforce the topology labels into the provided label set. + // NOTE: The cluster label is added at creation time so this object could be read by the ClusterTopology + // controller immediately after creation, even before other controllers are going to add the label (if missing). + labels := in.labels + if labels == nil { + labels = map[string]string{} + } + labels[clusterv1.ClusterLabelName] = in.cluster.Name + labels[clusterv1.ClusterTopologyLabelName] = "" + + // Generate the object from the template. + // NOTE: OwnerRef can't be set at this stage; other controllers are going to add OwnerReferences when + // the object is actually created. + object, err := external.GenerateTemplate(&external.GenerateTemplateInput{ + Template: in.template, + TemplateRef: in.templateClonedFromRef, + Namespace: in.cluster.Namespace, + Labels: labels, + Annotations: in.annotations, + ClusterName: in.cluster.Name, + }) + if err != nil { + return nil, err + } + + // Ensure the generated objects have a meaningful name. + // NOTE: In case there is already a ref to this object in the Cluster, re-use the same name + // in order to simplify compare at later stages of the reconcile process. + object.SetName(names.SimpleNameGenerator.GenerateName(in.namePrefix)) + if in.currentObjectRef != nil && len(in.currentObjectRef.Name) > 0 { + object.SetName(in.currentObjectRef.Name) + } + + return object, nil +} + +// templateToTemplate generates a template from an existing template, taking care +// of adding required labels (cluster, topology), annotations (clonedFrom) +// and assigning a meaningful name (or reusing current reference name). +// NOTE: We are creating a copy of the ClusterClass template for each cluster so +// it is possible to add cluster specific information without affecting the original object. +func templateToTemplate(in templateToInput) *unstructured.Unstructured { + template := &unstructured.Unstructured{} + in.template.DeepCopyInto(template) + + // Enforce the topology labels into the provided label set. + // NOTE: The cluster label is added at creation time so this object could be read by the ClusterTopology + // controller immediately after creation, even before other controllers are going to add the label (if missing). + labels := template.GetLabels() + if labels == nil { + labels = map[string]string{} + } + for key, value := range in.labels { + labels[key] = value + } + labels[clusterv1.ClusterLabelName] = in.cluster.Name + labels[clusterv1.ClusterTopologyLabelName] = "" + template.SetLabels(labels) + + // Enforce cloned from annotations. + annotations := template.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + for key, value := range in.annotations { + annotations[key] = value + } + annotations[clusterv1.TemplateClonedFromNameAnnotation] = in.templateClonedFromRef.Name + annotations[clusterv1.TemplateClonedFromGroupKindAnnotation] = in.templateClonedFromRef.GroupVersionKind().GroupKind().String() + template.SetAnnotations(annotations) + + // Ensure the generated template gets a meaningful name. + // NOTE: In case there is already an object ref to this template, it is required to re-use the same name + // in order to simplify compare at later stages of the reconcile process. + template.SetName(names.SimpleNameGenerator.GenerateName(in.namePrefix)) + if in.currentObjectRef != nil && len(in.currentObjectRef.Name) > 0 { + template.SetName(in.currentObjectRef.Name) + } + + return template } // getNestedRef returns the ref value of a nested field. @@ -175,22 +410,22 @@ func getNestedRef(obj *unstructured.Unstructured, fields ...string) (*corev1.Obj if v, ok, err := unstructured.NestedString(obj.UnstructuredContent(), append(fields, "apiVersion")...); ok && err == nil { ref.APIVersion = v } else { - return nil, errors.Errorf("failed to get reference apiVersion") + return nil, errors.Errorf("failed to get %s.apiVersion from %s", strings.Join(fields, "."), obj.GetKind()) } if v, ok, err := unstructured.NestedString(obj.UnstructuredContent(), append(fields, "kind")...); ok && err == nil { ref.Kind = v } else { - return nil, errors.Errorf("failed to get reference Kind") + return nil, errors.Errorf("failed to get %s.kind from %s", strings.Join(fields, "."), obj.GetKind()) } if v, ok, err := unstructured.NestedString(obj.UnstructuredContent(), append(fields, "name")...); ok && err == nil { ref.Name = v } else { - return nil, errors.Errorf("failed to get reference name") + return nil, errors.Errorf("failed to get %s.name from %s", strings.Join(fields, "."), obj.GetKind()) } if v, ok, err := unstructured.NestedString(obj.UnstructuredContent(), append(fields, "namespace")...); ok && err == nil { ref.Namespace = v } else { - return nil, errors.Errorf("failed to get reference namespace") + return nil, errors.Errorf("failed to get %s.namespace from %s", strings.Join(fields, "."), obj.GetKind()) } return ref, nil } @@ -215,3 +450,16 @@ func objToRef(obj client.Object) *corev1.ObjectReference { Name: obj.GetName(), } } + +// mergeMap merges two maps into another one. +// NOTE: In case a key exists in both maps, the value in the first map is preserved. +func mergeMap(a, b map[string]string) map[string]string { + m := make(map[string]string) + for k, v := range b { + m[k] = v + } + for k, v := range a { + m[k] = v + } + return m +} diff --git a/controllers/clustertopology_controller_compute_test.go b/controllers/clustertopology_controller_compute_test.go index 682e3b90f3db..3d0bd915363a 100644 --- a/controllers/clustertopology_controller_compute_test.go +++ b/controllers/clustertopology_controller_compute_test.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -32,6 +33,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) +var ( + fakeRef1 = &corev1.ObjectReference{ + Kind: "refKind1", + Namespace: "refNamespace1", + Name: "refName1", + APIVersion: "refAPIVersion1", + } + + fakeRef2 = &corev1.ObjectReference{ + Kind: "refKind2", + Namespace: "refNamespace2", + Name: "refName2", + APIVersion: "refAPIVersion2", + } +) + func TestGetClass(t *testing.T) { crds := []client.Object{ fakeInfrastructureClusterTemplateCRD, @@ -334,6 +351,593 @@ func TestGetTemplate(t *testing.T) { } } +func TestComputeInfrastructureCluster(t *testing.T) { + // templates and ClusterClass + infrastructureClusterTemplate := newFakeInfrastructureClusterTemplate(metav1.NamespaceDefault, "template1").Obj() + clusterClass := newFakeClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate(infrastructureClusterTemplate). + Obj() + + // aggregating templates and cluster class into topologyClass (simulating getClass) + topologyClass := &clusterTopologyClass{ + clusterClass: clusterClass, + infrastructureClusterTemplate: infrastructureClusterTemplate, + } + + // current cluster objects + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + + t.Run("Generates the infrastructureCluster from the template", func(t *testing.T) { + g := NewWithT(t) + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + } + + obj, err := computeInfrastructureCluster(topologyClass, current) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.Infrastructure.Ref, + template: topologyClass.infrastructureClusterTemplate, + labels: nil, + annotations: nil, + currentRef: nil, + obj: obj, + }) + }) + t.Run("If there is already a reference to the infrastructureCluster, it preserves the reference name", func(t *testing.T) { + g := NewWithT(t) + + // current cluster objects for the test scenario + clusterWithInfrastructureRef := cluster.DeepCopy() + clusterWithInfrastructureRef.Spec.InfrastructureRef = fakeRef1 + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: clusterWithInfrastructureRef, + } + + obj, err := computeInfrastructureCluster(topologyClass, current) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.Infrastructure.Ref, + template: topologyClass.infrastructureClusterTemplate, + labels: nil, + annotations: nil, + currentRef: current.cluster.Spec.InfrastructureRef, + obj: obj, + }) + }) +} + +func TestComputeControlPlaneInfrastructureMachineTemplate(t *testing.T) { + // templates and ClusterClass + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + + infrastructureMachineTemplate := newFakeInfrastructureMachineTemplate(metav1.NamespaceDefault, "template1").Obj() + clusterClass := newFakeClusterClass(metav1.NamespaceDefault, "class1"). + WithControlPlaneMetadata(labels, annotations). + WithControlPlaneInfrastructureMachineTemplate(infrastructureMachineTemplate). + Obj() + + // aggregating templates and cluster class into topologyClass (simulating getClass) + topologyClass := &clusterTopologyClass{ + clusterClass: clusterClass, + controlPlane: controlPlaneTopologyClass{ + infrastructureMachineTemplate: infrastructureMachineTemplate, + }, + } + + // current cluster objects + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + Spec: clusterv1.ClusterSpec{ + Topology: &clusterv1.Topology{ + ControlPlane: clusterv1.ControlPlaneTopology{ + Metadata: clusterv1.ObjectMeta{ + Labels: map[string]string{"l2": ""}, + Annotations: map[string]string{"a2": ""}, + }, + }, + }, + }, + } + + t.Run("Generates the infrastructureMachineTemplate from the template", func(t *testing.T) { + g := NewWithT(t) + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + } + + obj, err := computeControlPlaneInfrastructureMachineTemplate(topologyClass, current) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToTemplate(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.MachineInfrastructure.Ref, + template: topologyClass.controlPlane.infrastructureMachineTemplate, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: nil, + obj: obj, + }) + }) + t.Run("If there is already a reference to the infrastructureMachineTemplate, it preserves the reference name", func(t *testing.T) { + g := NewWithT(t) + + // current cluster objects for the test scenario + currentInfrastructureMachineTemplate := newFakeInfrastructureMachineTemplate(metav1.NamespaceDefault, "cluster1-template1").Obj() + + controlPlane := &unstructured.Unstructured{Object: map[string]interface{}{}} + err := setNestedRef(controlPlane, currentInfrastructureMachineTemplate, "spec", "machineTemplate", "infrastructureRef") + g.Expect(err).ToNot(HaveOccurred()) + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + controlPlane: controlPlaneTopologyState{ + object: controlPlane, + infrastructureMachineTemplate: currentInfrastructureMachineTemplate, + }, + } + + obj, err := computeControlPlaneInfrastructureMachineTemplate(topologyClass, current) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToTemplate(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.MachineInfrastructure.Ref, + template: topologyClass.controlPlane.infrastructureMachineTemplate, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: objToRef(currentInfrastructureMachineTemplate), + obj: obj, + }) + }) +} + +func TestComputeControlPlane(t *testing.T) { + // templates and ClusterClass + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + + controlPlaneTemplate := newFakeControlPlaneTemplate(metav1.NamespaceDefault, "template1").Obj() + clusterClass := newFakeClusterClass(metav1.NamespaceDefault, "class1"). + WithControlPlaneMetadata(labels, annotations). + WithControlPlaneTemplate(controlPlaneTemplate). + Obj() + + // aggregating templates and cluster class into topologyClass (simulating getClass) + topologyClass := &clusterTopologyClass{ + clusterClass: clusterClass, + controlPlane: controlPlaneTopologyClass{ + template: controlPlaneTemplate, + }, + } + + // current cluster objects + version := "v1.21.2" + replicas := 3 + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + Spec: clusterv1.ClusterSpec{ + Topology: &clusterv1.Topology{ + Version: version, + ControlPlane: clusterv1.ControlPlaneTopology{ + Metadata: clusterv1.ObjectMeta{ + Labels: map[string]string{"l2": ""}, + Annotations: map[string]string{"a2": ""}, + }, + Replicas: &replicas, + }, + }, + }, + } + + t.Run("Generates the ControlPlane from the template", func(t *testing.T) { + g := NewWithT(t) + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + } + + obj, err := computeControlPlane(topologyClass, current, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.Ref, + template: topologyClass.controlPlane.template, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: nil, + obj: obj, + }) + + assertNestedField(g, obj, version, "spec", "version") + assertNestedField(g, obj, int64(replicas), "spec", "replicas") + assertNestedFieldUnset(g, obj, "spec", "machineTemplate", "infrastructureRef") + }) + t.Run("Skips setting replicas if required", func(t *testing.T) { + g := NewWithT(t) + + // current cluster objects + clusterWithoutReplicas := cluster.DeepCopy() + clusterWithoutReplicas.Spec.Topology.ControlPlane.Replicas = nil + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: clusterWithoutReplicas, + } + + obj, err := computeControlPlane(topologyClass, current, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.Ref, + template: topologyClass.controlPlane.template, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: nil, + obj: obj, + }) + + assertNestedField(g, obj, version, "spec", "version") + assertNestedFieldUnset(g, obj, "spec", "replicas") + assertNestedFieldUnset(g, obj, "spec", "machineTemplate", "infrastructureRef") + }) + t.Run("Generates the ControlPlane from the template and adds the infrastructure machine template if required", func(t *testing.T) { + g := NewWithT(t) + + // templates and ClusterClass + infrastructureMachineTemplate := newFakeInfrastructureMachineTemplate(metav1.NamespaceDefault, "template1").Obj() + clusterClass := newFakeClusterClass(metav1.NamespaceDefault, "class1"). + WithControlPlaneMetadata(labels, annotations). + WithControlPlaneTemplate(controlPlaneTemplate). + WithControlPlaneInfrastructureMachineTemplate(infrastructureMachineTemplate). + Obj() + + // aggregating templates and cluster class into topologyClass (simulating getClass) + topologyClass := &clusterTopologyClass{ + clusterClass: clusterClass, + controlPlane: controlPlaneTopologyClass{ + template: controlPlaneTemplate, + infrastructureMachineTemplate: infrastructureMachineTemplate, + }, + } + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + } + + obj, err := computeControlPlane(topologyClass, current, infrastructureMachineTemplate) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.Ref, + template: topologyClass.controlPlane.template, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: nil, + obj: obj, + }) + + assertNestedField(g, obj, version, "spec", "version") + assertNestedField(g, obj, int64(replicas), "spec", "replicas") + assertNestedField(g, obj, map[string]interface{}{ + "kind": infrastructureMachineTemplate.GetKind(), + "namespace": infrastructureMachineTemplate.GetNamespace(), + "name": infrastructureMachineTemplate.GetName(), + "apiVersion": infrastructureMachineTemplate.GetAPIVersion(), + }, "spec", "machineTemplate", "infrastructureRef") + }) + t.Run("If there is already a reference to the ControlPlane, it preserves the reference name", func(t *testing.T) { + g := NewWithT(t) + + // current cluster objects for the test scenario + clusterWithControlPlaneRef := cluster.DeepCopy() + clusterWithControlPlaneRef.Spec.ControlPlaneRef = fakeRef1 + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: clusterWithControlPlaneRef, + } + + obj, err := computeControlPlane(topologyClass, current, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: current.cluster, + templateRef: topologyClass.clusterClass.Spec.ControlPlane.Ref, + template: topologyClass.controlPlane.template, + labels: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Labels), + annotations: mergeMap(current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, topologyClass.clusterClass.Spec.ControlPlane.Metadata.Annotations), + currentRef: current.cluster.Spec.ControlPlaneRef, + obj: obj, + }) + }) +} + +func TestComputeCluster(t *testing.T) { + // generated objects + infrastructureCluster := newFakeInfrastructureCluster(metav1.NamespaceDefault, "infrastructureCluster1").Obj() + controlPlane := newFakeControlPlane(metav1.NamespaceDefault, "controlplane1").Obj() + + // current cluster objects + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + + // aggregating current cluster objects into clusterTopologyState (simulating getCurrentState) + current := &clusterTopologyState{ + cluster: cluster, + } + + g := NewWithT(t) + + obj := computeCluster(current, infrastructureCluster, controlPlane) + g.Expect(obj).ToNot(BeNil()) + + // TypeMeta + g.Expect(obj.APIVersion).To(Equal(cluster.APIVersion)) + g.Expect(obj.Kind).To(Equal(cluster.Kind)) + + // ObjectMeta + g.Expect(obj.Name).To(Equal(cluster.Name)) + g.Expect(obj.Namespace).To(Equal(cluster.Namespace)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterLabelName, cluster.Name)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterTopologyLabelName, "")) + + // Spec + g.Expect(obj.Spec.InfrastructureRef).To(Equal(objToRef(infrastructureCluster))) + g.Expect(obj.Spec.ControlPlaneRef).To(Equal(objToRef(controlPlane))) +} + +func TestTemplateToObject(t *testing.T) { + template := newFakeInfrastructureClusterTemplate(metav1.NamespaceDefault, "infrastructureClusterTemplate").Obj() + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + + t.Run("Generates an object from a template", func(t *testing.T) { + g := NewWithT(t) + obj, err := templateToObject(templateToInput{ + template: template, + templateClonedFromRef: fakeRef1, + cluster: cluster, + namePrefix: cluster.Name, + currentObjectRef: nil, + labels: labels, + annotations: annotations, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertTemplateToObject(g, assertTemplateInput{ + cluster: cluster, + templateRef: fakeRef1, + template: template, + labels: labels, + annotations: annotations, + currentRef: nil, + obj: obj, + }) + }) + t.Run("Overrides the generated name if there is already a reference", func(t *testing.T) { + g := NewWithT(t) + obj, err := templateToObject(templateToInput{ + template: template, + templateClonedFromRef: fakeRef1, + cluster: cluster, + namePrefix: cluster.Name, + currentObjectRef: fakeRef2, + labels: labels, + annotations: annotations, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + // ObjectMeta + assertTemplateToObject(g, assertTemplateInput{ + cluster: cluster, + templateRef: fakeRef1, + template: template, + labels: labels, + annotations: annotations, + currentRef: fakeRef2, + obj: obj, + }) + }) +} + +func TestTemplateToTemplate(t *testing.T) { + template := newFakeInfrastructureClusterTemplate(metav1.NamespaceDefault, "infrastructureClusterTemplate").Obj() + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + + t.Run("Generates a template from a template", func(t *testing.T) { + g := NewWithT(t) + obj := templateToTemplate(templateToInput{ + template: template, + templateClonedFromRef: fakeRef1, + cluster: cluster, + namePrefix: cluster.Name, + currentObjectRef: nil, + labels: labels, + annotations: annotations, + }) + g.Expect(obj).ToNot(BeNil()) + assertTemplateToTemplate(g, assertTemplateInput{ + cluster: cluster, + templateRef: fakeRef1, + template: template, + labels: labels, + annotations: annotations, + currentRef: nil, + obj: obj, + }) + }) + t.Run("Overrides the generated name if there is already a reference", func(t *testing.T) { + g := NewWithT(t) + obj := templateToTemplate(templateToInput{ + template: template, + templateClonedFromRef: fakeRef1, + cluster: cluster, + namePrefix: cluster.Name, + currentObjectRef: fakeRef2, + labels: labels, + annotations: annotations, + }) + g.Expect(obj).ToNot(BeNil()) + assertTemplateToTemplate(g, assertTemplateInput{ + cluster: cluster, + templateRef: fakeRef1, + template: template, + labels: labels, + annotations: annotations, + currentRef: fakeRef2, + obj: obj, + }) + }) +} + +type assertTemplateInput struct { + cluster *clusterv1.Cluster + templateRef *corev1.ObjectReference + template *unstructured.Unstructured + labels, annotations map[string]string + currentRef *corev1.ObjectReference + obj *unstructured.Unstructured +} + +func assertTemplateToObject(g *WithT, in assertTemplateInput) { + // TypeMeta + g.Expect(in.obj.GetAPIVersion()).To(Equal(in.template.GetAPIVersion())) + g.Expect(in.obj.GetKind()).To(Equal(strings.TrimSuffix(in.template.GetKind(), "Template"))) + + // ObjectMeta + if in.currentRef != nil { + g.Expect(in.obj.GetName()).To(Equal(in.currentRef.Name)) + } else { + g.Expect(in.obj.GetName()).To(HavePrefix(in.cluster.Name)) + } + g.Expect(in.obj.GetNamespace()).To(Equal(in.cluster.Namespace)) + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterLabelName, in.cluster.Name)) + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterTopologyLabelName, "")) + for k, v := range in.labels { + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(k, v)) + } + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromGroupKindAnnotation, in.templateRef.GroupVersionKind().GroupKind().String())) + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromNameAnnotation, in.templateRef.Name)) + for k, v := range in.annotations { + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(k, v)) + } + // Spec + expectedSpec, ok, err := unstructured.NestedMap(in.template.UnstructuredContent(), "spec", "template", "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + + cloneSpec, ok, err := unstructured.NestedMap(in.obj.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + for k, v := range expectedSpec { + g.Expect(cloneSpec).To(HaveKeyWithValue(k, v)) + } +} + +func assertTemplateToTemplate(g *WithT, in assertTemplateInput) { + // TypeMeta + g.Expect(in.obj.GetAPIVersion()).To(Equal(in.template.GetAPIVersion())) + g.Expect(in.obj.GetKind()).To(Equal(in.template.GetKind())) + + // ObjectMeta + if in.currentRef != nil { + g.Expect(in.obj.GetName()).To(Equal(in.currentRef.Name)) + } else { + g.Expect(in.obj.GetName()).To(HavePrefix(in.cluster.Name)) + } + g.Expect(in.obj.GetNamespace()).To(Equal(in.cluster.Namespace)) + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterLabelName, in.cluster.Name)) + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterTopologyLabelName, "")) + for k, v := range in.labels { + g.Expect(in.obj.GetLabels()).To(HaveKeyWithValue(k, v)) + } + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromGroupKindAnnotation, in.templateRef.GroupVersionKind().GroupKind().String())) + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromNameAnnotation, in.templateRef.Name)) + for k, v := range in.annotations { + g.Expect(in.obj.GetAnnotations()).To(HaveKeyWithValue(k, v)) + } + // Spec + expectedSpec, ok, err := unstructured.NestedMap(in.template.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + + cloneSpec, ok, err := unstructured.NestedMap(in.obj.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(cloneSpec).To(Equal(expectedSpec)) +} + +func assertNestedField(g *WithT, obj *unstructured.Unstructured, value interface{}, fields ...string) { + v, ok, err := unstructured.NestedFieldCopy(obj.UnstructuredContent(), fields...) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(v).To(Equal(value)) +} + +func assertNestedFieldUnset(g *WithT, obj *unstructured.Unstructured, fields ...string) { + _, ok, err := unstructured.NestedFieldCopy(obj.UnstructuredContent(), fields...) + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ok).To(BeFalse()) +} + func TestGetNestedRef(t *testing.T) { t.Run("Gets a nested ref if defined", func(t *testing.T) { g := NewWithT(t) @@ -527,6 +1131,7 @@ type fakeClusterClass struct { namespace string name string infrastructureClusterTemplate *unstructured.Unstructured + controlPlaneMetadata *clusterv1.ObjectMeta controlPlaneTemplate *unstructured.Unstructured controlPlaneInfrastructureMachineTemplate *unstructured.Unstructured workerMachineDeploymentTemplates map[string]fakeClusterClassMachineDeploymentTemplates @@ -554,6 +1159,14 @@ func (f *fakeClusterClass) WithControlPlaneTemplate(t *unstructured.Unstructured return f } +func (f *fakeClusterClass) WithControlPlaneMetadata(labels, annotations map[string]string) *fakeClusterClass { + f.controlPlaneMetadata = &clusterv1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + } + return f +} + func (f *fakeClusterClass) WithControlPlaneInfrastructureMachineTemplate(t *unstructured.Unstructured) *fakeClusterClass { f.controlPlaneInfrastructureMachineTemplate = t return f @@ -590,16 +1203,17 @@ func (f *fakeClusterClass) Obj() *clusterv1.ClusterClass { Ref: objToRef(f.infrastructureClusterTemplate), } } + if f.controlPlaneMetadata != nil { + obj.Spec.ControlPlane.Metadata = *f.controlPlaneMetadata + } if f.controlPlaneTemplate != nil { - obj.Spec.ControlPlane = clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: objToRef(f.controlPlaneTemplate), - }, + obj.Spec.ControlPlane.LocalObjectTemplate = clusterv1.LocalObjectTemplate{ + Ref: objToRef(f.controlPlaneTemplate), } - if f.controlPlaneInfrastructureMachineTemplate != nil { - obj.Spec.ControlPlane.MachineInfrastructure = &clusterv1.LocalObjectTemplate{ - Ref: objToRef(f.controlPlaneInfrastructureMachineTemplate), - } + } + if f.controlPlaneInfrastructureMachineTemplate != nil { + obj.Spec.ControlPlane.MachineInfrastructure = &clusterv1.LocalObjectTemplate{ + Ref: objToRef(f.controlPlaneInfrastructureMachineTemplate), } } if len(f.workerMachineDeploymentTemplates) > 0 { @@ -743,7 +1357,7 @@ type fakeInfrastructureCluster struct { name string } -func newFakeInfrastructureCluster(namespace, name string) *fakeInfrastructureCluster { //nolint:deadcode +func newFakeInfrastructureCluster(namespace, name string) *fakeInfrastructureCluster { return &fakeInfrastructureCluster{ namespace: namespace, name: name, diff --git a/docs/proposals/202105256-cluster-class-and-managed-topologies.md b/docs/proposals/202105256-cluster-class-and-managed-topologies.md index a5a937f9c2ed..d827f9ced7ce 100644 --- a/docs/proposals/202105256-cluster-class-and-managed-topologies.md +++ b/docs/proposals/202105256-cluster-class-and-managed-topologies.md @@ -425,7 +425,7 @@ This section lists out the behavior for Cluster objects using `ClusterClass` in 1. Creates the infrastructure provider specific cluster using the cluster template referenced in the `ClusterClass.spec.infrastructure.ref` field. 1. Add the topology label to the provider cluster object: ```yaml - cluster.x-k8s.io/topology: + cluster.x-k8s.io/topology: "" ``` 1. For the ControlPlane object in `cluster.spec.topology.controlPlane` 1. Initializes a control plane object using the control plane template defined in the `ClusterClass.spec.controlPlane.ref field`. Use the name ``. @@ -433,7 +433,7 @@ This section lists out the behavior for Cluster objects using `ClusterClass` in 1. Sets the k8s version on the control plane object from the `spec.topology.version`. 1. Add the following labels to the control plane object: ```yaml - cluster.x-k8s.io/topology: + cluster.x-k8s.io/topology: "" ``` 1. Creates the control plane object. 1. Sets the `spec.infrastructureRef` and `spec.controlPlaneRef` fields for the Cluster object.