Skip to content

Commit

Permalink
Implements paused condition for kubeadm kcp
Browse files Browse the repository at this point in the history
  • Loading branch information
theobarberbany committed Jul 4, 2024
1 parent e67a7ac commit c4dbfce
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 24 deletions.
62 changes: 38 additions & 24 deletions controlplane/kubeadm/internal/controllers/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,6 @@ func (r *KubeadmControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.
log = log.WithValues("Cluster", klog.KObj(cluster))
ctx = ctrl.LoggerInto(ctx, log)

if annotations.IsPaused(cluster, kcp) {
log.Info("Reconciliation is paused for this object")
return ctrl.Result{}, nil
}

// Initialize the patch helper.
patchHelper, err := patch.NewHelper(kcp, r.Client)
if err != nil {
Expand All @@ -181,27 +176,28 @@ func (r *KubeadmControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.
// patch and return right away instead of reusing the main defer,
// because the main defer may take too much time to get cluster status
// Patch ObservedGeneration only if the reconciliation completed successfully

// TODO theobarberbany: Is this ordering correct, do we want finalizer to
// take priority over the paused condition?
patchOpts := []patch.Option{patch.WithStatusObservedGeneration{}}
if err := patchHelper.Patch(ctx, kcp, patchOpts...); err != nil {
return ctrl.Result{}, errors.Wrapf(err, "failed to add finalizer")
}

log.Info("Returning early to add finalizer")
return ctrl.Result{}, nil
}

// Initialize the control plane scope; this includes also checking for orphan machines and
// adopt them if necessary.
controlPlane, adoptableMachineFound, err := r.initControlPlaneScope(ctx, cluster, kcp)
if err != nil {
log.Error(err, "Failed to initControlPlaneScope")
return ctrl.Result{}, err
}
if adoptableMachineFound {
// if there are no errors but at least one CP machine has been adopted, then requeue and
// wait for the update event for the ownership to be set.
return ctrl.Result{}, nil
}
log.Info("initControlPlaneScope")

defer func() {
log.Info("start of deferred update status")
// Always attempt to update status.
if err := r.updateStatus(ctx, controlPlane); err != nil {
var connFailure *internal.RemoteClusterConnectionError
Expand All @@ -222,6 +218,7 @@ func (r *KubeadmControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.
log.Error(err, "Failed to patch KubeadmControlPlane")
reterr = kerrors.NewAggregate([]error{reterr, err})
}
log.Info("patched KubeadmControlPlane")

// Only requeue if there is no error, Requeue or RequeueAfter and the object does not have a deletion timestamp.
if reterr == nil && res.IsZero() && kcp.ObjectMeta.DeletionTimestamp.IsZero() {
Expand All @@ -243,6 +240,21 @@ func (r *KubeadmControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.
}
}()

if annotations.IsPaused(cluster, kcp) {
log.Info("Reconciliation is paused for this object")
conditions.MarkTrue(kcp, clusterv1.PausedCondition)
return ctrl.Result{}, nil
}
log.Info("Object not paused")
conditions.MarkFalse(kcp, clusterv1.PausedCondition, clusterv1.ResourceNotPausedReason, clusterv1.ConditionSeverityInfo, "Resource is operating as expected")

if adoptableMachineFound {
// if there are no errors but at least one CP machine has been adopted, then requeue and
// wait for the update event for the ownership to be set.
log.Info("Returning early, adoptableMachineFound")
return ctrl.Result{}, nil
}

if !kcp.ObjectMeta.DeletionTimestamp.IsZero() {
// Handle deletion reconciliation loop.
res, err = r.reconcileDelete(ctx, controlPlane)
Expand Down Expand Up @@ -325,19 +337,21 @@ func patchKubeadmControlPlane(ctx context.Context, patchHelper *patch.Helper, kc
)

// Patch the object, ignoring conflicts on the conditions owned by this controller.
// Also, if requested, we are adding additional options like e.g. Patch ObservedGeneration when issuing the
// patch at the end of the reconcile loop.
options = append(options, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
controlplanev1.MachinesCreatedCondition,
clusterv1.ReadyCondition,
controlplanev1.MachinesSpecUpToDateCondition,
controlplanev1.ResizedCondition,
controlplanev1.MachinesReadyCondition,
controlplanev1.AvailableCondition,
controlplanev1.CertificatesAvailableCondition,
}})

return patchHelper.Patch(ctx, kcp, options...)
return patchHelper.Patch(
ctx,
kcp,
patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
controlplanev1.MachinesCreatedCondition,
clusterv1.ReadyCondition,
clusterv1.PausedCondition,
controlplanev1.MachinesSpecUpToDateCondition,
controlplanev1.ResizedCondition,
controlplanev1.MachinesReadyCondition,
controlplanev1.AvailableCondition,
controlplanev1.CertificatesAvailableCondition,
}},
patch.WithStatusObservedGeneration{},
)
}

