Skip to content

Commit 7ca9bab

Browse files
authored
Merge pull request #1639 from fluxcd/backport-1638-to-release/v1.7.x
[release/v1.7.x] Requeue on artifact digest mismatch
2 parents b95e73a + 85e3cd2 commit 7ca9bab

4 files changed

Lines changed: 87 additions & 4 deletions

File tree

‎internal/controller/helmrelease_controller.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,14 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
336336
loadedChart, err := loader.SecureLoadChartFromURL(ctx, source.GetArtifact().URL, source.GetArtifact().Digest,
337337
r.ArtifactFetchRetries, r.ArtifactFetchTimeout, r.ArtifactServerTLSConfig)
338338
if err != nil {
339-
if errors.Is(err, loader.ErrFileNotFound) {
340-
msg := fmt.Sprintf("Source not ready: artifact not found. Retrying in %s", r.DependencyRequeueInterval.String())
339+
// Requeue at the dependency interval if the artifact is not found or
340+
// its digest doesn't match, as the source status may not be updated yet.
341+
if errors.Is(err, loader.ErrFileNotFound) || errors.Is(err, loader.ErrDigestMismatch) {
342+
cause := "artifact not found"
343+
if errors.Is(err, loader.ErrDigestMismatch) {
344+
cause = "artifact digest changed"
345+
}
346+
msg := fmt.Sprintf("Source not ready: %s. Retrying in %s", cause, r.DependencyRequeueInterval.String())
341347
conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "%s", msg)
342348
log.Info(msg)
343349
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, errWaitForDependency

‎internal/controller/helmrelease_controller_test.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,67 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) {
413413
}))
414414
})
415415

