Skip to content

Commit 3ecb5e8

Browse files
author
Yuvaraj Kakaraparthi
committed
Implement BeforeClusterCreate, BeforeClusterUpgrade and BeforeClusterDelete lifecycle hooks
1 parent 4e26957 commit 3ecb5e8

14 files changed

+669
-22
lines changed

api/v1beta1/condition_consts.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,8 @@ const (
277277
// TopologyReconciledMachineDeploymentsUpgradePendingReason (Severity=Info) documents reconciliation of a Cluster topology
278278
// not yet completed because at least one of the MachineDeployments is not yet updated to match the desired topology spec.
279279
TopologyReconciledMachineDeploymentsUpgradePendingReason = "MachineDeploymentsUpgradePending"
280+
281+
// TopologyReconciledHookBlockingReason (Severity=Info) documents reconciliation of a Cluster topology
282+
// not yet completed because at least one of the lifecycle hooks is blocking.
283+
TopologyReconciledHookBlockingReason = "LifecycleHookBlocking"
280284
)

controllers/alias.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
clustertopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster"
3333
machinedeploymenttopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/machinedeployment"
3434
machinesettopologycontroller "sigs.k8s.io/cluster-api/internal/controllers/topology/machineset"
35+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
3536
)
3637

3738
// Following types provides access to reconcilers implemented in internal/controllers, thus
@@ -42,6 +43,8 @@ type ClusterReconciler struct {
4243
Client client.Client
4344
APIReader client.Reader
4445

46+
RuntimeClient runtimeclient.Client
47+
4548
// WatchFilterValue is the label value used to filter events prior to reconciliation.
4649
WatchFilterValue string
4750
}
@@ -50,6 +53,7 @@ func (r *ClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manag
5053
return (&clustercontroller.Reconciler{
5154
Client: r.Client,
5255
APIReader: r.APIReader,
56+
RuntimeClient: r.RuntimeClient,
5357
WatchFilterValue: r.WatchFilterValue,
5458
}).SetupWithManager(ctx, mgr, options)
5559
}
@@ -133,6 +137,8 @@ type ClusterTopologyReconciler struct {
133137
// race conditions caused by an outdated cache.
134138
APIReader client.Reader
135139

140+
RuntimeClient runtimeclient.Client
141+
136142
// WatchFilterValue is the label value used to filter events prior to reconciliation.
137143
WatchFilterValue string
138144

@@ -145,6 +151,7 @@ func (r *ClusterTopologyReconciler) SetupWithManager(ctx context.Context, mgr ct
145151
return (&clustertopologycontroller.Reconciler{
146152
Client: r.Client,
147153
APIReader: r.APIReader,
154+
RuntimeClient: r.RuntimeClient,
148155
UnstructuredCachingClient: r.UnstructuredCachingClient,
149156
WatchFilterValue: r.WatchFilterValue,
150157
}).SetupWithManager(ctx, mgr, options)

internal/controllers/cluster/cluster_controller.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ import (
4040
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
4141
"sigs.k8s.io/cluster-api/controllers/external"
4242
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
43+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
4344
"sigs.k8s.io/cluster-api/feature"
45+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
4446
"sigs.k8s.io/cluster-api/util"
4547
"sigs.k8s.io/cluster-api/util/annotations"
4648
"sigs.k8s.io/cluster-api/util/collections"
@@ -67,6 +69,8 @@ type Reconciler struct {
6769
Client client.Client
6870
APIReader client.Reader
6971

72+
RuntimeClient runtimeclient.Client
73+
7074
// WatchFilterValue is the label value used to filter events prior to reconciliation.
7175
WatchFilterValue string
7276

@@ -215,6 +219,22 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster)
215219
func (r *Reconciler) reconcileDelete(ctx context.Context, cluster *clusterv1.Cluster) (reconcile.Result, error) {
216220
log := ctrl.LoggerFrom(ctx)
217221

222+
// Call the BeforeClusterDelete hook to before proceeding further.
223+
if feature.Gates.Enabled(feature.RuntimeSDK) {
224+
hookRequest := &runtimehooksv1.BeforeClusterDeleteRequest{
225+
Cluster: *cluster,
226+
}
227+
hookResponse := &runtimehooksv1.BeforeClusterDeleteResponse{}
228+
if err := r.RuntimeClient.CallAllExtensions(ctx, runtimehooksv1.BeforeClusterDelete, hookRequest, hookResponse); err != nil {
229+
return ctrl.Result{}, errors.Wrap(err, "error calling BeforeClusterDelete hook")
230+
}
231+
if hookResponse.RetryAfterSeconds != 0 {
232+
// Cannot proceed with deleting the cluster yet. Lets requeue to retry at a later time.
233+
return ctrl.Result{RequeueAfter: time.Duration(hookResponse.RetryAfterSeconds) * time.Second}, nil
234+
}
235+
// We can proceed with the delete operation.
236+
}
237+
218238
descendants, err := r.listDescendants(ctx, cluster)
219239
if err != nil {
220240
log.Error(err, "Failed to list descendants")

internal/controllers/cluster/cluster_controller_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ package cluster
1818

1919
import (
2020
"testing"
21+
"time"
2122

2223
. "github.com/onsi/gomega"
2324
corev1 "k8s.io/api/core/v1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
utilfeature "k8s.io/component-base/featuregate/testing"
2527
"k8s.io/utils/pointer"
2628
ctrl "sigs.k8s.io/controller-runtime"
2729
"sigs.k8s.io/controller-runtime/pkg/client"
2830
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2931

3032
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3133
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
34+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
3235
"sigs.k8s.io/cluster-api/feature"
36+
runtimecatalog "sigs.k8s.io/cluster-api/internal/runtime/catalog"
37+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
38+
runtimeclienttest "sigs.k8s.io/cluster-api/internal/runtime/client/test"
3339
"sigs.k8s.io/cluster-api/util"
3440
"sigs.k8s.io/cluster-api/util/conditions"
3541
"sigs.k8s.io/cluster-api/util/patch"
@@ -350,6 +356,90 @@ func TestClusterReconciler(t *testing.T) {
350356
})
351357
}
352358

359+
func TestReconcileDelete(t *testing.T) {
360+
defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)()
361+
gvh, err := runtimeclienttest.DefaultTestCatalog.GroupVersionHook(runtimehooksv1.BeforeClusterDelete)
362+
if err != nil {
363+
panic(err)
364+
}
365+
366+
blockingResponse := &runtimehooksv1.BeforeClusterDeleteResponse{
367+
CommonRetryResponse: runtimehooksv1.CommonRetryResponse{
368+
RetryAfterSeconds: int32(10),
369+
},
370+
}
371+
runtimeClientWithBlockingResponse := runtimeclienttest.NewFakeRuntimeClientBuilder().
372+
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
373+
gvh: blockingResponse,
374+
}).
375+
Build()
376+
377+
nonBlockingResponse := &runtimehooksv1.BeforeClusterDeleteResponse{
378+
CommonRetryResponse: runtimehooksv1.CommonRetryResponse{
379+
RetryAfterSeconds: int32(0),
380+
},
381+
}
382+
runtimeClientWithNonBlockingResponse := runtimeclienttest.NewFakeRuntimeClientBuilder().
383+
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
384+
gvh: nonBlockingResponse,
385+
}).
386+
Build()
387+
388+
tests := []struct {
389+
name string
390+
cluster *clusterv1.Cluster
391+
runtimeClient runtimeclient.Client
392+
wantResult ctrl.Result
393+
wantErr bool
394+
}{
395+
{
396+
name: "should requeue if BeforeClusterDelete hook is blocking",
397+
cluster: &clusterv1.Cluster{
398+
ObjectMeta: metav1.ObjectMeta{
399+
Name: "test-name",
400+
Namespace: "test-ns",
401+
},
402+
Spec: clusterv1.ClusterSpec{},
403+
},
404+
runtimeClient: runtimeClientWithBlockingResponse,
405+
wantResult: ctrl.Result{RequeueAfter: time.Duration(10) * time.Second},
406+
wantErr: false,
407+
},
408+
{
409+
name: "should perform delete if BeforeClusterDelete hook is non-blocking",
410+
cluster: &clusterv1.Cluster{
411+
ObjectMeta: metav1.ObjectMeta{
412+
Name: "test-name",
413+
Namespace: "test-ns",
414+
},
415+
Spec: clusterv1.ClusterSpec{},
416+
},
417+
runtimeClient: runtimeClientWithNonBlockingResponse,
418+
wantResult: ctrl.Result{},
419+
wantErr: false,
420+
},
421+
}
422+
423+
for _, tt := range tests {
424+
t.Run(tt.name, func(t *testing.T) {
425+
g := NewWithT(t)
426+
427+
r := &Reconciler{
428+
RuntimeClient: tt.runtimeClient,
429+
Client: env,
430+
APIReader: env,
431+
}
432+
result, err := r.reconcileDelete(ctx, tt.cluster)
433+
if tt.wantErr {
434+
g.Expect(err).NotTo(BeNil())
435+
} else {
436+
g.Expect(err).To(BeNil())
437+
g.Expect(result).To(Equal(tt.wantResult))
438+
}
439+
})
440+
}
441+
}
442+
353443
func TestClusterReconcilerNodeRef(t *testing.T) {
354444
t.Run("machine to cluster", func(t *testing.T) {
355445
cluster := &clusterv1.Cluster{

internal/controllers/topology/cluster/cluster_controller.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cluster
1919
import (
2020
"context"
2121
"fmt"
22+
"time"
2223

2324
"github.com/pkg/errors"
2425
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -30,13 +31,17 @@ import (
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132
"sigs.k8s.io/controller-runtime/pkg/controller"
3233
"sigs.k8s.io/controller-runtime/pkg/handler"
34+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3335
"sigs.k8s.io/controller-runtime/pkg/source"
3436

3537
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3638
"sigs.k8s.io/cluster-api/api/v1beta1/index"
3739
"sigs.k8s.io/cluster-api/controllers/external"
40+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
41+
"sigs.k8s.io/cluster-api/feature"
3842
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches"
3943
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/scope"
44+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
4045
"sigs.k8s.io/cluster-api/util"
4146
"sigs.k8s.io/cluster-api/util/annotations"
4247
"sigs.k8s.io/cluster-api/util/patch"
@@ -58,6 +63,8 @@ type Reconciler struct {
5863
// race conditions caused by an outdated cache.
5964
APIReader client.Reader
6065

66+
RuntimeClient runtimeclient.Client
67+
6168
// WatchFilterValue is the label value used to filter events prior to reconciliation.
6269
WatchFilterValue string
6370

@@ -200,6 +207,17 @@ func (r *Reconciler) reconcile(ctx context.Context, s *scope.Scope) (ctrl.Result
200207
return ctrl.Result{}, errors.Wrap(err, "error reading current state of the Cluster topology")
201208
}
202209

210+
// The cluster topology is yet to be created. Call the BeforeClusterCreate hook before proceeding.
211+
if feature.Gates.Enabled(feature.RuntimeSDK) {
212+
res, err := r.callBeforeClusterCreateHook(ctx, s)
213+
if err != nil {
214+
return reconcile.Result{}, errors.Wrap(err, "error calling BeforeClusterCreate hook")
215+
}
216+
if !res.IsZero() {
217+
return res, nil
218+
}
219+
}
220+
203221
// Setup watches for InfrastructureCluster and ControlPlane CRs when they exist.
204222
if err := r.setupDynamicWatches(ctx, s); err != nil {
205223
return ctrl.Result{}, errors.Wrap(err, "error creating dynamic watch")
@@ -216,6 +234,11 @@ func (r *Reconciler) reconcile(ctx context.Context, s *scope.Scope) (ctrl.Result
216234
return ctrl.Result{}, errors.Wrap(err, "error reconciling the Cluster topology")
217235
}
218236

237+
requeueAfter := s.HookResponseTracker.EffectiveRequeueAfter()
238+
if requeueAfter != 0 {
239+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
240+
}
241+
219242
return ctrl.Result{}, nil
220243
}
221244

@@ -240,6 +263,25 @@ func (r *Reconciler) setupDynamicWatches(ctx context.Context, s *scope.Scope) er
240263
return nil
241264
}
242265

266+
func (r *Reconciler) callBeforeClusterCreateHook(ctx context.Context, s *scope.Scope) (reconcile.Result, error) {
267+
// If the cluster objects (InfraCluster, ControlPlane, etc) are not yet created we are in the creation phase.
268+
// Call the BeforeClusterCreate hook before proceeding.
269+
if s.Current.Cluster.Spec.InfrastructureRef == nil && s.Current.Cluster.Spec.ControlPlaneRef == nil {
270+
hookRequest := &runtimehooksv1.BeforeClusterCreateRequest{
271+
Cluster: *s.Current.Cluster,
272+
}
273+
hookResponse := &runtimehooksv1.BeforeClusterCreateResponse{}
274+
if err := r.RuntimeClient.CallAllExtensions(ctx, runtimehooksv1.BeforeClusterCreate, hookRequest, hookResponse); err != nil {
275+
return ctrl.Result{}, errors.Wrap(err, "error calling the BeforeClusterCreate hook")
276+
}
277+
s.HookResponseTracker.Add("BeforeClusterCreate", hookResponse)
278+
if hookResponse.RetryAfterSeconds != 0 {
279+
return ctrl.Result{RequeueAfter: time.Duration(hookResponse.RetryAfterSeconds) * time.Second}, nil
280+
}
281+
}
282+
return ctrl.Result{}, nil
283+
}
284+
243285
// clusterClassToCluster is a handler.ToRequestsFunc to be used to enqueue requests for reconciliation
244286
// for Cluster to update when its own ClusterClass gets updated.
245287
func (r *Reconciler) clusterClassToCluster(o client.Object) []ctrl.Request {

0 commit comments

Comments
 (0)