// reconcile handles KubeadmControlPlane reconciliation.
Expand Down
101 changes: 101 additions & 0 deletions controlplane/kubeadm/internal/controllers/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ import (
"sigs.k8s.io/cluster-api/util/secret"
)

const (
timeout = time.Second * 30
)

func TestClusterToKubeadmControlPlane(t *testing.T) {
g := NewWithT(t)
fakeClient := newFakeClient()
Expand Down Expand Up @@ -391,6 +395,15 @@ func TestReconcilePaused(t *testing.T) {
Client: fakeClient,
SecretCachingClient: fakeClient,
recorder: record.NewFakeRecorder(32),
managementCluster: &fakeManagementCluster{
Management: &internal.Management{Client: env},
Workload: fakeWorkloadCluster{
Workload: &internal.Workload{
Client: env,
},
Status: internal.ClusterStatus{},
},
},
}

_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)})
Expand Down Expand Up @@ -2280,6 +2293,94 @@ func TestKubeadmControlPlaneReconciler_reconcileDelete(t *testing.T) {
})
}

func TestReconcilePausedCondition(t *testing.T) {
g := NewWithT(t)

ns, err := env.CreateNamespace(ctx, "test-reconcile-pause-condition")
g.Expect(err).ToNot(HaveOccurred())

cluster, kcp, _ := createClusterWithControlPlane(ns.Name)
g.Expect(env.Create(ctx, cluster)).To(Succeed())
g.Expect(env.Create(ctx, kcp)).To(Succeed())
defer func(do ...client.Object) {
g.Expect(env.Cleanup(ctx, do...)).To(Succeed())
}(cluster, kcp, ns)

// Set cluster.status.InfrastructureReady so we actually enter in the reconcile loop
patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf("{\"status\":{\"infrastructureReady\":%t}}", true)))
g.Expect(env.Status().Patch(ctx, cluster, patch)).To(Succeed())

genericInfrastructureMachineTemplate := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "GenericInfrastructureMachineTemplate",
"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
"metadata": map[string]interface{}{
"name": "infra-foo",
"namespace": cluster.Namespace,
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"hello": "world",
},
},
},
},
}
g.Expect(env.Create(ctx, genericInfrastructureMachineTemplate)).To(Succeed())

r := &KubeadmControlPlaneReconciler{
Client: env,
SecretCachingClient: secretCachingClient,
recorder: record.NewFakeRecorder(32),
managementCluster: &fakeManagementCluster{
Management: &internal.Management{Client: env},
Workload: fakeWorkloadCluster{
Workload: &internal.Workload{
Client: env,
},
Status: internal.ClusterStatus{},
},
},
}

// We start unpaused
g.Eventually(func() bool {
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)})
g.Expect(err).ToNot(HaveOccurred())

key := util.ObjectKey(kcp)
if err := env.Get(ctx, key, kcp); err != nil {
return false
}

// Checks the condition is set
if !conditions.Has(kcp, clusterv1.PausedCondition) {
return false
}
// The condition is set to false
return conditions.IsFalse(kcp, clusterv1.PausedCondition)
}, timeout).Should(BeTrue())

// Pause the cluster
cluster.Spec.Paused = true
g.Expect(env.Update(ctx, cluster)).To(Succeed())

g.Eventually(func() bool {
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)})
g.Expect(err).ToNot(HaveOccurred())

key := util.ObjectKey(kcp)
if err := env.Get(ctx, key, kcp); err != nil {
return false
}

// The condition is set to true
return conditions.IsTrue(kcp, clusterv1.PausedCondition)
}, timeout).Should(BeTrue())

}

Check failure on line 2382 in controlplane/kubeadm/internal/controllers/controller_test.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary trailing newline (whitespace)

// test utils.

func newFakeClient(initObjs ...client.Object) client.Client {
Expand Down

0 comments on commit c4dbfce

Please sign in to comment.