Skip to content

Commit

Permalink
Generate desired Cluster and its referenced objects for a managed top…
Browse files Browse the repository at this point in the history
…ology
  • Loading branch information
fabriziopandini committed Jul 22, 2021
1 parent 1bb8d60 commit 84db72c
Show file tree
Hide file tree
Showing 3 changed files with 407 additions and 15 deletions.
3 changes: 3 additions & 0 deletions api/v1alpha4/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
254 changes: 240 additions & 14 deletions controllers/clustertopology_controller_compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 84db72c

Please sign in to comment.