416+
t.Run("requeues on Helm chart digest mismatch", func(t *testing.T) {
417+
g := NewWithT(t)
418+
419+
chartMock := testutil.BuildChart()
420+
chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
421+
g.Expect(err).ToNot(HaveOccurred())
422+
// Advertise a digest that differs from the served artifact.
423+
chartArtifact.Digest = digest.SHA256.FromString("rebuilt").String()
424+
425+
chart := &sourcev1.HelmChart{
426+
ObjectMeta: metav1.ObjectMeta{
427+
Name: "chart",
428+
Namespace: "mock",
429+
Generation: 1,
430+
},
431+
Status: sourcev1.HelmChartStatus{
432+
ObservedGeneration: 1,
433+
Artifact: chartArtifact,
434+
Conditions: []metav1.Condition{
435+
{
436+
Type: meta.ReadyCondition,
437+
Status: metav1.ConditionTrue,
438+
},
439+
},
440+
},
441+
}
442+
443+
obj := &v2.HelmRelease{
444+
ObjectMeta: metav1.ObjectMeta{
445+
Name: "release",
446+
Namespace: "mock",
447+
},
448+
Status: v2.HelmReleaseStatus{
449+
HelmChart: "mock/chart",
450+
},
451+
}
452+
453+
recorder := events.NewFakeRecorder(32, false)
454+
r := &HelmReleaseReconciler{
455+
Client: fake.NewClientBuilder().
456+
WithScheme(NewTestScheme()).
457+
WithStatusSubresource(&v2.HelmRelease{}).
458+
WithObjects(chart, obj).
459+
Build(),
460+
Recorder: recorder,
461+
DependencyRequeueInterval: 10 * time.Second,
462+
}
463+
r.APIReader = r.Client
464+
465+
res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
466+
g.Expect(err).To(Equal(errWaitForDependency))
467+
g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
468+
469+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
470+
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
471+
*conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason,
472+
"Source not ready: artifact digest changed. Retrying in 10s"),
473+
}))
474+
g.Expect(recorder.Events).To(BeEmpty())
475+
})
476+
416477
t.Run("uninstalls HelmRelease if target has changed", func(t *testing.T) {
417478
g := NewWithT(t)
418479

‎internal/loader/artifact_url.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,16 @@ const (
4747
var (
4848
// ErrFileNotFound is an error type used to signal 404 HTTP status code responses.
4949
ErrFileNotFound = errors.New("file not found")
50+
51+
// ErrDigestMismatch is an error type used to signal that the downloaded
52+
// artifact does not match the provided digest.
53+
ErrDigestMismatch = fluxfetch.ErrDigestMismatch
5054
)
5155

5256
// SecureLoadChartFromURL attempts to download a Helm chart from the given URL,
5357
// verifies it against the given digest and loads it. The error may be of type
54-
// ErrFileNotFound if the artifact does not exist.
58+
// ErrFileNotFound if the artifact does not exist, or ErrDigestMismatch if the
59+
// downloaded artifact does not match the given digest.
5560
func SecureLoadChartFromURL(ctx context.Context, URL, digest string, retries int, timeout time.Duration, tlsConfig *tls.Config) (*chart.Chart, error) {
5661
sourceLocalhost := os.Getenv(envSourceControllerLocalhost)
5762
if strings.Contains(URL, "//source-watcher") {
@@ -76,6 +81,9 @@ func SecureLoadChartFromURL(ctx context.Context, URL, digest string, retries int
7681
if errors.Is(err, fluxfetch.ErrFileNotFound) {
7782
return nil, fmt.Errorf("failed to download chart from '%s': %w", URL, ErrFileNotFound)
7883
}
84+
if errors.Is(err, fluxfetch.ErrDigestMismatch) {
85+
return nil, fmt.Errorf("failed to download chart from '%s': %w", URL, err)
86+
}
7987
return nil, err
8088
}
8189

‎internal/loader/artifact_url_test.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,13 @@ func TestSecureLoadChartFromURL(t *testing.T) {
104104
t.Run("error on chart data digest mismatch", func(t *testing.T) {
105105
g := NewWithT(t)
106106

107-
got, err := SecureLoadChartFromURL(context.Background(), chartURL, digestlib.SHA256.FromString("invalid").String(), 2, 5*time.Second, nil)
107+
wrongDigest := digestlib.SHA256.FromString("invalid").String()
108+
got, err := SecureLoadChartFromURL(context.Background(), chartURL, wrongDigest, 2, 5*time.Second, nil)
108109
g.Expect(err).To(HaveOccurred())
110+
g.Expect(errors.Is(err, ErrDigestMismatch)).To(BeTrue())
111+
g.Expect(errors.Is(err, ErrFileNotFound)).To(BeFalse())
112+
g.Expect(err.Error()).To(ContainSubstring(chartURL))
113+
g.Expect(err.Error()).To(ContainSubstring("computed digest doesn't match provided '" + wrongDigest + "'"))
109114
g.Expect(got).To(BeNil())
110115
})
111116

@@ -114,6 +119,7 @@ func TestSecureLoadChartFromURL(t *testing.T) {
114119

115120
got, err := SecureLoadChartFromURL(context.Background(), server.URL+notFoundPath, digest.String(), 2, 5*time.Second, nil)
116121
g.Expect(errors.Is(err, ErrFileNotFound)).To(BeTrue())
122+
g.Expect(errors.Is(err, ErrDigestMismatch)).To(BeFalse())
117123
g.Expect(got).To(BeNil())
118124
})
119125

@@ -123,6 +129,7 @@ func TestSecureLoadChartFromURL(t *testing.T) {
123129
got, err := SecureLoadChartFromURL(context.Background(), server.URL+"/invalid.tgz", digest.String(), 2, 5*time.Second, nil)
124130
g.Expect(err).To(HaveOccurred())
125131
g.Expect(errors.Is(err, ErrFileNotFound)).To(BeFalse())
132+
g.Expect(errors.Is(err, ErrDigestMismatch)).To(BeFalse())
126133
g.Expect(got).To(BeNil())
127134
})
128135

@@ -131,6 +138,7 @@ func TestSecureLoadChartFromURL(t *testing.T) {
131138

132139
got, err := SecureLoadChartFromURL(context.Background(), server.URL+slowPath, digest.String(), 0, 10*time.Millisecond, nil)
133140
g.Expect(err).To(HaveOccurred())
141+
g.Expect(errors.Is(err, ErrDigestMismatch)).To(BeFalse())
134142
g.Expect(got).To(BeNil())
135143
})
136144
}

0 commit comments

Comments
 (0)