Skip to content

Commit 30a1ac3

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

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
// New creates a new Reconciler that reconciles custom resources that define a
@@ -470,6 +471,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
470471
}
471472
}
472473

474+
// WithPauseReconcileAnnotation is an Option that sets
475+
// a PauseReconcile annotation. If the Custom Resource watched by this
476+
// reconciler has the given annotation, and its value is set to `true`,
477+
// then reconciliation for this CR will not be performed until this annotation
478+
// is removed.
479+
func WithPauseReconcileAnnotation(annotationName string) Option {
480+
return func(r *Reconciler) error {
481+
r.pauseReconcileAnnotation = annotationName
482+
return nil
483+
}
484+
}
485+
473486
// WithPreHook is an Option that configures the reconciler to run the given
474487
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
475488
// or reconciliation).
@@ -662,6 +675,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
662675
}
663676
}()
664677

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

pkg/reconciler/reconciler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,13 @@ var _ = Describe("Reconciler", func() {
404404
}))
405405
})
406406
})
407+
_ = Describe("WithPauseReconcileAnnotation", func() {
408+
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
409+
a := "my.domain/pause-reconcile"
410+
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
411+
Expect(r.pauseReconcileAnnotation).To(Equal(a))
412+
})
413+
})
407414
_ = Describe("WithPreHook", func() {
408415
It("should set a reconciler prehook", func() {
409416
called := false
@@ -545,6 +552,7 @@ var _ = Describe("Reconciler", func() {
545552
WithInstallAnnotations(annotation.InstallDescription{}),
546553
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
547554
WithUninstallAnnotations(annotation.UninstallDescription{}),
555+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
548556
WithOverrideValues(map[string]string{
549557
"image.repository": "custom-nginx",
550558
}),
@@ -559,6 +567,7 @@ var _ = Describe("Reconciler", func() {
559567
WithInstallAnnotations(annotation.InstallDescription{}),
560568
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
561569
WithUninstallAnnotations(annotation.UninstallDescription{}),
570+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
562571
WithOverrideValues(map[string]string{
563572
"image.repository": "custom-nginx",
564573
}),
@@ -1429,6 +1438,64 @@ var _ = Describe("Reconciler", func() {
14291438
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
14301439
})
14311440

1441+
By("ensuring the finalizer is removed and the CR is deleted", func() {
1442+
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
1443+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1444+
})
1445+
})
1446+
})
1447+
When("pause-reconcile annotation is present", func() {
1448+
It("pauses reconciliation", func() {
1449+
By("adding the pause-reconcile annotation to the CR", func() {
1450+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1451+
obj.SetAnnotations(map[string]string{"my.domain/pause-reconcile": "true"})
1452+
obj.Object["spec"] = map[string]interface{}{"replicaCount": "666"}
1453+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1454+
})
1455+
1456+
By("deleting the CR", func() {
1457+
Expect(mgr.GetClient().Delete(ctx, obj)).To(Succeed())
1458+
})
1459+
1460+
By("successfully reconciling a request when paused", func() {
1461+
res, err := r.Reconcile(ctx, req)
1462+
Expect(res).To(Equal(reconcile.Result{}))
1463+
Expect(err).ToNot(HaveOccurred())
1464+
})
1465+
1466+
By("getting the CR", func() {
1467+
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
1468+
})
1469+
1470+
By("verifying the CR status is Paused", func() {
1471+
objStat := &objStatus{}
1472+
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
1473+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypePaused)).To(BeTrue())
1474+
})
1475+
1476+
By("verifying the release has not changed", func() {
1477+
rel, err := ac.Get(obj.GetName())
1478+
Expect(err).ToNot(HaveOccurred())
1479+
Expect(rel).NotTo(BeNil())
1480+
Expect(*rel).To(Equal(*currentRelease))
1481+
})
1482+
1483+
By("removing the pause-reconcile annotation from the CR", func() {
1484+
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
1485+
obj.SetAnnotations(nil)
1486+
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
1487+
})
1488+
1489+
By("successfully reconciling a request", func() {
1490+
res, err := r.Reconcile(ctx, req)
1491+
Expect(res).To(Equal(reconcile.Result{}))
1492+
Expect(err).ToNot(HaveOccurred())
1493+
})
1494+
1495+
By("verifying the release is uninstalled", func() {
1496+
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
1497+
})
1498+
14321499
By("ensuring the finalizer is removed and the CR is deleted", func() {
14331500
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
14341501
Expect(apierrors.IsNotFound(err)).To(BeTrue())

0 commit comments

Comments
 (0)