Skip to content

Commit 650e85f

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

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
@@ -160,6 +160,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
160160
}
161161
}
162162

163+
func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
164+
return func(status *helmAppStatus) bool {
165+
return status.Conditions.RemoveCondition(t)
166+
}
167+
}
168+
163169
func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
164170
return func(status *helmAppStatus) bool {
165171
newRel := helmAppReleaseFor(rel)

pkg/reconciler/reconciler.go

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

9292
stripManifestFromStatus bool
9393

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

101102
type watchDescription struct {
@@ -459,6 +460,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
459460
}
460461
}
461462

463+
// WithPauseReconcileAnnotation is an Option that sets
464+
// a PauseReconcile annotation. If the Custom Resource watched by this
465+
// reconciler has the given annotation, and its value is set to `true`,
466+
// then reconciliation for this CR will not be performed until this annotation
467+
// is removed.
468+
func WithPauseReconcileAnnotation(annotationName string) Option {
469+
return func(r *Reconciler) error {
470+
r.pauseReconcileAnnotation = annotationName
471+
return nil
472+
}
473+
}
474+
462475
// WithPreHook is an Option that configures the reconciler to run the given
463476
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
464477
// or reconciliation).
@@ -657,6 +670,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
657670
}
658671
}()
659672

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

pkg/reconciler/reconciler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,13 @@ var _ = Describe("Reconciler", func() {
385385
}))
386386
})
387387
})
388+
var _ = Describe("WithPauseReconcileAnnotation", func() {
389+
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
390+
a := "my.domain/pause-reconcile"
391+
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
392+
Expect(r.pauseReconcileAnnotation).To(Equal(a))
393+
})
394+
})
388395
var _ = Describe("WithPreHook", func() {
389396
It("should set a reconciler prehook", func() {
390397
called := false
@@ -527,6 +534,7 @@ var _ = Describe("Reconciler", func() {
527534
WithInstallAnnotations(annotation.InstallDescription{}),
528535
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
529536
WithUninstallAnnotations(annotation.UninstallDescription{}),
537+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
530538
WithOverrideValues(map[string]string{
531539
"image.repository": "custom-nginx",
532540
}),
@@ -541,6 +549,7 @@ var _ = Describe("Reconciler", func() {
541549
WithInstallAnnotations(annotation.InstallDescription{}),
542550
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
543551
WithUninstallAnnotations(annotation.UninstallDescription{}),
552+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
544553
WithOverrideValues(map[string]string{
545554
"image.repository": "custom-nginx",
546555
}),
@@ -1319,6 +1328,64 @@ var _ = Describe("Reconciler", func() {
13191328
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
13201329
})
13211330

1331+
By("ensuring the finalizer is removed and the CR is deleted", func() {
1332+
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
1333+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1334+
})
1335+
})
1336+
})
1337+
When("pause-reconcile annotation is present", func() {
1338+
It("pauses reconciliation", func() {
1339+
By("adding the pause-reconcile annotation to the CR", func() {
1340+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1341+
obj.SetAnnotations(map[string]string{"my.domain/pause-reconcile": "true"})
1342+
obj.Object["spec"] = map[string]interface{}{"replicaCount": "666"}
1343+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1344+
})
1345+
1346+
By("deleting the CR", func() {
1347+
Expect(mgr.GetClient().Delete(ctx, obj)).To(Succeed())
1348+
})
1349+
1350+
By("successfully reconciling a request when paused", func() {
1351+
res, err := r.Reconcile(ctx, req)
1352+
Expect(res).To(Equal(reconcile.Result{}))
1353+
Expect(err).To(BeNil())
1354+
})
1355+
1356+
By("getting the CR", func() {
1357+
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
1358+
})
1359+
1360+
By("verifying the CR status is Paused", func() {
1361+
objStat := &objStatus{}
1362+
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
1363+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypePaused)).To(BeTrue())
1364+
})
1365+
1366+
By("verifying the release has not changed", func() {
1367+
rel, err := ac.Get(obj.GetName())
1368+
Expect(err).To(BeNil())
1369+
Expect(rel).NotTo(BeNil())
1370+
Expect(*rel).To(Equal(*currentRelease))
1371+
})
1372+
1373+
By("removing the pause-reconcile annotation from the CR", func() {
1374+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1375+
obj.SetAnnotations(nil)
1376+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1377+
})
1378+
1379+
By("successfully reconciling a request", func() {
1380+
res, err := r.Reconcile(ctx, req)
1381+
Expect(res).To(Equal(reconcile.Result{}))
1382+
Expect(err).To(BeNil())
1383+
})
1384+
1385+
By("verifying the release is uninstalled", func() {
1386+
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
1387+
})
1388+
13221389
By("ensuring the finalizer is removed and the CR is deleted", func() {
13231390
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
13241391
Expect(apierrors.IsNotFound(err)).To(BeTrue())

0 commit comments

Comments
 (0)