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

🌱 Compute desired Cluster and its referenced objects for a managed topology #5002

Merged
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
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
274 changes: 261 additions & 13 deletions controllers/clustertopology_controller_compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
fabriziopandini marked this conversation as resolved.
Show resolved Hide resolved
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] = ""
fabriziopandini marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
fabriziopandini marked this conversation as resolved.
Show resolved Hide resolved
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
fabriziopandini marked this conversation as resolved.
Show resolved Hide resolved
// it is possible to add cluster specific information without affecting the original object.
fabriziopandini marked this conversation as resolved.
Show resolved Hide resolved
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 All @@ -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
}
Expand All @@ -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
}
Loading