Skip to content

Commit 8ceb4e0

Browse files
authored
Allow users to add annotation to set configure rollback (#6546)
* Allow users to add annotation to set configure rollback Allow users to add annotation, "helm.sdk.operatorframework.io/rollback-force", to set rollback to false. Default value is true Signed-off-by: Edmund Ochieng <[email protected]> * Update OPERATOR_SDK_VERSION in testdata Makefiles * Add tests for function reading helm.sdk.operatorframework.io/rollback-force annotation * Move rollback out of the UpgradeRelease method * Print error whenever release upgrade fails --------- Signed-off-by: Edmund Ochieng <[email protected]>
1 parent e67da35 commit 8ceb4e0

File tree

11 files changed

+175
-14
lines changed

11 files changed

+175
-14
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# entries is a list of entries to include in
2+
# release notes and/or the migration guide
3+
entries:
4+
- description: >
5+
For Helm-based operators, whenever the operator encounters an
6+
error during reconcilliation, it would attempt to rollback the
7+
changes with the `--force` option. This behavior could have
8+
undesired side effects in some scenario.
9+
10+
This change allows the users to change this behavior by adding the
11+
annotation, `helm.sdk.operatorframework.io/rollback-force: false`
12+
to the custom resource.
13+
14+
# kind is one of:
15+
# - addition
16+
# - change
17+
# - deprecation
18+
# - removal
19+
# - bugfix
20+
kind: "addition"
21+
22+
# Is this a breaking change?
23+
breaking: false
24+
25+
# NOTE: ONLY USE `pull_request_override` WHEN ADDING THIS
26+
# FILE FOR A PREVIOUSLY MERGED PULL_REQUEST!
27+
#
28+
# The generator auto-detects the PR number from the commit
29+
# message in which this file was originally added.
30+
#
31+
# What is the pull request number (without the "#")?
32+
# pull_request_override: 0
33+

internal/helm/controller/reconcile.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"strconv"
22+
"strings"
2223
"time"
2324

2425
rpb "helm.sh/helm/v3/pkg/release"
@@ -63,6 +64,7 @@ const (
6364
uninstallFinalizerLegacy = "uninstall-helm-release"
6465

6566
helmUpgradeForceAnnotation = "helm.sdk.operatorframework.io/upgrade-force"
67+
helmRollbackForceAnnotation = "helm.sdk.operatorframework.io/rollback-force"
6668
helmUninstallWaitAnnotation = "helm.sdk.operatorframework.io/uninstall-wait"
6769
helmReconcilePeriodAnnotation = "helm.sdk.operatorframework.io/reconcile-period"
6870
)
@@ -311,8 +313,18 @@ func (r HelmOperatorReconciler) Reconcile(ctx context.Context, request reconcile
311313
"Chart value %q overridden to %q by operator's watches.yaml", k, v)
312314
}
313315
force := hasAnnotation(helmUpgradeForceAnnotation, o)
316+
314317
previousRelease, upgradedRelease, err := manager.UpgradeRelease(ctx, release.ForceUpgrade(force))
315318
if err != nil {
319+
if errors.Is(err, release.ErrUpgradeFailed) {
320+
// the forceRollback variable takes the value of the annotation,
321+
// "helm.sdk.operatorframework.io/rollback-force".
322+
// The default value for the annotation is true
323+
forceRollback := readBoolAnnotationWithDefault(o, helmRollbackForceAnnotation, true)
324+
if err := manager.RollBack(ctx, release.ForceRollback(forceRollback)); err != nil {
325+
log.Error(err, "Error rolling back release")
326+
}
327+
}
316328
log.Error(err, "Release failed")
317329
status.SetCondition(types.HelmAppCondition{
318330
Type: types.ConditionReleaseFailed,
@@ -446,6 +458,21 @@ func hasAnnotation(anno string, o *unstructured.Unstructured) bool {
446458
return value
447459
}
448460

461+
func readBoolAnnotationWithDefault(obj *unstructured.Unstructured, annotation string, fallback bool) bool {
462+
val, ok := obj.GetAnnotations()[annotation]
463+
if !ok {
464+
return fallback
465+
}
466+
r, err := strconv.ParseBool(val)
467+
if err != nil {
468+
log.Error(
469+
fmt.Errorf(strings.ToLower(err.Error())), "error parsing annotation", "annotation", annotation)
470+
return fallback
471+
}
472+
473+
return r
474+
}
475+
449476
func (r HelmOperatorReconciler) updateResource(ctx context.Context, o client.Object) error {
450477
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
451478
return r.Client.Update(ctx, o)

internal/helm/controller/reconcile_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,64 @@ func annotations(m map[string]interface{}) *unstructured.Unstructured {
190190
},
191191
}
192192
}
193+
194+
func Test_readBoolAnnotationWithDefault(t *testing.T) {
195+
objBuilder := func(anno map[string]string) *unstructured.Unstructured {
196+
object := &unstructured.Unstructured{}
197+
object.SetAnnotations(anno)
198+
return object
199+
}
200+
201+
type args struct {
202+
obj *unstructured.Unstructured
203+
annotation string
204+
fallback bool
205+
}
206+
207+
tests := []struct {
208+
name string
209+
args args
210+
want bool
211+
}{
212+
{
213+
name: "Should return value of annotation read",
214+
args: args{
215+
obj: objBuilder(map[string]string{
216+
"helm.sdk.operatorframework.io/rollback-force": "false",
217+
}),
218+
annotation: "helm.sdk.operatorframework.io/rollback-force",
219+
fallback: true,
220+
},
221+
want: false,
222+
},
223+
{
224+
name: "Should return fallback when annotation is not present",
225+
args: args{
226+
obj: objBuilder(map[string]string{
227+
"helm.sdk.operatorframework.io/upgrade-force": "true",
228+
}),
229+
annotation: "helm.sdk.operatorframework.io/rollback-force",
230+
fallback: false,
231+
},
232+
want: false,
233+
},
234+
{
235+
name: "Should return fallback when errors while parsing bool value",
236+
args: args{
237+
obj: objBuilder(map[string]string{
238+
"helm.sdk.operatorframework.io/rollback-force": "force",
239+
}),
240+
annotation: "helm.sdk.operatorframework.io/rollback-force",
241+
fallback: true,
242+
},
243+
want: true,
244+
},
245+
}
246+
for _, tc := range tests {
247+
t.Run(tc.name, func(t *testing.T) {
248+
if got := readBoolAnnotationWithDefault(tc.args.obj, tc.args.annotation, tc.args.fallback); got != tc.want {
249+
assert.Equal(t, tc.want, got, "readBoolAnnotationWithDefault() function")
250+
}
251+
})
252+
}
253+
}

internal/helm/release/manager.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Manager interface {
5454
Sync(context.Context) error
5555
InstallRelease(context.Context, ...InstallOption) (*rpb.Release, error)
5656
UpgradeRelease(context.Context, ...UpgradeOption) (*rpb.Release, *rpb.Release, error)
57+
RollBack(context.Context, ...RollBackOption) error
5758
ReconcileRelease(context.Context) (*rpb.Release, error)
5859
UninstallRelease(context.Context, ...UninstallOption) (*rpb.Release, error)
5960
CleanupRelease(context.Context, string) (bool, error)
@@ -79,6 +80,7 @@ type manager struct {
7980
type InstallOption func(*action.Install) error
8081
type UpgradeOption func(*action.Upgrade) error
8182
type UninstallOption func(*action.Uninstall) error
83+
type RollBackOption func(*action.Rollback) error
8284

8385
// ReleaseName returns the name of the release.
8486
func (m manager) ReleaseName() string {
@@ -202,10 +204,13 @@ func ForceUpgrade(force bool) UpgradeOption {
202204
}
203205
}
204206

207+
var ErrUpgradeFailed = errors.New("upgrade failed; rollback required")
208+
205209
// UpgradeRelease performs a Helm release upgrade.
206210
func (m manager) UpgradeRelease(ctx context.Context, opts ...UpgradeOption) (*rpb.Release, *rpb.Release, error) {
207211
upgrade := action.NewUpgrade(m.actionConfig)
208212
upgrade.Namespace = m.namespace
213+
209214
for _, o := range opts {
210215
if err := o(upgrade); err != nil {
211216
return nil, nil, fmt.Errorf("failed to apply upgrade option: %w", err)
@@ -216,24 +221,44 @@ func (m manager) UpgradeRelease(ctx context.Context, opts ...UpgradeOption) (*rp
216221
if err != nil {
217222
// Workaround for helm/helm#3338
218223
if upgradedRelease != nil {
219-
rollback := action.NewRollback(m.actionConfig)
220-
rollback.Force = true
221-
222224
// As of Helm 2.13, if UpgradeRelease returns a non-nil release, that
223225
// means the release was also recorded in the release store.
224226
// Therefore, we should perform the rollback when we have a non-nil
225227
// release. Any rollback error here would be unexpected, so always
226228
// log both the upgrade and rollback errors.
227-
rollbackErr := rollback.Run(m.releaseName)
228-
if rollbackErr != nil {
229-
return nil, nil, fmt.Errorf("failed upgrade (%s) and failed rollback: %w", err, rollbackErr)
230-
}
229+
fmt.Printf("release upgrade failed; %v", err)
230+
231+
return nil, nil, ErrUpgradeFailed
231232
}
232233
return nil, nil, fmt.Errorf("failed to upgrade release: %w", err)
233234
}
234235
return m.deployedRelease, upgradedRelease, err
235236
}
236237

238+
func ForceRollback(force bool) RollBackOption {
239+
return func(r *action.Rollback) error {
240+
r.Force = force
241+
return nil
242+
}
243+
}
244+
245+
// RollBack attempts to reverse any partially applied releases
246+
func (m manager) RollBack(ctx context.Context, opts ...RollBackOption) error {
247+
rollback := action.NewRollback(m.actionConfig)
248+
249+
for _, fn := range opts {
250+
if err := fn(rollback); err != nil {
251+
return fmt.Errorf("failed to apply rollback option: %w", err)
252+
}
253+
}
254+
255+
if err := rollback.Run(m.releaseName); err != nil {
256+
return fmt.Errorf("rollback failed: %w", err)
257+
}
258+
259+
return nil
260+
}
261+
237262
// ReconcileRelease creates or patches resources as necessary to match the
238263
// deployed release's manifest.
239264
func (m manager) ReconcileRelease(ctx context.Context) (*rpb.Release, error) {

testdata/ansible/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

testdata/go/v3/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

testdata/go/v3/monitoring/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

testdata/go/v4-alpha/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

testdata/go/v4-alpha/monitoring/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

testdata/helm/memcached-operator/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ endif
4848

4949
# Set the Operator SDK version to use. By default, what is installed on the system is used.
5050
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
51-
OPERATOR_SDK_VERSION ?= v1.30.0
51+
OPERATOR_SDK_VERSION ?= v1.31.0
5252

5353
# Image URL to use all building/pushing image targets
5454
IMG ?= controller:latest

website/content/en/docs/building-operators/helm/reference/advanced_features/annotations.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,19 @@ metadata:
166166
The value that is present under this key must be in the h/m/s format. For example, 1h2m4s, 3m0s, 4s are all valid values, but 1x3m9s is invalid.
167167

168168
**NOTE**: This is just one way of specifying the reconcile period for Helm-based operators. There are two other ways: using the `--reconcile-period` command-line flag and under the 'reconcilePeriod' key in the watches.yaml file. If these three methods are used simultaneously to specify reconcile period (which they should not be), the order of precedence is as follows:
169-
Custom Resource Annotations > watches.yaml > command-line flag.
169+
Custom Resource Annotations > watches.yaml > command-line flag.
170+
171+
## `helm.sdk.operatorframework.io/rollback-force`
172+
173+
Whenever a helm-based operator encounters an error during reconcilliation, by default, it would attempt to perform a rollback with the `--force` option. While this works as expected in most scenarios, there are a few edge cases where performing a rollback with `--force` could have undesired side effects.
174+
175+
```sh
176+
...
177+
metadata:
178+
name: nginx-sample
179+
annotations:
180+
helm.sdk.operatorframework.io/rollback-force: false
181+
...
182+
```
183+
184+
Adding annotation to the custom resource, `helm.sdk.operatorframework.io/rollback-force: false` therefore allows a user, to change the default behavior of the helm-based operator whereby, rollbacks will be performed without the `--force` option whenever an error is encountered.

0 commit comments

Comments
 (0)