Skip to content

Commit 10aa91d

Browse files
vladbologaporridge
andcommitted
ROX-12219: Add support for pause-reconcile annotation (#29)
Co-authored-by: Marcin Owsiany <[email protected]>
1 parent 1e94c72 commit 10aa91d

File tree

4 files changed

+125
-8
lines changed

4 files changed

+125
-8
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")
@@ -60,6 +62,10 @@ func Irreconcilable(stat corev1.ConditionStatus, reason status.ConditionReason,
6062
return newCondition(TypeIrreconcilable, stat, reason, message)
6163
}
6264

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

pkg/reconciler/internal/updater/updater.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
158158
}
159159
}
160160

161+
func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
162+
return func(status *helmAppStatus) bool {
163+
return status.Conditions.RemoveCondition(t)
164+
}
165+
}
166+
161167
func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
162168
return func(status *helmAppStatus) bool {
163169
newRel := helmAppReleaseFor(rel)

pkg/reconciler/reconciler.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,12 @@ type Reconciler struct {
9292

9393
stripManifestFromStatus bool
9494

95-
annotSetupOnce sync.Once
96-
annotations map[string]struct{}
97-
installAnnotations map[string]annotation.Install
98-
upgradeAnnotations map[string]annotation.Upgrade
99-
uninstallAnnotations map[string]annotation.Uninstall
95+
annotSetupOnce sync.Once
96+
annotations map[string]struct{}
97+
installAnnotations map[string]annotation.Install
98+
upgradeAnnotations map[string]annotation.Upgrade
99+
uninstallAnnotations map[string]annotation.Uninstall
100+
pauseReconcileAnnotation string
100101
}
101102

102103
type watchDescription struct {
@@ -476,6 +477,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
476477
}
477478
}
478479

480+
// WithPauseReconcileAnnotation is an Option that sets
481+
// a PauseReconcile annotation. If the Custom Resource watched by this
482+
// reconciler has the given annotation, and its value is set to `true`,
483+
// then reconciliation for this CR will not be performed until this annotation
484+
// is removed.
485+
func WithPauseReconcileAnnotation(annotationName string) Option {
486+
return func(r *Reconciler) error {
487+
r.pauseReconcileAnnotation = annotationName
488+
return nil
489+
}
490+
}
491+
479492
// WithPreHook is an Option that configures the reconciler to run the given
480493
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
481494
// or reconciliation).
@@ -668,6 +681,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
668681
}
669682
}()
670683

