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

✨ Populate fleet external annotation on management clusters #917

Merged
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
2 changes: 1 addition & 1 deletion charts/rancher-turtles/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ spec:
containers:
- args:
- --leader-elect
- --feature-gates=propagate-labels={{ index .Values "rancherTurtles" "features" "propagate-labels" "enabled"}},managementv3-cluster={{ index .Values "rancherTurtles" "features" "managementv3-cluster" "enabled"}},rancher-kube-secret-patch={{ index .Values "rancherTurtles" "features" "rancher-kubeconfigs" "label"}}
- --feature-gates=propagate-labels={{ index .Values "rancherTurtles" "features" "propagate-labels" "enabled"}},managementv3-cluster={{ index .Values "rancherTurtles" "features" "managementv3-cluster" "enabled"}},rancher-kube-secret-patch={{ index .Values "rancherTurtles" "features" "rancher-kubeconfigs" "label"}},addon-provider-fleet={{ index .Values "rancherTurtles" "features" "addon-provider-fleet" "enabled"}}
{{- range .Values.rancherTurtles.managerArguments }}
- {{ . }}
{{- end }}
Expand Down
6 changes: 6 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const (
// PropagateLabels is used to enable copying the labels from the CAPI cluster
// to the Rancher cluster.
PropagateLabels featuregate.Feature = "propagate-labels"

// ExternalFleet allows to disable in-tree management of the Fleet clusters
// in the imported rancher clusters, by setting "provisioning.cattle.io/externally-managed"
// annotation.
ExternalFleet featuregate.Feature = "addon-provider-fleet"
)

