Skip to content

Commit 7f9160c

Browse files
authored
Merge pull request #823 from fluxcd/reset-force-annotations
Introduce `forceAt` and `resetAt` annotations
2 parents d310c8b + 6b7789a commit 7f9160c

10 files changed

+441
-9
lines changed

api/v2beta2/annotations.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2023 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v2beta2
18+
19+
import "github.com/fluxcd/pkg/apis/meta"
20+
21+
const (
22+
// ForceRequestAnnotation is the annotation used for triggering a one-off forced
23+
// Helm release, even when there are no new changes in the HelmRelease.
24+
// The value is interpreted as a token, and must equal the value of
25+
// meta.ReconcileRequestAnnotation in order to trigger a release.
26+
ForceRequestAnnotation string = "reconcile.fluxcd.io/forceAt"
27+
28+
// ResetRequestAnnotation is the annotation used for resetting the failure counts
29+
// of a HelmRelease, so that it can be retried again.
30+
// The value is interpreted as a token, and must equal the value of
31+
// meta.ReconcileRequestAnnotation in order to reset the failure counts.
32+
ResetRequestAnnotation string = "reconcile.fluxcd.io/resetAt"
33+
)
34+
35+
// ShouldHandleResetRequest returns true if the HelmRelease has a reset request
36+
// annotation, and the value of the annotation matches the value of the
37+
// meta.ReconcileRequestAnnotation annotation.
38+
//
39+
// To ensure that the reset request is handled only once, the value of
40+
// HelmReleaseStatus.LastHandledResetAt is updated to match the value of the
41+
// reset request annotation (even if the reset request is not handled because
42+
// the value of the meta.ReconcileRequestAnnotation annotation does not match).
43+
func ShouldHandleResetRequest(obj *HelmRelease) bool {
44+
return handleRequest(obj, ResetRequestAnnotation, &obj.Status.LastHandledResetAt)
45+
}
46+
47+
// ShouldHandleForceRequest returns true if the HelmRelease has a force request
48+
// annotation, and the value of the annotation matches the value of the
49+
// meta.ReconcileRequestAnnotation annotation.
50+
//
51+
// To ensure that the force request is handled only once, the value of
52+
// HelmReleaseStatus.LastHandledForceAt is updated to match the value of the
53+
// force request annotation (even if the force request is not handled because
54+
// the value of the meta.ReconcileRequestAnnotation annotation does not match).
55+
func ShouldHandleForceRequest(obj *HelmRelease) bool {
56+
return handleRequest(obj, ForceRequestAnnotation, &obj.Status.LastHandledForceAt)
57+
}
58+
59+
// handleRequest returns true if the HelmRelease has a request annotation, and
60+
// the value of the annotation matches the value of the meta.ReconcileRequestAnnotation
61+
// annotation.
62+
//
63+
// The lastHandled argument is used to ensure that the request is handled only
64+
// once, and is updated to match the value of the request annotation (even if
65+
// the request is not handled because the value of the meta.ReconcileRequestAnnotation
66+
// annotation does not match).
67+
func handleRequest(obj *HelmRelease, annotation string, lastHandled *string) bool {
68+
requestAt, requestOk := obj.GetAnnotations()[annotation]
69+
reconcileAt, reconcileOk := meta.ReconcileAnnotationValue(obj.GetAnnotations())
70+
71+
var lastHandledRequest string
72+
if requestOk {
73+
lastHandledRequest = *lastHandled
74+
*lastHandled = requestAt
75+
}
76+
77+
if requestOk && reconcileOk && requestAt == reconcileAt {
78+
lastHandledReconcile := obj.Status.GetLastHandledReconcileRequest()
79+
if lastHandledReconcile != reconcileAt && lastHandledRequest != requestAt {
80+
return true
81+
}
82+
}
83+
return false
84+
}