684+
if r.pauseReconcileAnnotation != "" {
685+
if v, ok := obj.GetAnnotations()[r.pauseReconcileAnnotation]; ok {
686+
if v == "true" {
687+
log.Info(fmt.Sprintf("Resource has '%s' annotation set to 'true', reconcile paused.", r.pauseReconcileAnnotation))
688+
u.UpdateStatus(
689+
updater.EnsureCondition(conditions.Paused(corev1.ConditionTrue, conditions.ReasonPauseReconcileAnnotationTrue, "")),
690+
updater.EnsureConditionUnknown(conditions.TypeIrreconcilable),
691+
updater.EnsureConditionUnknown(conditions.TypeDeployed),
692+
updater.EnsureConditionUnknown(conditions.TypeInitialized),
693+
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
694+
updater.EnsureDeployedRelease(nil),
695+
)
696+
return ctrl.Result{}, nil
697+
}
698+
}
699+
}
700+
701+
u.UpdateStatus(
702+
// TODO(ROX-12637): change to updater.EnsureCondition(conditions.Paused(corev1.ConditionFalse, "", "")))
703+
// once stackrox operator with pause support is released.
704+
// At that time also add `Paused` to the list of conditions expected in stackrox operator e2e tests.
705+
// Otherwise, the number of conditions in the `status.conditions` list will vary depending on the version
706+
// of used operator, which is cumbersome due to https://github.com/kudobuilder/kuttl/issues/76
707+
updater.EnsureConditionAbsent(conditions.TypePaused))
708+
671709
actionClient, err := r.actionClientGetter.ActionClientFor(ctx, obj)
672710
if err != nil {
673711
u.UpdateStatus(

pkg/reconciler/reconciler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,13 @@ var _ = Describe("Reconciler", func() {
402402
}))
403403
})
404404
})
405+
_ = Describe("WithPauseReconcileAnnotation", func() {
406+
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
407+
a := "my.domain/pause-reconcile"
408+
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
409+
Expect(r.pauseReconcileAnnotation).To(Equal(a))
410+
})
411+
})
405412
_ = Describe("WithPreHook", func() {
406413
It("should set a reconciler prehook", func() {
407414
called := false
@@ -543,6 +550,7 @@ var _ = Describe("Reconciler", func() {
543550
WithInstallAnnotations(annotation.InstallDescription{}),
544551
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
545552
WithUninstallAnnotations(annotation.UninstallDescription{}),
553+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
546554
WithOverrideValues(map[string]string{
547555
"image.repository": "custom-nginx",
548556
}),
@@ -557,6 +565,7 @@ var _ = Describe("Reconciler", func() {
557565
WithInstallAnnotations(annotation.InstallDescription{}),
558566
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
559567
WithUninstallAnnotations(annotation.UninstallDescription{}),
568+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
560569
WithOverrideValues(map[string]string{
561570
"image.repository": "custom-nginx",
562571
}),
@@ -1382,6 +1391,64 @@ var _ = Describe("Reconciler", func() {
13821391
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
13831392
})
13841393

1394+
By("ensuring the finalizer is removed and the CR is deleted", func() {
1395+
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
1396+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1397+
})
1398+
})
1399+
})
1400+
When("pause-reconcile annotation is present", func() {
1401+
It("pauses reconciliation", func() {
1402+
By("adding the pause-reconcile annotation to the CR", func() {
1403+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1404+
obj.SetAnnotations(map[string]string{"my.domain/pause-reconcile": "true"})
1405+
obj.Object["spec"] = map[string]interface{}{"replicaCount": "666"}
1406+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1407+
})
1408+
1409+
By("deleting the CR", func() {
1410+
Expect(mgr.GetClient().Delete(ctx, obj)).To(Succeed())
1411+
})
1412+
1413+
By("successfully reconciling a request when paused", func() {
1414+
res, err := r.Reconcile(ctx, req)
1415+
Expect(res).To(Equal(reconcile.Result{}))
1416+
Expect(err).To(BeNil())
1417+
})
1418+
1419+
By("getting the CR", func() {
1420+
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
1421+
})
1422+
1423+
By("verifying the CR status is Paused", func() {
1424+
objStat := &objStatus{}
1425+
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
1426+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypePaused)).To(BeTrue())
1427+
})
1428+
1429+
By("verifying the release has not changed", func() {
1430+
rel, err := ac.Get(obj.GetName())
1431+
Expect(err).To(BeNil())
1432+
Expect(rel).NotTo(BeNil())
1433+
Expect(*rel).To(Equal(*currentRelease))
1434+
})
1435+
1436+
By("removing the pause-reconcile annotation from the CR", func() {
1437+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1438+
obj.SetAnnotations(nil)
1439+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1440+
})
1441+
1442+
By("successfully reconciling a request", func() {
1443+
res, err := r.Reconcile(ctx, req)
1444+
Expect(res).To(Equal(reconcile.Result{}))
1445+
Expect(err).To(BeNil())
1446+
})
1447+
1448+
By("verifying the release is uninstalled", func() {
1449+
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
1450+
})
1451+
13851452
By("ensuring the finalizer is removed and the CR is deleted", func() {
13861453
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
13871454
Expect(apierrors.IsNotFound(err)).To(BeTrue())

0 commit comments

Comments
 (0)