Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -88,11 +88,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 @@ -449,6 +450,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 @@ -611,6 +624,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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By("getting the CR", func() {
By("getting the CR verify the reconciler is paused", func() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I cannot parse your suggestion.

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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By("verifying the release is uninstalled", func() {
By("verifying reconciliation unpaused and the release is uninstalled", func() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we don't actually check the status here, so I'd leave this as is...

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