api/v2beta2/annotations_test.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
Copyright 2023 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v2beta2
18+
19+
import (
20+
"testing"
21+
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
24+
"github.com/fluxcd/pkg/apis/meta"
25+
)
26+
27+
func TestShouldHandleResetRequest(t *testing.T) {
28+
t.Run("should handle reset request", func(t *testing.T) {
29+
obj := &HelmRelease{
30+
ObjectMeta: metav1.ObjectMeta{
31+
Annotations: map[string]string{
32+
meta.ReconcileRequestAnnotation: "b",
33+
ResetRequestAnnotation: "b",
34+
},
35+
},
36+
Status: HelmReleaseStatus{
37+
LastHandledResetAt: "a",
38+
ReconcileRequestStatus: meta.ReconcileRequestStatus{
39+
LastHandledReconcileAt: "a",
40+
},
41+
},
42+
}
43+
44+
if !ShouldHandleResetRequest(obj) {
45+
t.Error("ShouldHandleResetRequest() = false")
46+
}
47+
48+
if obj.Status.LastHandledResetAt != "b" {
49+
t.Error("ShouldHandleResetRequest did not update LastHandledResetAt")
50+
}
51+
})
52+
}
53+
54+
func TestShouldHandleForceRequest(t *testing.T) {
55+
t.Run("should handle force request", func(t *testing.T) {
56+
obj := &HelmRelease{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Annotations: map[string]string{
59+
meta.ReconcileRequestAnnotation: "b",
60+
ForceRequestAnnotation: "b",
61+
},
62+
},
63+
Status: HelmReleaseStatus{
64+
LastHandledForceAt: "a",
65+
ReconcileRequestStatus: meta.ReconcileRequestStatus{
66+
LastHandledReconcileAt: "a",
67+
},
68+
},
69+
}
70+
71+
if !ShouldHandleForceRequest(obj) {
72+
t.Error("ShouldHandleForceRequest() = false")
73+
}
74+
75+
if obj.Status.LastHandledForceAt != "b" {
76+
t.Error("ShouldHandleForceRequest did not update LastHandledForceAt")
77+
}
78+
})
79+
}
80+
81+
func Test_handleRequest(t *testing.T) {
82+
const requestAnnotation = "requestAnnotation"
83+
84+
tests := []struct {
85+
name string
86+
annotations map[string]string
87+
lastHandledReconcile string
88+
lastHandledRequest string
89+
want bool
90+
expectLastHandledRequest string
91+
}{
92+
{
93+
name: "valid request and reconcile annotations",
94+
annotations: map[string]string{
95+
meta.ReconcileRequestAnnotation: "b",
96+
requestAnnotation: "b",
97+
},
98+
want: true,
99+
expectLastHandledRequest: "b",
100+
},
101+
{
102+
name: "mismatched annotations",
103+
annotations: map[string]string{
104+
meta.ReconcileRequestAnnotation: "b",
105+
requestAnnotation: "c",
106+
},
107+
want: false,
108+
expectLastHandledRequest: "c",
109+
},
110+
{
111+
name: "reconcile matches previous request",
112+
annotations: map[string]string{
113+
meta.ReconcileRequestAnnotation: "b",
114+
requestAnnotation: "b",
115+
},
116+
lastHandledReconcile: "a",
117+
lastHandledRequest: "b",
118+
want: false,
119+
expectLastHandledRequest: "b",
120+
},
121+
{
122+
name: "request matches previous reconcile",
123+
annotations: map[string]string{
124+
meta.ReconcileRequestAnnotation: "b",
125+
requestAnnotation: "b",
126+
},
127+
lastHandledReconcile: "b",
128+
lastHandledRequest: "a",
129+
want: false,
130+
expectLastHandledRequest: "b",
131+
},
132+
{
133+
name: "missing annotations",
134+
annotations: map[string]string{},
135+
lastHandledRequest: "a",
136+
want: false,
137+
expectLastHandledRequest: "a",
138+
},
139+
}
140+
141+
for _, tt := range tests {
142+
t.Run(tt.name, func(t *testing.T) {
143+
obj := &HelmRelease{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Annotations: tt.annotations,
146+
},
147+
Status: HelmReleaseStatus{
148+
ReconcileRequestStatus: meta.ReconcileRequestStatus{
149+
LastHandledReconcileAt: tt.lastHandledReconcile,
150+
},
151+
},
152+
}
153+
154+
lastHandled := tt.lastHandledRequest
155+
result := handleRequest(obj, requestAnnotation, &lastHandled)
156+
157+
if result != tt.want {
158+
t.Errorf("handleRequest() = %v, want %v", result, tt.want)
159+
}
160+
if lastHandled != tt.expectLastHandledRequest {
161+
t.Errorf("lastHandledRequest = %v, want %v", lastHandled, tt.expectLastHandledRequest)
162+
}
163+
})
164+
}
165+
}

api/v2beta2/helmrelease_types.go

+10
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,16 @@ type HelmReleaseStatus struct {
10091009
// +optional
10101010
LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"`
10111011

1012+
// LastHandledForceAt holds the value of the most recent force request
1013+
// value, so a change of the annotation value can be detected.
1014+
// +optional
1015+
LastHandledForceAt string `json:"lastHandledForceAt,omitempty"`
1016+
1017+
// LastHandledResetAt holds the value of the most recent reset request
1018+
// value, so a change of the annotation value can be detected.
1019+
// +optional
1020+
LastHandledResetAt string `json:"lastHandledResetAt,omitempty"`
1021+
10121022
meta.ReconcileRequestStatus `json:",inline"`
10131023
}
10141024

