From e1319b5b533399557b8efa273787368396c58ed7 Mon Sep 17 00:00:00 2001 From: Vince Prignano Date: Wed, 5 Feb 2020 12:35:54 -0800 Subject: [PATCH] :running: Add support to auto-update external references Signed-off-by: Vince Prignano --- .../kubeadm/config/crd/kustomization.yaml | 4 ++ controllers/cluster_controller_phases.go | 5 +++ controllers/machine_controller_phases.go | 6 +++ controllers/machinedeployment_controller.go | 28 ++++++++++++- controllers/machineset_controller.go | 13 ++++-- .../kubeadm/config/crd/kustomization.yaml | 3 ++ .../providers/v1alpha2-to-v1alpha3.md | 19 +++++++++ .../docker/config/crd/kustomization.yaml | 3 ++ util/conversion/conversion.go | 42 +++++++++++++++++++ 9 files changed, 118 insertions(+), 5 deletions(-) diff --git a/bootstrap/kubeadm/config/crd/kustomization.yaml b/bootstrap/kubeadm/config/crd/kustomization.yaml index f2cb743988e1..5248a35c328c 100644 --- a/bootstrap/kubeadm/config/crd/kustomization.yaml +++ b/bootstrap/kubeadm/config/crd/kustomization.yaml @@ -1,3 +1,7 @@ +commonLabels: + cluster.x-k8s.io/v1alpha2: v1alpha2 + cluster.x-k8s.io/v1alpha3: v1alpha3 + # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default diff --git a/controllers/cluster_controller_phases.go b/controllers/cluster_controller_phases.go index 52d9aee2c088..afd4229332f6 100644 --- a/controllers/cluster_controller_phases.go +++ b/controllers/cluster_controller_phases.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/cluster-api/controllers/external" capierrors "sigs.k8s.io/cluster-api/errors" "sigs.k8s.io/cluster-api/util" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/cluster-api/util/kubeconfig" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/secret" @@ -62,6 +63,10 @@ func (r *ClusterReconciler) reconcilePhase(_ context.Context, cluster *clusterv1 func (r *ClusterReconciler) reconcileExternal(ctx context.Context, cluster *clusterv1.Cluster, ref *corev1.ObjectReference) (external.ReconcileOutput, error) { logger := r.Log.WithValues("cluster", cluster.Name, "namespace", cluster.Namespace) + if err := utilconversion.ConvertReferenceAPIContract(ctx, r.Client, ref); err != nil { + return external.ReconcileOutput{}, err + } + obj, err := external.Get(ctx, r.Client, ref, cluster.Namespace) if err != nil { if apierrors.IsNotFound(errors.Cause(err)) { diff --git a/controllers/machine_controller_phases.go b/controllers/machine_controller_phases.go index 6c2fdb8efc89..43b78f85d7ce 100644 --- a/controllers/machine_controller_phases.go +++ b/controllers/machine_controller_phases.go @@ -27,6 +27,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/utils/pointer" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -77,6 +78,10 @@ func (r *MachineReconciler) reconcilePhase(_ context.Context, m *clusterv1.Machi func (r *MachineReconciler) reconcileExternal(ctx context.Context, cluster *clusterv1.Cluster, m *clusterv1.Machine, ref *corev1.ObjectReference) (external.ReconcileOutput, error) { logger := r.Log.WithValues("machine", m.Name, "namespace", m.Namespace) + if err := utilconversion.ConvertReferenceAPIContract(ctx, r.Client, ref); err != nil { + return external.ReconcileOutput{}, err + } + obj, err := external.Get(ctx, r.Client, ref, m.Namespace) if err != nil { if apierrors.IsNotFound(errors.Cause(err)) { @@ -146,6 +151,7 @@ func (r *MachineReconciler) reconcileBootstrap(ctx context.Context, cluster *clu // Call generic external reconciler if we have an external reference. var bootstrapConfig *unstructured.Unstructured if m.Spec.Bootstrap.ConfigRef != nil { + bootstrapReconcileResult, err := r.reconcileExternal(ctx, cluster, m, m.Spec.Bootstrap.ConfigRef) if err != nil { return err diff --git a/controllers/machinedeployment_controller.go b/controllers/machinedeployment_controller.go index 733b04204b25..78bcf7e933b1 100644 --- a/controllers/machinedeployment_controller.go +++ b/controllers/machinedeployment_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "strings" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -29,7 +30,9 @@ import ( kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/record" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/cluster-api/controllers/external" "sigs.k8s.io/cluster-api/util" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/cluster-api/util/patch" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -131,7 +134,7 @@ func (r *MachineDeploymentReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result return result, nil } -func (r *MachineDeploymentReconciler) reconcile(_ context.Context, cluster *clusterv1.Cluster, d *clusterv1.MachineDeployment) (ctrl.Result, error) { +func (r *MachineDeploymentReconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster, d *clusterv1.MachineDeployment) (ctrl.Result, error) { logger := r.Log.WithValues("machinedeployment", d.Name, "namespace", d.Namespace) logger.V(4).Info("Reconcile MachineDeployment") @@ -166,6 +169,17 @@ func (r *MachineDeploymentReconciler) reconcile(_ context.Context, cluster *clus return ctrl.Result{}, nil } + // Make sure to reconcile the external infrastructure reference. + if err := r.reconcileExternalReference(ctx, cluster, &d.Spec.Template.Spec.InfrastructureRef); err != nil { + return ctrl.Result{}, err + } + // Make sure to reconcile the external bootstrap reference, if any. + if d.Spec.Template.Spec.Bootstrap.ConfigRef != nil { + if err := r.reconcileExternalReference(ctx, cluster, d.Spec.Template.Spec.Bootstrap.ConfigRef); err != nil { + return ctrl.Result{}, err + } + } + msList, err := r.getMachineSetsForDeployment(d) if err != nil { return ctrl.Result{}, err @@ -182,6 +196,18 @@ func (r *MachineDeploymentReconciler) reconcile(_ context.Context, cluster *clus return ctrl.Result{}, errors.Errorf("unexpected deployment strategy type: %s", d.Spec.Strategy.Type) } +func (r *MachineDeploymentReconciler) reconcileExternalReference(ctx context.Context, cluster *clusterv1.Cluster, ref *corev1.ObjectReference) error { + if !strings.HasSuffix(ref.Kind, external.TemplateSuffix) { + return nil + } + + if err := utilconversion.ConvertReferenceAPIContract(ctx, r.Client, ref); err != nil { + return err + } + + return nil +} + // getMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment. func (r *MachineDeploymentReconciler) getMachineSetsForDeployment(d *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) { logger := r.Log.WithValues("machinedeployemnt", d.Name, "namespace", d.Namespace) diff --git a/controllers/machineset_controller.go b/controllers/machineset_controller.go index 0640a17cf025..566b347ff841 100644 --- a/controllers/machineset_controller.go +++ b/controllers/machineset_controller.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/cluster-api/controllers/noderefutil" "sigs.k8s.io/cluster-api/controllers/remote" "sigs.k8s.io/cluster-api/util" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/cluster-api/util/patch" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -163,12 +164,12 @@ func (r *MachineSetReconciler) reconcile(ctx context.Context, cluster *clusterv1 } // Make sure to reconcile the external infrastructure reference. - if err := r.reconcileExternalReference(ctx, cluster, machineSet.Spec.Template.Spec.InfrastructureRef); err != nil { + if err := r.reconcileExternalReference(ctx, cluster, &machineSet.Spec.Template.Spec.InfrastructureRef); err != nil { return ctrl.Result{}, err } // Make sure to reconcile the external bootstrap reference, if any. if machineSet.Spec.Template.Spec.Bootstrap.ConfigRef != nil { - if err := r.reconcileExternalReference(ctx, cluster, *machineSet.Spec.Template.Spec.Bootstrap.ConfigRef); err != nil { + if err := r.reconcileExternalReference(ctx, cluster, machineSet.Spec.Template.Spec.Bootstrap.ConfigRef); err != nil { return ctrl.Result{}, err } } @@ -268,12 +269,16 @@ func (r *MachineSetReconciler) reconcile(ctx context.Context, cluster *clusterv1 return ctrl.Result{}, nil } -func (r *MachineSetReconciler) reconcileExternalReference(ctx context.Context, cluster *clusterv1.Cluster, ref corev1.ObjectReference) error { +func (r *MachineSetReconciler) reconcileExternalReference(ctx context.Context, cluster *clusterv1.Cluster, ref *corev1.ObjectReference) error { if !strings.HasSuffix(ref.Kind, external.TemplateSuffix) { return nil } - obj, err := external.Get(ctx, r.Client, &ref, cluster.Namespace) + if err := utilconversion.ConvertReferenceAPIContract(ctx, r.Client, ref); err != nil { + return err + } + + obj, err := external.Get(ctx, r.Client, ref, cluster.Namespace) if err != nil { return err } diff --git a/controlplane/kubeadm/config/crd/kustomization.yaml b/controlplane/kubeadm/config/crd/kustomization.yaml index 27193e5c8cb2..e01417005cb8 100644 --- a/controlplane/kubeadm/config/crd/kustomization.yaml +++ b/controlplane/kubeadm/config/crd/kustomization.yaml @@ -1,3 +1,6 @@ +commonLabels: + cluster.x-k8s.io/v1alpha3: v1alpha3 + # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default diff --git a/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md b/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md index 434bd3286bcd..91facc6ba802 100644 --- a/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md +++ b/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md @@ -154,3 +154,22 @@ unique keys to `failureDomainSpec`s as well as respecting a set `Machine.Spec.Fa instances. Please see the cluster and machine infrastructure provider specifications for more detail. + +# Apply the contract version label `cluster.x-k8s.io/: version1,version2,version3` to your CRDs + +- Providers MUST set `cluster.x-k8s.io/` labels on all Custom Resource Definitions related to Cluster API starting with v1alpha3. +- The label is a map from an API Version of Cluster API (contract) to your Custom Resource Definition versions. + - The value is a comma-delimited list of versions. + - Each value MUST point to an available version in your CRD Spec. +- The label allows Cluster API controllers to perform automatic conversions for object references, the controllers will + pick the last available version in the list if multiple versions are found. +- To apply the label to CRDs it's possible to use `commonLabels` in your `kustomize.yaml` file, usually in `config/crd`. + +In this example we show how to map a particular Cluster API contract version to your own CRD using Kustomize's `commonLabels` feature: + +```yaml +commonLabels: + cluster.x-k8s.io/v1alpha2: v1alpha1 + cluster.x-k8s.io/v1alpha3: v1alpha2 + cluster.x-k8s.io/v1beta1: v1alphaX,v1beta1 +``` diff --git a/test/infrastructure/docker/config/crd/kustomization.yaml b/test/infrastructure/docker/config/crd/kustomization.yaml index 0058f714e9ea..ea31036cf50c 100644 --- a/test/infrastructure/docker/config/crd/kustomization.yaml +++ b/test/infrastructure/docker/config/crd/kustomization.yaml @@ -1,3 +1,6 @@ +commonLabels: + cluster.x-k8s.io/v1alpha3: v1alpha3 + # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default diff --git a/util/conversion/conversion.go b/util/conversion/conversion.go index dedbf37ede84..3d5bced7a596 100644 --- a/util/conversion/conversion.go +++ b/util/conversion/conversion.go @@ -17,11 +17,16 @@ limitations under the License. package conversion import ( + "context" "math/rand" + "strings" "testing" fuzz "github.com/google/gofuzz" "github.com/onsi/gomega" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" apiequality "k8s.io/apimachinery/pkg/api/equality" metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" @@ -30,6 +35,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/json" "k8s.io/utils/diff" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/conversion" ) @@ -37,6 +44,41 @@ const ( DataAnnotation = "cluster.x-k8s.io/conversion-data" ) +var ( + contract = clusterv1.GroupVersion.String() +) + +// ConvertReferenceAPIContract takes a client and object reference, queries the API Server for +// the Custom Resource Definition and looks which one is the stored version available. +// +// The object passed as input is modified in place if an updated compatible version is found. +func ConvertReferenceAPIContract(ctx context.Context, c client.Client, ref *corev1.ObjectReference) error { + // TODO(vincepri): Use CRDv1 once we have support for it. + crd := &apiextensionsv1beta1.CustomResourceDefinition{} + key := client.ObjectKey{Name: ref.Kind} + if err := c.Get(ctx, key, crd); err != nil { + return errors.Wrapf(err, "cannot find CustomResourceDefinition for %v", ref.GroupVersionKind()) + } + + // If there is no label, return early without changing the reference. + supportedVersions, ok := crd.Labels[contract] + if !ok { + return nil + } + + // If the label is present, but there list is empty, return an error. + versions := strings.Split(supportedVersions, ",") + if len(versions) == 0 { + return errors.Errorf("cannot find any version matching contract %q for CRD %v", contract, crd.Name) + } + + // Modify the GroupVersionKind with the new version. + gvk := ref.GroupVersionKind() + gvk.Version = versions[len(versions)-1] + ref.SetGroupVersionKind(gvk) + return nil +} + // MarshalData stores the source object as json data in the destination object annotations map. func MarshalData(src metav1.Object, dst metav1.Object) error { data, err := json.Marshal(src)