diff --git a/api/v1alpha4/common_types.go b/api/v1alpha4/common_types.go index fe0e3518bf11..e7e6c43183c6 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 dd0d3d3d0834..4b015b39ceb1 100644 --- a/controllers/clustertopology_controller_compute.go +++ b/controllers/clustertopology_controller_compute.go @@ -18,25 +18,29 @@ package controllers import ( "context" + "fmt" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apiserver/pkg/storage/names" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" + "sigs.k8s.io/cluster-api/controllers/external" "sigs.k8s.io/controller-runtime/pkg/client" ) // clusterTopologyClass holds all the objects required for computing the desired state of a managed Cluster topology. type clusterTopologyClass struct { - clusterClass *clusterv1.ClusterClass //nolint:structcheck - infrastructureClusterTemplate *unstructured.Unstructured //nolint:structcheck - controlPlane controlPlaneTopologyClass //nolint:structcheck + clusterClass *clusterv1.ClusterClass + infrastructureClusterTemplate *unstructured.Unstructured + controlPlane controlPlaneTopologyClass machineDeployments map[string]machineDeploymentTopologyClass //nolint:structcheck } // controlPlaneTopologyClass holds the templates required for computing the desired state of a managed control plane. type controlPlaneTopologyClass struct { - template *unstructured.Unstructured //nolint:structcheck - infrastructureMachineTemplate *unstructured.Unstructured //nolint:structcheck + template *unstructured.Unstructured + infrastructureMachineTemplate *unstructured.Unstructured } // machineDeploymentTopologyClass holds the templates required for computing the desired state of a managed deployment. @@ -49,16 +53,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. @@ -80,10 +84,232 @@ 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 control plane object requires it, compute the InfrastructureMachineTemplate for the control plane. + if class.controlPlane.infrastructureMachineTemplate != nil { + desiredState.controlPlane.infrastructureMachineTemplate = computeControlPlaneInfrastructureMachineTemplate(class, current) + } + + // 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 cluster class. +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.Wrap(err, "failed to generate the cluster infrastructure object") + } + return infrastructureCluster, nil +} + +// computeControlPlaneInfrastructureMachineTemplate computes the desired state for InfrastructureMachineTemplate +// that should be referenced by the control plane object. +func computeControlPlaneInfrastructureMachineTemplate(class *clusterTopologyClass, current *clusterTopologyState) *unstructured.Unstructured { + controlPlaneInfrastructureMachineTemplate := templateToTemplate(templateToInput{ + template: class.controlPlane.infrastructureMachineTemplate, + templateClonedFromRef: objToRef(class.controlPlane.infrastructureMachineTemplate), + cluster: current.cluster, + namePrefix: fmt.Sprintf("%s-controlplane-", current.cluster.Name), + currentObjectRef: getNestedRef(current.controlPlane.object, "spec", "machineTemplate", "infrastructureRef", "name"), + }) + return controlPlaneInfrastructureMachineTemplate +} + +// computeControlPlane computes the desired state for the ControlPlane object starting from the +// corresponding template defined in cluster class. +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: current.cluster.Spec.Topology.ControlPlane.Metadata.Labels, + annotations: current.cluster.Spec.Topology.ControlPlane.Metadata.Annotations, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to generate the control plane object") + } + + // If there is InfrastructureMachineTemplate to be used for the control plane, add a reference to it. + // 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 infrastructureMachineTemplate != nil { + if err := setNestedRef(controlPlane, infrastructureMachineTemplate, "spec", "machineTemplate", "infrastructureRef"); err != nil { + return nil, errors.Wrap(err, "failed to set the reference to the infrastructure machine template object within the control plane 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. + // TODO: uncomment when https://github.com/kubernetes-sigs/cluster-api/issues/4991 is merged + // 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 the replicas in the control plane object") + } + // } + + // Sets the desired Kubernetes version for the control plane. + // NOTE: Supporting this field is a requirement for control plane providers being compatible with ClusterClass. + // 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 the version in the control plane 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, errors.Wrap(err, "failed to compute desired state for the cluster infrastructure object") + } + + // 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. diff --git a/controllers/clustertopology_controller_compute_test.go b/controllers/clustertopology_controller_compute_test.go index e6a04129393d..634899422e83 100644 --- a/controllers/clustertopology_controller_compute_test.go +++ b/controllers/clustertopology_controller_compute_test.go @@ -17,15 +17,178 @@ limitations under the License. package controllers import ( + "strings" "testing" . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" ) +func TestTemplateToObject(t *testing.T) { + templateClonedFromRef := &corev1.ObjectReference{ + Kind: "clonedFromKind", + Namespace: "clonedFromNamespace", + Name: "clonedFromName", + APIVersion: "clonedFromAPIVersion", + } + template := newFakeInfrastructureClusterTemplate(metav1.NamespaceDefault, "infrastructureClusterTemplate").Obj() + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + namePrefix := "prefix-" + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + currerntRef := &corev1.ObjectReference{ + Kind: "refKind", + Namespace: "refNamespace", + Name: "refName", + APIVersion: "refAPIVersion", + } + + t.Run("Generates an object from a template", func(t *testing.T) { + g := NewWithT(t) + obj, err := templateToObject(templateToInput{ + template: template, + templateClonedFromRef: templateClonedFromRef, + cluster: cluster, + namePrefix: namePrefix, + currentObjectRef: nil, + labels: labels, + annotations: annotations, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + // TypeMeta + g.Expect(obj.GetAPIVersion()).To(Equal(template.GetAPIVersion())) + g.Expect(obj.GetKind()).To(Equal(strings.TrimSuffix(template.GetKind(), "Template"))) + + // ObjectMeta + g.Expect(obj.GetName()).To(HavePrefix(namePrefix)) + g.Expect(obj.GetNamespace()).To(Equal(cluster.Namespace)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterLabelName, cluster.Name)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterTopologyLabelName, "")) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue("l1", "")) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromGroupKindAnnotation, templateClonedFromRef.GroupVersionKind().GroupKind().String())) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromNameAnnotation, templateClonedFromRef.Name)) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue("a1", "")) + + // Spec + expectedSpec, ok, err := unstructured.NestedMap(template.UnstructuredContent(), "spec", "template", "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + + cloneSpec, ok, err := unstructured.NestedMap(obj.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(cloneSpec).To(Equal(expectedSpec)) + }) + 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: templateClonedFromRef, + cluster: cluster, + namePrefix: namePrefix, + currentObjectRef: currerntRef, + labels: labels, + annotations: annotations, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + // ObjectMeta + g.Expect(obj.GetName()).To(Equal(currerntRef.Name)) + }) +} + +func TestTemplateToTemplate(t *testing.T) { + templateClonedFromRef := &corev1.ObjectReference{ + Kind: "clonedFromKind", + Namespace: "clonedFromNamespace", + Name: "clonedFromName", + APIVersion: "clonedFromAPIVersion", + } + template := newFakeInfrastructureClusterTemplate(metav1.NamespaceDefault, "infrastructureClusterTemplate").Obj() + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + Namespace: metav1.NamespaceDefault, + }, + } + namePrefix := "prefix-" + labels := map[string]string{"l1": ""} + annotations := map[string]string{"a1": ""} + currerntRef := &corev1.ObjectReference{ + Kind: "refKind", + Namespace: "refNamespace", + Name: "refName", + APIVersion: "refAPIVersion", + } + + t.Run("Generates a template from a template", func(t *testing.T) { + g := NewWithT(t) + obj := templateToTemplate(templateToInput{ + template: template, + templateClonedFromRef: templateClonedFromRef, + cluster: cluster, + namePrefix: namePrefix, + currentObjectRef: nil, + labels: labels, + annotations: annotations, + }) + g.Expect(obj).ToNot(BeNil()) + + // TypeMeta + g.Expect(obj.GetAPIVersion()).To(Equal(template.GetAPIVersion())) + g.Expect(obj.GetKind()).To(Equal(template.GetKind())) + + // ObjectMeta + g.Expect(obj.GetName()).To(HavePrefix(namePrefix)) + g.Expect(obj.GetNamespace()).To(Equal(cluster.Namespace)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterLabelName, cluster.Name)) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue(clusterv1.ClusterTopologyLabelName, "")) + g.Expect(obj.GetLabels()).To(HaveKeyWithValue("l1", "")) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromGroupKindAnnotation, templateClonedFromRef.GroupVersionKind().GroupKind().String())) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromNameAnnotation, templateClonedFromRef.Name)) + g.Expect(obj.GetAnnotations()).To(HaveKeyWithValue("a1", "")) + + // Spec + expectedSpec, ok, err := unstructured.NestedMap(template.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + + cloneSpec, ok, err := unstructured.NestedMap(obj.UnstructuredContent(), "spec") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(cloneSpec).To(Equal(expectedSpec)) + }) + 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: templateClonedFromRef, + cluster: cluster, + namePrefix: namePrefix, + currentObjectRef: currerntRef, + labels: labels, + annotations: annotations, + }) + g.Expect(obj).ToNot(BeNil()) + + // ObjectMeta + g.Expect(obj.GetName()).To(Equal(currerntRef.Name)) + }) +} + func TestGetNestedRef(t *testing.T) { t.Run("Gets a nested ref if defined", func(t *testing.T) { g := NewWithT(t) @@ -143,7 +306,7 @@ type fakeInfrastructureClusterTemplate struct { name string } -func newFakeInfrastructureClusterTemplate(namespace, name string) *fakeInfrastructureClusterTemplate { //nolint:deadcode +func newFakeInfrastructureClusterTemplate(namespace, name string) *fakeInfrastructureClusterTemplate { return &fakeInfrastructureClusterTemplate{ namespace: namespace, name: name,