func init() {
Expand All @@ -42,4 +47,5 @@ var defaultGates = map[featuregate.Feature]featuregate.FeatureSpec{
RancherKubeSecretPatch: {Default: false, PreRelease: featuregate.Beta},
ManagementV3Cluster: {Default: false, PreRelease: featuregate.Beta},
PropagateLabels: {Default: false, PreRelease: featuregate.Beta},
ExternalFleet: {Default: false, PreRelease: featuregate.Beta},
}
1 change: 1 addition & 0 deletions internal/controllers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const (
capiClusterOwner = "cluster-api.cattle.io/capi-cluster-owner"
capiClusterOwnerNamespace = "cluster-api.cattle.io/capi-cluster-owner-ns"
v1ClusterMigrated = "cluster-api.cattle.io/migrated"
externalFleetAnnotation = "provisioning.cattle.io/externally-managed"

defaultRequeueDuration = 1 * time.Minute
trueAnnotationValue = "true"
Expand Down
170 changes: 92 additions & 78 deletions internal/controllers/import_controller_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package controllers

import (
"cmp"
"context"
"fmt"
"strings"
Expand All @@ -27,7 +28,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
errorutils "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
Expand Down Expand Up @@ -184,13 +184,8 @@ func (r *CAPIImportManagementV3Reconciler) Reconcile(ctx context.Context, req ct
errs = append(errs, fmt.Errorf("error reconciling cluster: %w", err))
}

if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := r.Client.Patch(ctx, capiCluster, patchBase); err != nil {
errs = append(errs, fmt.Errorf("failed to patch cluster: %w", err))
}
return nil
}); err != nil {
return ctrl.Result{}, err
if err := r.Client.Patch(ctx, capiCluster, patchBase); err != nil {
errs = append(errs, fmt.Errorf("failed to patch cluster: %w", err))
}

if len(errs) > 0 {
Expand All @@ -200,7 +195,7 @@ func (r *CAPIImportManagementV3Reconciler) Reconcile(ctx context.Context, req ct
return result, nil
}

func (r *CAPIImportManagementV3Reconciler) reconcile(ctx context.Context, capiCluster *clusterv1.Cluster) (ctrl.Result, error) {
func (r *CAPIImportManagementV3Reconciler) reconcile(ctx context.Context, capiCluster *clusterv1.Cluster) (res ctrl.Result, reterr error) {
log := log.FromContext(ctx)

migrated, err := r.verifyV1ClusterMigration(ctx, capiCluster)
Expand All @@ -214,7 +209,7 @@ func (r *CAPIImportManagementV3Reconciler) reconcile(ctx context.Context, capiCl
ownedLabelName: "",
}

rancherCluster := &managementv3.Cluster{}
var rancherCluster *managementv3.Cluster

rancherClusterList := &managementv3.ClusterList{}
selectors := []client.ListOption{
Expand All @@ -234,7 +229,7 @@ func (r *CAPIImportManagementV3Reconciler) reconcile(ctx context.Context, capiCl
rancherCluster = &rancherClusterList.Items[0]
}

if !rancherCluster.ObjectMeta.DeletionTimestamp.IsZero() {
if rancherCluster != nil && !rancherCluster.ObjectMeta.DeletionTimestamp.IsZero() {
if err := r.reconcileDelete(ctx, capiCluster); err != nil {
log.Error(err, "Removing CAPI Cluster failed, retrying")
return ctrl.Result{}, err
Expand All @@ -253,86 +248,72 @@ func (r *CAPIImportManagementV3Reconciler) reconcile(ctx context.Context, capiCl
}
}

return r.reconcileNormal(ctx, capiCluster, rancherCluster)
patchBase := client.MergeFromWithOptions(rancherCluster.DeepCopy(), client.MergeFromWithOptimisticLock{})

defer func() {
// As the rancherCluster is created inside reconcileNormal, we can only patch existing object
// Skipping non-existent cluster or returned error
if reterr != nil || rancherCluster == nil {
return
}

if err := r.Client.Patch(ctx, rancherCluster, patchBase); err != nil {
reterr = fmt.Errorf("failed to patch Rancher cluster: %w", err)
}
}()

res, reterr = r.reconcileNormal(ctx, capiCluster, rancherCluster)

return res, reterr
}

func (r *CAPIImportManagementV3Reconciler) reconcileNormal(ctx context.Context, capiCluster *clusterv1.Cluster,
rancherCluster *managementv3.Cluster,
) (ctrl.Result, error) {
log := log.FromContext(ctx)

err := r.RancherClient.Get(ctx, client.ObjectKeyFromObject(rancherCluster), rancherCluster)
if apierrors.IsNotFound(err) {
if autoImport, err := r.shouldAutoImportUncached(ctx, capiCluster); err != nil || !autoImport {
return ctrl.Result{}, err
}
clusterMissing := rancherCluster == nil

newCluster := &managementv3.Cluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: capiCluster.Namespace,
GenerateName: "c-",
Labels: map[string]string{
capiClusterOwner: capiCluster.Name,
capiClusterOwnerNamespace: capiCluster.Namespace,
ownedLabelName: "",
},
Annotations: map[string]string{
turtlesannotations.NoCreatorRBACAnnotation: trueAnnotationValue,
},
Finalizers: []string{
managementv3.CapiClusterFinalizer,
},
updatedCluster := &managementv3.Cluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: capiCluster.Namespace,
GenerateName: "c-",
Labels: map[string]string{
capiClusterOwner: capiCluster.Name,
capiClusterOwnerNamespace: capiCluster.Namespace,
ownedLabelName: "",
},
Spec: managementv3.ClusterSpec{
DisplayName: capiCluster.Name,
Description: "CAPI cluster imported to Rancher",
Finalizers: []string{
managementv3.CapiClusterFinalizer,
},
}

if feature.Gates.Enabled(feature.PropagateLabels) {
for labelKey, labelVal := range capiCluster.Labels {
newCluster.Labels[labelKey] = labelVal
}
}

if err := r.RancherClient.Create(ctx, newCluster); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating rancher cluster: %w", err)
}

return ctrl.Result{Requeue: true}, nil
},
Spec: managementv3.ClusterSpec{
DisplayName: capiCluster.Name,
Description: "CAPI cluster imported to Rancher",
},
}

if err != nil {
log.Error(err, fmt.Sprintf("Unable to fetch rancher cluster %s", client.ObjectKeyFromObject(rancherCluster)))
rancherCluster = cmp.Or(rancherCluster, updatedCluster)

return ctrl.Result{}, err
}
r.optOutOfClusterOwner(ctx, rancherCluster)
r.optOutOfFleetManagement(ctx, rancherCluster)
r.propagateLabels(ctx, capiCluster, rancherCluster)

if err := r.optOutOfClusterOwner(ctx, rancherCluster); err != nil {
return ctrl.Result{}, fmt.Errorf("error annotating rancher cluster %s to opt out of cluster owner: %w", rancherCluster.Name, err)
addedFinalizer := controllerutil.AddFinalizer(rancherCluster, managementv3.CapiClusterFinalizer)
if addedFinalizer {
log.Info("Successfully added capicluster.turtles.cattle.io finalizer to Rancher cluster")
}

patchBase := client.MergeFromWithOptions(rancherCluster.DeepCopy(), client.MergeFromWithOptimisticLock{})
needsFinalizer := controllerutil.AddFinalizer(rancherCluster, managementv3.CapiClusterFinalizer)

if feature.Gates.Enabled(feature.PropagateLabels) {
if rancherCluster.Labels == nil {
rancherCluster.Labels = map[string]string{}
}

for labelKey, labelVal := range capiCluster.Labels {
rancherCluster.Labels[labelKey] = labelVal
if clusterMissing {
if autoImport, err := r.shouldAutoImportUncached(ctx, capiCluster); err != nil || !autoImport {
return ctrl.Result{}, err
}

if err := r.Client.Patch(ctx, rancherCluster, patchBase); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch Rancher cluster: %w", err)
if err := r.RancherClient.Create(ctx, rancherCluster); err != nil {
return ctrl.Result{}, fmt.Errorf("error creating rancher cluster: %w", err)
}

log.Info("Successfully propagated labels to Rancher cluster")
} else if needsFinalizer {
if err := r.Client.Patch(ctx, rancherCluster, patchBase); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch Rancher cluster: %w", err)
}
return ctrl.Result{Requeue: true}, nil
}

if conditions.IsTrue(rancherCluster, managementv3.ClusterConditionReady) {
Expand Down Expand Up @@ -548,7 +529,7 @@ func (r *CAPIImportManagementV3Reconciler) verifyV1ClusterMigration(ctx context.

// optOutOfClusterOwner annotates the cluster with the opt-out annotation.
// Rancher will detect this annotation and it won't create ProjectOwner or ClusterOwner roles.
func (r *CAPIImportManagementV3Reconciler) optOutOfClusterOwner(ctx context.Context, rancherCluster *managementv3.Cluster) error {
func (r *CAPIImportManagementV3Reconciler) optOutOfClusterOwner(ctx context.Context, rancherCluster *managementv3.Cluster) {
log := log.FromContext(ctx)

annotations := rancherCluster.GetAnnotations()
Expand All @@ -561,15 +542,48 @@ func (r *CAPIImportManagementV3Reconciler) optOutOfClusterOwner(ctx context.Cont
rancherCluster.Name,
turtlesannotations.ClusterImportedAnnotation))

patchBase := client.MergeFromWithOptions(rancherCluster.DeepCopy(), client.MergeFromWithOptimisticLock{})

annotations[turtlesannotations.NoCreatorRBACAnnotation] = trueAnnotationValue
rancherCluster.SetAnnotations(annotations)
}
}

if err := r.Client.Patch(ctx, rancherCluster, patchBase); err != nil {
return fmt.Errorf("error patching rancher cluster: %w", err)
}
// optOutOfFleetManagement annotates the cluster with the fleet provisioning opt-out annotation,
// allowing external fleet cluster management.
func (r *CAPIImportManagementV3Reconciler) optOutOfFleetManagement(ctx context.Context, rancherCluster *managementv3.Cluster) {
log := log.FromContext(ctx)

annotations := rancherCluster.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}

return nil
if _, found := annotations[externalFleetAnnotation]; !found && feature.Gates.Enabled(feature.ExternalFleet) {
annotations[externalFleetAnnotation] = "true"
rancherCluster.SetAnnotations(annotations)

log.Info("Added fleet annotation to Rancher cluster")
}
}

func (r *CAPIImportManagementV3Reconciler) propagateLabels(
ctx context.Context,
capiCluster *clusterv1.Cluster,
rancherCluster *managementv3.Cluster,
) {
log := log.FromContext(ctx)

labels := rancherCluster.GetLabels()
if rancherCluster.Labels == nil {
labels = map[string]string{}
}

if feature.Gates.Enabled(feature.PropagateLabels) {
for labelKey, labelVal := range capiCluster.Labels {
labels[labelKey] = labelVal
}

rancherCluster.SetLabels(labels)

log.V(5).Info("Propagated labels to Rancher cluster")
}
}
68 changes: 68 additions & 0 deletions internal/controllers/import_controller_v3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,29 @@ var _ = Describe("reconcile CAPI Cluster", func() {
Expect(rancherClusters.Items[0].Name).To(ContainSubstring("c-"))
})

It("should set fleet annotation on a freshly imported rancher cluster", func() {
Expect(cl.Create(ctx, capiCluster)).To(Succeed())
capiCluster.Status.ControlPlaneReady = true
Expect(cl.Status().Update(ctx, capiCluster)).To(Succeed())

Eventually(ctx, func(g Gomega) {
res, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: capiCluster.Namespace,
Name: capiCluster.Name,
},
})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(res.Requeue).To(BeTrue())
}).Should(Succeed())

Eventually(ctx, func(g Gomega) {
g.Expect(cl.List(ctx, rancherClusters, selectors...)).ToNot(HaveOccurred())
g.Expect(rancherClusters.Items).To(HaveLen(1))
}).Should(Succeed())
Expect(rancherClusters.Items[0].Annotations).To(HaveKeyWithValue(externalFleetAnnotation, testLabelVal))
})

It("should reconcile a CAPI cluster when rancher cluster exists, and have finalizers set", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -404,6 +427,51 @@ var _ = Describe("reconcile CAPI Cluster", func() {
}).Should(Succeed())
})

It("should set the fleet annotation on an already imported cluster", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(sampleTemplate))
}))
defer server.Close()

Expect(cl.Create(ctx, capiCluster)).To(Succeed())
capiCluster.Status.ControlPlaneReady = true
Expect(cl.Status().Update(ctx, capiCluster)).To(Succeed())

Expect(cl.Create(ctx, capiKubeconfigSecret)).To(Succeed())

Expect(cl.Create(ctx, rancherCluster)).To(Succeed())
Eventually(ctx, func(g Gomega) {
g.Expect(cl.List(ctx, rancherClusters, selectors...)).ToNot(HaveOccurred())
g.Expect(rancherClusters.Items).To(HaveLen(1))
}).Should(Succeed())
cluster := rancherClusters.Items[0]
Expect(cluster.Name).To(ContainSubstring("c-"))

clusterRegistrationToken.Name = cluster.Name
clusterRegistrationToken.Namespace = cluster.Name
_, err := testEnv.CreateNamespaceWithName(ctx, cluster.Name)
Expect(err).ToNot(HaveOccurred())
Expect(cl.Create(ctx, clusterRegistrationToken)).To(Succeed())
token := clusterRegistrationToken.DeepCopy()
token.Status.ManifestURL = server.URL
Expect(cl.Status().Update(ctx, token)).To(Succeed())

Eventually(ctx, func(g Gomega) {
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: capiCluster.Namespace,
Name: capiCluster.Name,
},
})
g.Expect(err).ToNot(HaveOccurred())

rancherCluster := cluster.DeepCopy()
g.Expect(cl.Get(ctx, client.ObjectKeyFromObject(&cluster), rancherCluster)).To(Succeed())
g.Expect(rancherCluster.Annotations).To(HaveKeyWithValue(externalFleetAnnotation, testLabelVal))
}, 5*time.Second).Should(Succeed())
})

It("should reconcile a CAPI cluster when rancher cluster exists and a cluster registration token does not exist", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
func init() {
utilruntime.Must(feature.MutableGates.SetFromMap(map[string]bool{
string(feature.PropagateLabels): true,
string(feature.ExternalFleet): true,
}))
}

Expand Down
Loading