Skip to content

Commit fbf7122

Browse files
vladbologaporridge
authored andcommitted
ROX-12219: Add support for pause-reconcile annotation (#29)
Co-authored-by: Marcin Owsiany <porridge@redhat.com>
1 parent 5a71a3e commit fbf7122

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
@@ -142,6 +142,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
142142
}
143143
}
144144

145+
func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
146+
return func(status *helmAppStatus) bool {
147+
return status.Conditions.RemoveCondition(t)
148+
}
149+
}
150+
145151
func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
146152
return func(status *helmAppStatus) bool {
147153
newRel := helmAppReleaseFor(rel)

pkg/reconciler/reconciler.go

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

9090
stripManifestFromStatus bool
9191

92-
annotSetupOnce sync.Once
93-
annotations map[string]struct{}
94-
installAnnotations map[string]annotation.Install
95-
upgradeAnnotations map[string]annotation.Upgrade
96-
uninstallAnnotations map[string]annotation.Uninstall
92+
annotSetupOnce sync.Once
93+
annotations map[string]struct{}
94+
installAnnotations map[string]annotation.Install
95+
upgradeAnnotations map[string]annotation.Upgrade
96+
uninstallAnnotations map[string]annotation.Uninstall
97+
pauseReconcileAnnotation string
9798
}
9899

99100
type watchDescription struct {
@@ -450,6 +451,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
450451
}
451452
}
452453

454+
// WithPauseReconcileAnnotation is an Option that sets
455+
// a PauseReconcile annotation. If the Custom Resource watched by this
456+
// reconciler has the given annotation, and its value is set to `true`,
457+
// then reconciliation for this CR will not be performed until this annotation
458+
// is removed.
459+
func WithPauseReconcileAnnotation(annotationName string) Option {
460+
return func(r *Reconciler) error {
461+
r.pauseReconcileAnnotation = annotationName
462+
return nil
463+
}
464+
}
465+
453466
// WithPreHook is an Option that configures the reconciler to run the given
454467
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
455468
// or reconciliation).
@@ -612,6 +625,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
612625
}
613626
}()
614627

628+
if r.pauseReconcileAnnotation != "" {
629+
if v, ok := obj.GetAnnotations()[r.pauseReconcileAnnotation]; ok {
630+
if v == "true" {
631+
log.Info(fmt.Sprintf("Resource has '%s' annotation set to 'true', reconcile paused.", r.pauseReconcileAnnotation))
632+
u.UpdateStatus(
633+
updater.EnsureCondition(conditions.Paused(corev1.ConditionTrue, conditions.ReasonPauseReconcileAnnotationTrue, "")),
634+
updater.EnsureConditionUnknown(conditions.TypeIrreconcilable),
635+
updater.EnsureConditionUnknown(conditions.TypeDeployed),
636+
updater.EnsureConditionUnknown(conditions.TypeInitialized),
637+
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
638+
updater.EnsureDeployedRelease(nil),
639+
)
640+
return ctrl.Result{}, nil
641+
}
642+
}
643+
}
644+
645+
u.UpdateStatus(
646+
// TODO(ROX-12637): change to updater.EnsureCondition(conditions.Paused(corev1.ConditionFalse, "", "")))
647+
// once stackrox operator with pause support is released.
648+
// At that time also add `Paused` to the list of conditions expected in stackrox operator e2e tests.
649+
// Otherwise, the number of conditions in the `status.conditions` list will vary depending on the version
650+
// of used operator, which is cumbersome due to https://github.com/kudobuilder/kuttl/issues/76
651+
updater.EnsureConditionAbsent(conditions.TypePaused))
652+
615653
actionClient, err := r.actionClientGetter.ActionClientFor(obj)
616654
if err != nil {
617655
u.UpdateStatus(

pkg/reconciler/reconciler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,13 @@ var _ = Describe("Reconciler", func() {
382382
}))
383383
})
384384
})
385+
var _ = Describe("WithPauseReconcileAnnotation", func() {
386+
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
387+
a := "my.domain/pause-reconcile"
388+
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
389+
Expect(r.pauseReconcileAnnotation).To(Equal(a))
390+
})
391+
})
385392
var _ = Describe("WithPreHook", func() {
386393
It("should set a reconciler prehook", func() {
387394
called := false
@@ -524,6 +531,7 @@ var _ = Describe("Reconciler", func() {
524531
WithInstallAnnotations(annotation.InstallDescription{}),
525532
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
526533
WithUninstallAnnotations(annotation.UninstallDescription{}),
534+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
527535
WithOverrideValues(map[string]string{
528536
"image.repository": "custom-nginx",
529537
}),
@@ -538,6 +546,7 @@ var _ = Describe("Reconciler", func() {
538546
WithInstallAnnotations(annotation.InstallDescription{}),
539547
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
540548
WithUninstallAnnotations(annotation.UninstallDescription{}),
549+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
541550
WithOverrideValues(map[string]string{
542551
"image.repository": "custom-nginx",
543552
}),
@@ -1316,6 +1325,64 @@ var _ = Describe("Reconciler", func() {
13161325
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
13171326
})
13181327

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

0 commit comments

Comments
 (0)