config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -2126,11 +2126,21 @@ spec:
21262126
the values of the last reconciliation attempt. Deprecated: Use LastAttemptedConfigDigest
21272127
instead.'
21282128
type: string
2129+
lastHandledForceAt:
2130+
description: LastHandledForceAt holds the value of the most recent
2131+
force request value, so a change of the annotation value can be
2132+
detected.
2133+
type: string
21292134
lastHandledReconcileAt:
21302135
description: LastHandledReconcileAt holds the value of the most recent
21312136
reconcile request value, so a change of the annotation value can
21322137
be detected.
21332138
type: string
2139+
lastHandledResetAt:
2140+
description: LastHandledResetAt holds the value of the most recent
2141+
reset request value, so a change of the annotation value can be
2142+
detected.
2143+
type: string
21342144
lastReleaseRevision:
21352145
description: 'LastReleaseRevision is the revision of the last successful
21362146
Helm release. Deprecated: Use History instead.'

docs/api/v2beta2/helm.md

+26
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,32 @@ string
15281528
</tr>
15291529
<tr>
15301530
<td>
1531+
<code>lastHandledForceAt</code><br>
1532+
<em>
1533+
string
1534+
</em>
1535+
</td>
1536+
<td>
1537+
<em>(Optional)</em>
1538+
<p>LastHandledForceAt holds the value of the most recent force request
1539+
value, so a change of the annotation value can be detected.</p>
1540+
</td>
1541+
</tr>
1542+
<tr>
1543+
<td>
1544+
<code>lastHandledResetAt</code><br>
1545+
<em>
1546+
string
1547+
</em>
1548+
</td>
1549+
<td>
1550+
<em>(Optional)</em>
1551+
<p>LastHandledResetAt holds the value of the most recent reset request
1552+
value, so a change of the annotation value can be detected.</p>
1553+
</td>
1554+
</tr>
1555+
<tr>
1556+
<td>
15311557
<code>ReconcileRequestStatus</code><br>
15321558
<em>
15331559
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">

internal/action/reset.go

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
differentGenerationReason = "generation differs from last attempt"
3030
differentRevisionReason = "chart version differs from last attempt"
3131
differentValuesReason = "values differ from last attempt"
32+
resetRequestedReason = "reset requested through annotation"
3233
)
3334

3435
// MustResetFailures returns a reason and true if the HelmRelease's status
@@ -38,6 +39,12 @@ const (
3839
// For example, a change in generation, chart version, or values.
3940
// If no change is detected, an empty string is returned along with false.
4041
func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values chartutil.Values) (string, bool) {
42+
// Always check if a reset is requested.
43+
// This is done first, so that the HelmReleaseStatus.LastHandledResetAt
44+
// field is updated even if the reset request is not handled due to other
45+
// diverging data.
46+
resetRequested := v2.ShouldHandleResetRequest(obj)
47+
4148
switch {
4249
case obj.Status.LastAttemptedGeneration != obj.Generation:
4350
return differentGenerationReason, true
@@ -53,5 +60,10 @@ func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values chartu
5360
return differentValuesReason, true
5461
}
5562
}
63+
64+
if resetRequested {
65+
return resetRequestedReason, true
66+
}
67+
5668
return "", false
5769
}

internal/action/reset_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"helm.sh/helm/v3/pkg/chartutil"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626

27+
"github.com/fluxcd/pkg/apis/meta"
28+
2729
v2 "github.com/fluxcd/helm-controller/api/v2beta2"
2830
)
2931

@@ -108,6 +110,31 @@ func TestMustResetFailures(t *testing.T) {
108110
want: true,
109111
wantReason: differentValuesReason,
110112
},
113+
{
114+
name: "on reset request through annotation",
115+
obj: &v2.HelmRelease{
116+
ObjectMeta: metav1.ObjectMeta{
117+
Generation: 1,
118+
Annotations: map[string]string{
119+
meta.ReconcileRequestAnnotation: "a",
120+
v2.ResetRequestAnnotation: "a",
121+
},
122+
},
123+
Status: v2.HelmReleaseStatus{
124+
LastAttemptedGeneration: 1,
125+
LastAttemptedRevision: "1.0.0",
126+
LastAttemptedConfigDigest: "sha256:1dabc4e3cbbd6a0818bd460f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e",
127+
},
128+
},
129+
chart: &chart.Metadata{
130+
Version: "1.0.0",
131+
},
132+
values: chartutil.Values{
133+
"foo": "bar",
134+
},
135+
want: true,
136+
wantReason: resetRequestedReason,
137+
},
111138
{
112139
name: "without change no reset",
113140
obj: &v2.HelmRelease{

0 commit comments

Comments
 (0)