Skip to content

Commit 21768c8

Browse files
vladbologaporridgePer Goncalves da Silva
authored
ROX-28223: Pause-reconcile handler (#426)
* ROX-28223: Pause-reconcile annotation Co-authored-by: Marcin Owsiany <porridge@redhat.com> * Remove Stackrox specific workaround * Pause reconcile handler * Fix panic & tests * Code review suggestions * Fix lint issues Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com> --------- Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com> Co-authored-by: Marcin Owsiany <porridge@redhat.com> Co-authored-by: Per Goncalves da Silva <pegoncal@redhat.com>
1 parent d72505f commit 21768c8

File tree

3 files changed

+122
-3
lines changed

3 files changed

+122
-3
lines changed

pkg/reconciler/internal/conditions/conditions.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ const (
2929
TypeDeployed = "Deployed"
3030
TypeReleaseFailed = "ReleaseFailed"
3131
TypeIrreconcilable = "Irreconcilable"
32+
TypePaused = "Paused"
3233

33-
ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
34-
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
35-
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
34+
ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
35+
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
36+
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
37+
ReasonPauseReconcileAnnotationTrue = status.ConditionReason("PauseReconcileAnnotationTrue")
3638

3739
ReasonErrorGettingClient = status.ConditionReason("ErrorGettingClient")
3840
ReasonErrorGettingValues = status.ConditionReason("ErrorGettingValues")
@@ -59,6 +61,10 @@ func Irreconcilable(stat corev1.ConditionStatus, reason status.ConditionReason,
5961
return newCondition(TypeIrreconcilable, stat, reason, message)
6062
}
6163

64+
func Paused(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
65+
return newCondition(TypePaused, stat, reason, message)
66+
}
67+
6268
func newCondition(t status.ConditionType, s corev1.ConditionStatus, r status.ConditionReason, m interface{}) status.Condition {
6369
message := fmt.Sprintf("%s", m)
6470
return status.Condition{

pkg/reconciler/reconciler.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type Reconciler struct {
8383
maxReleaseHistory *int
8484
skipPrimaryGVKSchemeRegistration bool
8585
controllerSetupFuncs []ControllerSetupFunc
86+
pauseHandler PauseReconcileHandlerFunc
8687

8788
annotSetupOnce sync.Once
8889
annotations map[string]struct{}
@@ -439,6 +440,33 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
439440
}
440441
}
441442

443+
// PauseReconcileHandlerFunc defines a function type that determines whether reconciliation should be paused
444+
// for a given custom resource
445+
type PauseReconcileHandlerFunc func(ctx context.Context, obj *unstructured.Unstructured) (bool, error)
446+
447+
// WithPauseReconcileHandler is an Option that sets a PauseReconcile handler, which is a function that
448+
// determines whether reconciliation should be paused for the custom resource watched by this reconciler.
449+
//
450+
// Example usage: WithPauseReconcileHandler(PauseReconcileIfAnnotationTrue("my.domain/pause-reconcile"))
451+
func WithPauseReconcileHandler(handler PauseReconcileHandlerFunc) Option {
452+
return func(r *Reconciler) error {
453+
r.pauseHandler = handler
454+
return nil
455+
}
456+
}
457+
458+
// PauseReconcileIfAnnotationTrue returns a PauseReconcileHandlerFunc that pauses reconciliation if the given
459+
// annotation is present and set to "true"
460+
func PauseReconcileIfAnnotationTrue(annotationName string) PauseReconcileHandlerFunc {
461+
return func(_ context.Context, obj *unstructured.Unstructured) (bool, error) {
462+
if v, ok := obj.GetAnnotations()[annotationName]; ok && v == "true" {
463+
return true, nil
464+
}
465+
466+
return false, nil
467+
}
468+
}
469+
442470
// WithPreHook is an Option that configures the reconciler to run the given
443471
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
444472
// or reconciliation).
@@ -591,6 +619,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
591619
}
592620
}()
593621

622+
if r.pauseHandler != nil {
623+
paused, err := r.pauseHandler(ctx, obj)
624+
if err != nil {
625+
log.Error(err, "pause reconcile handler failed")
626+
}
627+
628+
if paused {
629+
log.Info("Reconcile is paused for this resource.")
630+
u.UpdateStatus(
631+
updater.EnsureCondition(conditions.Paused(corev1.ConditionTrue, conditions.ReasonPauseReconcileAnnotationTrue, "")),
632+
updater.EnsureConditionUnknown(conditions.TypeIrreconcilable),
633+
updater.EnsureConditionUnknown(conditions.TypeDeployed),
634+
updater.EnsureConditionUnknown(conditions.TypeInitialized),
635+
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
636+
updater.EnsureDeployedRelease(nil),
637+
)
638+
return ctrl.Result{}, nil
639+
}
640+
}
641+
642+
u.UpdateStatus(updater.EnsureCondition(conditions.Paused(corev1.ConditionFalse, "", "")))
643+
594644
actionClient, err := r.actionClientGetter.ActionClientFor(ctx, obj)
595645
if err != nil {
596646
u.UpdateStatus(

pkg/reconciler/reconciler_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,69 @@ var _ = Describe("Reconciler", func() {
14191419
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
14201420
})
14211421

1422+
By("ensuring the finalizer is removed and the CR is deleted", func() {
1423+
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
1424+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1425+
})
1426+
})
1427+
})
1428+
When("pause-reconcile annotation is present", func() {
1429+
It("pauses reconciliation", func() {
1430+
By("adding a pause-reconcile handler to the Reconciler", func() {
1431+
pauseHandler := WithPauseReconcileHandler(PauseReconcileIfAnnotationTrue("my.domain/pause-reconcile"))
1432+
Expect(pauseHandler(r)).To(Succeed())
1433+
})
1434+
1435+
By("adding the pause-reconcile annotation to the CR", func() {
1436+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1437+
obj.SetAnnotations(map[string]string{"my.domain/pause-reconcile": "true"})
1438+
obj.Object["spec"] = map[string]interface{}{"replicaCount": "666"}
1439+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1440+
})
1441+
1442+
By("deleting the CR", func() {
1443+
Expect(mgr.GetClient().Delete(ctx, obj)).To(Succeed())
1444+
})
1445+
1446+
By("successfully reconciling a request when paused", func() {
1447+
res, err := r.Reconcile(ctx, req)
1448+
Expect(res).To(Equal(reconcile.Result{}))
1449+
Expect(err).ToNot(HaveOccurred())
1450+
})
1451+
1452+
By("getting the CR", func() {
1453+
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
1454+
})
1455+
1456+
By("verifying the CR status is Paused", func() {
1457+
objStat := &objStatus{}
1458+
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
1459+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypePaused)).To(BeTrue())
1460+
})
1461+
1462+
By("verifying the release has not changed", func() {
1463+
rel, err := ac.Get(obj.GetName())
1464+
Expect(err).ToNot(HaveOccurred())
1465+
Expect(rel).NotTo(BeNil())
1466+
Expect(*rel).To(Equal(*currentRelease))
1467+
})
1468+
1469+
By("removing the pause-reconcile annotation from the CR", func() {
1470+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1471+
obj.SetAnnotations(nil)
1472+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1473+
})
1474+
1475+
By("successfully reconciling a request", func() {
1476+
res, err := r.Reconcile(ctx, req)
1477+
Expect(res).To(Equal(reconcile.Result{}))
1478+
Expect(err).ToNot(HaveOccurred())
1479+
})
1480+
1481+
By("verifying the release is uninstalled", func() {
1482+
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
1483+
})
1484+
14221485
By("ensuring the finalizer is removed and the CR is deleted", func() {
14231486
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
14241487
Expect(apierrors.IsNotFound(err)).To(BeTrue())

0 commit comments

Comments
 (0)