Skip to content

Commit

Permalink
ROX-12219: Add support for pause-reconcile annotation (#29)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcin Owsiany <porridge@redhat.com>
  • Loading branch information
2 people authored and ludydoo committed Jun 27, 2023
1 parent cf795db commit 5d4fc78
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 8 deletions.
12 changes: 9 additions & 3 deletions pkg/reconciler/internal/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ const (
TypeDeployed = "Deployed"
TypeReleaseFailed = "ReleaseFailed"
TypeIrreconcilable = "Irreconcilable"
TypePaused = "Paused"

ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
ReasonPauseReconcileAnnotationTrue = status.ConditionReason("PauseReconcileAnnotationTrue")

ReasonErrorGettingClient = status.ConditionReason("ErrorGettingClient")
ReasonErrorGettingValues = status.ConditionReason("ErrorGettingValues")
Expand Down Expand Up @@ -60,6 +62,10 @@ func Irreconcilable(stat corev1.ConditionStatus, reason status.ConditionReason,
return newCondition(TypeIrreconcilable, stat, reason, message)
}

func Paused(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypePaused, stat, reason, message)
}

func newCondition(t status.ConditionType, s corev1.ConditionStatus, r status.ConditionReason, m interface{}) status.Condition {
message := fmt.Sprintf("%s", m)
return status.Condition{
Expand Down
6 changes: 6 additions & 0 deletions pkg/reconciler/internal/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
}
}

func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
return status.Conditions.RemoveCondition(t)
}
}

func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
newRel := helmAppReleaseFor(rel)
Expand Down
48 changes: 43 additions & 5 deletions pkg/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ type Reconciler struct {

stripManifestFromStatus bool

annotSetupOnce sync.Once
annotations map[string]struct{}
installAnnotations map[string]annotation.Install
upgradeAnnotations map[string]annotation.Upgrade
uninstallAnnotations map[string]annotation.Uninstall
annotSetupOnce sync.Once
annotations map[string]struct{}
installAnnotations map[string]annotation.Install
upgradeAnnotations map[string]annotation.Upgrade
uninstallAnnotations map[string]annotation.Uninstall
pauseReconcileAnnotation string
}

type watchDescription struct {
Expand Down Expand Up @@ -450,6 +451,18 @@ func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
}
}

// WithPauseReconcileAnnotation is an Option that sets
// a PauseReconcile annotation. If the Custom Resource watched by this
// reconciler has the given annotation, and its value is set to `true`,
// then reconciliation for this CR will not be performed until this annotation
// is removed.
func WithPauseReconcileAnnotation(annotationName string) Option {
return func(r *Reconciler) error {
r.pauseReconcileAnnotation = annotationName
return nil
}
}

// WithPreHook is an Option that configures the reconciler to run the given
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
// or reconciliation).
Expand Down Expand Up @@ -612,6 +625,31 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
}
}()

if r.pauseReconcileAnnotation != "" {
if v, ok := obj.GetAnnotations()[r.pauseReconcileAnnotation]; ok {
if v == "true" {
log.Info(fmt.Sprintf("Resource has '%s' annotation set to 'true', reconcile paused.", r.pauseReconcileAnnotation))
u.UpdateStatus(
updater.EnsureCondition(conditions.Paused(corev1.ConditionTrue, conditions.ReasonPauseReconcileAnnotationTrue, "")),
updater.EnsureConditionUnknown(conditions.TypeIrreconcilable),
updater.EnsureConditionUnknown(conditions.TypeDeployed),
updater.EnsureConditionUnknown(conditions.TypeInitialized),
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
updater.EnsureDeployedRelease(nil),
)
return ctrl.Result{}, nil
}
}
}

