Skip to content

Commit 1c07dc0

Browse files
vladbologaporridge
andcommitted
ROX-28223: Pause-reconcile annotation
Co-authored-by: Marcin Owsiany <[email protected]>
1 parent 6261f25 commit 1c07dc0

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")
@@ -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/internal/updater/updater.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
146146
}
147147
}
148148

149+
func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
150+
return func(status *helmAppStatus) bool {
151+
return status.Conditions.RemoveCondition(t)
152+
}
153+
}
154+
149155
func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
150156
return func(status *helmAppStatus) bool {
151157
newRel := helmAppReleaseFor(rel)

pkg/reconciler/reconciler.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ type Reconciler struct {
8484
skipPrimaryGVKSchemeRegistration bool
8585
controllerSetupFuncs []ControllerSetupFunc
8686

87-
annotSetupOnce sync.Once
88-
annotations map[string]struct{}
89-
installAnnotations map[string]annotation.Install
90-
upgradeAnnotations map[string]annotation.Upgrade
91-
uninstallAnnotations map[string]annotation.Uninstall
87+
annotSetupOnce sync.Once
88+
annotations map[string]struct{}
89+
installAnnotations map[string]annotation.Install
90+
upgradeAnnotations map[string]annotation.Upgrade
91+
uninstallAnnotations map[string]annotation.Uninstall
92+
pauseReconcileAnnotation string
9293
}
9394

9495
// New creates a new Reconciler that reconciles custom resources that define a
@@ -439,6 +440,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
439440
}
440441
}
441442

443+
// WithPauseReconcileAnnotation is an Option that sets
444+
// a PauseReconcile annotation. If the Custom Resource watched by this
445+
// reconciler has the given annotation, and its value is set to `true`,
446+
// then reconciliation for this CR will not be performed until this annotation
447+
// is removed.
448+
func WithPauseReconcileAnnotation(annotationName string) Option {
449+
return func(r *Reconciler) error {
450+
r.pauseReconcileAnnotation = annotationName
451+
return nil
452+
}
453+
}
454+
442455
// WithPreHook is an Option that configures the reconciler to run the given
443456
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
444457
// or reconciliation).
@@ -591,6 +604,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
591604
}
592605
}()
593606

607+
if r.pauseReconcileAnnotation != "" {
608+
if v, ok := obj.GetAnnotations()[r.pauseReconcileAnnotation]; ok {
609+
if v == "true" {
610+
log.Info(fmt.Sprintf("Resource has '%s' annotation set to 'true', reconcile paused.", r.pauseReconcileAnnotation))
611+
u.UpdateStatus(
612+
updater.EnsureCondition(conditions.Paused(corev1.ConditionTrue, conditions.ReasonPauseReconcileAnnotationTrue, "")),
613+
updater.EnsureConditionUnknown(conditions.TypeIrreconcilable),
614+
updater.EnsureConditionUnknown(conditions.TypeDeployed),
615+
updater.EnsureConditionUnknown(conditions.TypeInitialized),
616+
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
617+
updater.EnsureDeployedRelease(nil),
618+
)
619+
return ctrl.Result{}, nil
620+
}
621+
}
622+
}
623+
624+
u.UpdateStatus(
625+
// TODO(ROX-12637): change to updater.EnsureCondition(conditions.Paused(corev1.ConditionFalse, "", "")))
626+
// once stackrox operator with pause support is released.
627+
// At that time also add `Paused` to the list of conditions expected in stackrox operator e2e tests.
628+
// Otherwise, the number of conditions in the `status.conditions` list will vary depending on the version
629+
// of used operator, which is cumbersome due to https://github.com/kudobuilder/kuttl/issues/76
630+
updater.EnsureConditionAbsent(conditions.TypePaused))
631+
594632
actionClient, err := r.actionClientGetter.ActionClientFor(ctx, obj)
595633
if err != nil {
596634
u.UpdateStatus(

pkg/reconciler/reconciler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ var _ = Describe("Reconciler", func() {
394394
}))
395395
})
396396
})
397+
_ = Describe("WithPauseReconcileAnnotation", func() {
398+
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
399+
a := "my.domain/pause-reconcile"
400+
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
401+
Expect(r.pauseReconcileAnnotation).To(Equal(a))
402+
})
403+
})
397404
_ = Describe("WithPreHook", func() {
398405
It("should set a reconciler prehook", func() {
399406
called := false
@@ -535,6 +542,7 @@ var _ = Describe("Reconciler", func() {
535542
WithInstallAnnotations(annotation.InstallDescription{}),
536543
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
537544
WithUninstallAnnotations(annotation.UninstallDescription{}),
545+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
538546
WithOverrideValues(map[string]string{
539547
"image.repository": "custom-nginx",
540548
}),
@@ -549,6 +557,7 @@ var _ = Describe("Reconciler", func() {
549557
WithInstallAnnotations(annotation.InstallDescription{}),
550558
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
551559
WithUninstallAnnotations(annotation.UninstallDescription{}),
560+
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
552561
WithOverrideValues(map[string]string{
553562
"image.repository": "custom-nginx",
554563
}),
@@ -1419,6 +1428,64 @@ var _ = Describe("Reconciler", func() {
14191428
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
14201429
})
14211430

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

0 commit comments

Comments
 (0)