u.UpdateStatus(
// TODO(ROX-12637): change to updater.EnsureCondition(conditions.Paused(corev1.ConditionFalse, "", "")))
// once stackrox operator with pause support is released.
// At that time also add `Paused` to the list of conditions expected in stackrox operator e2e tests.
// Otherwise, the number of conditions in the `status.conditions` list will vary depending on the version
// of used operator, which is cumbersome due to https://github.com/kudobuilder/kuttl/issues/76
updater.EnsureConditionAbsent(conditions.TypePaused))

actionClient, err := r.actionClientGetter.ActionClientFor(obj)
if err != nil {
u.UpdateStatus(
Expand Down
67 changes: 67 additions & 0 deletions pkg/reconciler/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@ var _ = Describe("Reconciler", func() {
}))
})
})
var _ = Describe("WithPauseReconcileAnnotation", func() {
It("should set the pauseReconcileAnnotation field to the annotation name", func() {
a := "my.domain/pause-reconcile"
Expect(WithPauseReconcileAnnotation(a)(r)).To(Succeed())
Expect(r.pauseReconcileAnnotation).To(Equal(a))
})
})
var _ = Describe("WithPreHook", func() {
It("should set a reconciler prehook", func() {
called := false
Expand Down Expand Up @@ -524,6 +531,7 @@ var _ = Describe("Reconciler", func() {
WithInstallAnnotations(annotation.InstallDescription{}),
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
WithUninstallAnnotations(annotation.UninstallDescription{}),
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
WithOverrideValues(map[string]string{
"image.repository": "custom-nginx",
}),
Expand All @@ -538,6 +546,7 @@ var _ = Describe("Reconciler", func() {
WithInstallAnnotations(annotation.InstallDescription{}),
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
WithUninstallAnnotations(annotation.UninstallDescription{}),
WithPauseReconcileAnnotation("my.domain/pause-reconcile"),
WithOverrideValues(map[string]string{
"image.repository": "custom-nginx",
}),
Expand Down Expand Up @@ -1316,6 +1325,64 @@ var _ = Describe("Reconciler", func() {
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
})

By("ensuring the finalizer is removed and the CR is deleted", func() {
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
})
})
When("pause-reconcile annotation is present", func() {
It("pauses reconciliation", func() {
By("adding the pause-reconcile annotation to the CR", func() {
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
obj.SetAnnotations(map[string]string{"my.domain/pause-reconcile": "true"})
obj.Object["spec"] = map[string]interface{}{"replicaCount": "666"}
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
})

By("deleting the CR", func() {
Expect(mgr.GetClient().Delete(ctx, obj)).To(Succeed())
})

By("successfully reconciling a request when paused", func() {
res, err := r.Reconcile(ctx, req)
Expect(res).To(Equal(reconcile.Result{}))
Expect(err).To(BeNil())
})

By("getting the CR", func() {
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
})

By("verifying the CR status is Paused", func() {
objStat := &objStatus{}
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypePaused)).To(BeTrue())
})

By("verifying the release has not changed", func() {
rel, err := ac.Get(obj.GetName())
Expect(err).To(BeNil())
Expect(rel).NotTo(BeNil())
Expect(*rel).To(Equal(*currentRelease))
})

By("removing the pause-reconcile annotation from the CR", func() {
Expect(mgr.GetClient().Get(ctx, objKey, obj)).To(Succeed())
obj.SetAnnotations(nil)
Expect(mgr.GetClient().Update(ctx, obj)).To(Succeed())
})

By("successfully reconciling a request", func() {
res, err := r.Reconcile(ctx, req)
Expect(res).To(Equal(reconcile.Result{}))
Expect(err).To(BeNil())
})

By("verifying the release is uninstalled", func() {
verifyNoRelease(ctx, mgr.GetClient(), obj.GetNamespace(), obj.GetName(), currentRelease)
})

By("ensuring the finalizer is removed and the CR is deleted", func() {
err := mgr.GetAPIReader().Get(ctx, objKey, obj)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
Expand Down

0 comments on commit 5d4fc78

Please sign in to comment.