Skip to content

Commit 745d6ee

Browse files
authored
Merge pull request #691 from fluxcd/cached-helmrepo-diff-checksum
helmrepo: same revision different checksum scenario
2 parents dd40748 + eeaa958 commit 745d6ee

File tree

2 files changed

+147
-16
lines changed

2 files changed

+147
-16
lines changed

controllers/helmrepository_controller.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, obj *sou
421421
return sreconcile.ResultSuccess, nil
422422
}
423423

424-
// Load the cached repository index to ensure it passes validation.
424+
// Load the cached repository index to ensure it passes validation. This
425+
// also populates chartRepo.Checksum.
425426
if err := chartRepo.LoadFromCache(); err != nil {
426427
e := &serror.Event{
427428
Err: fmt.Errorf("failed to load Helm repository from cache: %w", err),
@@ -433,13 +434,15 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, obj *sou
433434
chartRepo.Unload()
434435

435436
// Mark observations about the revision on the object.
436-
if !obj.GetArtifact().HasRevision(newChartRepo.Checksum) {
437+
if !obj.GetArtifact().HasRevision(chartRepo.Checksum) {
437438
message := fmt.Sprintf("new index revision '%s'", checksum)
438439
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message)
439440
conditions.MarkReconciling(obj, "NewRevision", message)
440441
}
441442

442443
// Create potential new artifact.
444+
// Note: Since this is a potential artifact, artifact.Checksum is empty at
445+
// this stage. It's populated when the artifact is written in storage.
443446
*artifact = r.Storage.NewArtifactFor(obj.Kind,
444447
obj.ObjectMeta.GetObjectMeta(),
445448
chartRepo.Checksum,

controllers/helmrepository_controller_test.go

Lines changed: 142 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21+
"crypto/tls"
2122
"errors"
2223
"fmt"
2324
"net/http"
@@ -33,6 +34,7 @@ import (
3334
"github.com/fluxcd/pkg/runtime/conditions"
3435
"github.com/fluxcd/pkg/runtime/patch"
3536
. "github.com/onsi/gomega"
37+
helmgetter "helm.sh/helm/v3/pkg/getter"
3638
corev1 "k8s.io/api/core/v1"
3739
apierrors "k8s.io/apimachinery/pkg/api/errors"
3840
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -43,6 +45,7 @@ import (
4345
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
4446

4547
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
48+
"github.com/fluxcd/source-controller/internal/helm/getter"
4649
"github.com/fluxcd/source-controller/internal/helm/repository"
4750
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
4851
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
@@ -288,8 +291,8 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
288291
protocol string
289292
server options
290293
secret *corev1.Secret
291-
beforeFunc func(t *WithT, obj *sourcev1.HelmRepository)
292-
afterFunc func(t *WithT, obj *sourcev1.HelmRepository)
294+
beforeFunc func(t *WithT, obj *sourcev1.HelmRepository, checksum string)
295+
afterFunc func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository)
293296
want sreconcile.Result
294297
wantErr bool
295298
assertConditions []metav1.Condition
@@ -302,6 +305,12 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
302305
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
303306
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
304307
},
308+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
309+
t.Expect(chartRepo.Checksum).ToNot(BeEmpty())
310+
t.Expect(chartRepo.CachePath).ToNot(BeEmpty())
311+
t.Expect(artifact.Checksum).To(BeEmpty())
312+
t.Expect(artifact.Revision).ToNot(BeEmpty())
313+
},
305314
},
306315
{
307316
name: "HTTP with Basic Auth secret makes ArtifactOutdated=True",
@@ -319,14 +328,20 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
319328
"password": []byte("1234"),
320329
},
321330
},
322-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
331+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
323332
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
324333
},
325334
want: sreconcile.ResultSuccess,
326335
assertConditions: []metav1.Condition{
327336
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
328337
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
329338
},
339+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
340+
t.Expect(chartRepo.Checksum).ToNot(BeEmpty())
341+
t.Expect(chartRepo.CachePath).ToNot(BeEmpty())
342+
t.Expect(artifact.Checksum).To(BeEmpty())
343+
t.Expect(artifact.Revision).ToNot(BeEmpty())
344+
},
330345
},
331346
{
332347
name: "HTTPS with CAFile secret makes ArtifactOutdated=True",
@@ -344,14 +359,20 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
344359
"caFile": tlsCA,
345360
},
346361
},
347-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
362+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
348363
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
349364
},
350365
want: sreconcile.ResultSuccess,
351366
assertConditions: []metav1.Condition{
352367
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
353368
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
354369
},
370+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
371+
t.Expect(chartRepo.Checksum).ToNot(BeEmpty())
372+
t.Expect(chartRepo.CachePath).ToNot(BeEmpty())
373+
t.Expect(artifact.Checksum).To(BeEmpty())
374+
t.Expect(artifact.Revision).ToNot(BeEmpty())
375+
},
355376
},
356377
{
357378
name: "HTTPS with invalid CAFile secret makes FetchFailed=True and returns error",
@@ -369,48 +390,76 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
369390
"caFile": []byte("invalid"),
370391
},
371392
},
372-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
393+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
373394
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
374395
},
375396
wantErr: true,
376397
assertConditions: []metav1.Condition{
377398
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to create TLS client config with secret data: cannot append certificate into certificate pool: invalid caFile"),
378399
},
400+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
401+
// No repo index due to fetch fail.
402+
t.Expect(chartRepo.Checksum).To(BeEmpty())
403+
t.Expect(chartRepo.CachePath).To(BeEmpty())
404+
t.Expect(artifact.Checksum).To(BeEmpty())
405+
t.Expect(artifact.Revision).To(BeEmpty())
406+
},
379407
},
380408
{
381409
name: "Invalid URL makes FetchFailed=True and returns stalling error",
382410
protocol: "http",
383-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
411+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
384412
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "")
385413
},
386414
want: sreconcile.ResultEmpty,
387415
wantErr: true,
388416
assertConditions: []metav1.Condition{
389417
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.URLInvalidReason, "first path segment in URL cannot contain colon"),
390418
},
419+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
420+
// No repo index due to fetch fail.
421+
t.Expect(chartRepo.Checksum).To(BeEmpty())
422+
t.Expect(chartRepo.CachePath).To(BeEmpty())
423+
t.Expect(artifact.Checksum).To(BeEmpty())
424+
t.Expect(artifact.Revision).To(BeEmpty())
425+
},
391426
},
392427
{
393428
name: "Unsupported scheme makes FetchFailed=True and returns stalling error",
394429
protocol: "http",
395-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
430+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
396431
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "ftp://")
397432
},
398433
want: sreconcile.ResultEmpty,
399434
wantErr: true,
400435
assertConditions: []metav1.Condition{
401436
*conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FailedReason, "scheme \"ftp\" not supported"),
402437
},
438+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
439+
// No repo index due to fetch fail.
440+
t.Expect(chartRepo.Checksum).To(BeEmpty())
441+
t.Expect(chartRepo.CachePath).To(BeEmpty())
442+
t.Expect(artifact.Checksum).To(BeEmpty())
443+
t.Expect(artifact.Revision).To(BeEmpty())
444+
},
403445
},
404446
{
405447
name: "Missing secret returns FetchFailed=True and returns error",
406448
protocol: "http",
407-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
449+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
408450
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing"}
409451
},
410452
wantErr: true,
411453
assertConditions: []metav1.Condition{
412454
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "secrets \"non-existing\" not found"),
413455
},
456+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
457+
// No repo index due to fetch fail.
458+
t.Expect(chartRepo.Checksum).To(BeEmpty())
459+
t.Expect(chartRepo.CachePath).To(BeEmpty())
460+
t.Expect(artifact.Checksum).To(BeEmpty())
461+
t.Expect(artifact.Revision).To(BeEmpty())
462+
},
414463
},
415464
{
416465
name: "Malformed secret returns FetchFailed=True and returns error",
@@ -423,13 +472,56 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
423472
"username": []byte("git"),
424473
},
425474
},
426-
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
475+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
427476
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "malformed-basic-auth"}
428477
},
429478
wantErr: true,
430479
assertConditions: []metav1.Condition{
431480
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "required fields 'username' and 'password"),
432481
},
482+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
483+
// No repo index due to fetch fail.
484+
t.Expect(chartRepo.Checksum).To(BeEmpty())
485+
t.Expect(chartRepo.CachePath).To(BeEmpty())
486+
t.Expect(artifact.Checksum).To(BeEmpty())
487+
t.Expect(artifact.Revision).To(BeEmpty())
488+
},
489+
},
490+
{
491+
name: "cached index with same checksum",
492+
protocol: "http",
493+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
494+
obj.Status.Artifact = &sourcev1.Artifact{
495+
Revision: checksum,
496+
Checksum: checksum,
497+
}
498+
},
499+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
500+
// chartRepo.Checksum isn't populated, artifact.Checksum is
501+
// populated from the cached repo index data.
502+
t.Expect(chartRepo.Checksum).To(BeEmpty())
503+
t.Expect(chartRepo.CachePath).ToNot(BeEmpty())
504+
t.Expect(artifact.Checksum).To(Equal(obj.Status.Artifact.Checksum))
505+
t.Expect(artifact.Revision).To(Equal(obj.Status.Artifact.Revision))
506+
},
507+
want: sreconcile.ResultSuccess,
508+
},
509+
{
510+
name: "cached index with different checksum",
511+
protocol: "http",
512+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, checksum string) {
513+
obj.Status.Artifact = &sourcev1.Artifact{
514+
Revision: checksum,
515+
Checksum: "foo",
516+
}
517+
},
518+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo repository.ChartRepository) {
519+
t.Expect(chartRepo.Checksum).ToNot(BeEmpty())
520+
t.Expect(chartRepo.CachePath).ToNot(BeEmpty())
521+
t.Expect(artifact.Checksum).To(BeEmpty())
522+
t.Expect(artifact.Revision).To(Equal(obj.Status.Artifact.Revision))
523+
},
524+
want: sreconcile.ResultSuccess,
433525
},
434526
}
435527

@@ -481,15 +573,51 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
481573
t.Fatalf("unsupported protocol %q", tt.protocol)
482574
}
483575

484-
if tt.beforeFunc != nil {
485-
tt.beforeFunc(g, obj)
486-
}
487-
488576
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
489577
if secret != nil {
490578
builder.WithObjects(secret.DeepCopy())
491579
}
492580

581+
// Calculate the artifact checksum for valid repos configurations.
582+
clientOpts := []helmgetter.Option{
583+
helmgetter.WithURL(server.URL()),
584+
}
585+
var newChartRepo *repository.ChartRepository
586+
var tOpts *tls.Config
587+
validSecret := true
588+
if secret != nil {
589+
// Extract the client options from secret, ignoring any invalid
590+
// value. validSecret is used to determine if the indexChecksum
591+
// should be calculated below.
592+
var cOpts []helmgetter.Option
593+
var serr error
594+
cOpts, serr = getter.ClientOptionsFromSecret(*secret)
595+
if serr != nil {
596+
validSecret = false
597+
}
598+
clientOpts = append(clientOpts, cOpts...)
599+
tOpts, serr = getter.TLSClientConfigFromSecret(*secret, server.URL())
600+
if serr != nil {
601+
validSecret = false
602+
}
603+
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts)
604+
} else {
605+
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil, nil)
606+
}
607+
g.Expect(err).ToNot(HaveOccurred())
608+
609+
// NOTE: checksum will be empty in beforeFunc for invalid repo
610+
// configurations as the client can't get the repo.
611+
var indexChecksum string
612+
if validSecret {
613+
indexChecksum, err = newChartRepo.CacheIndex()
614+
g.Expect(err).ToNot(HaveOccurred())
615+
}
616+
617+
if tt.beforeFunc != nil {
618+
tt.beforeFunc(g, obj, indexChecksum)
619+
}
620+
493621
r := &HelmRepositoryReconciler{
494622
EventRecorder: record.NewFakeRecorder(32),
495623
Client: builder.Build(),
@@ -507,7 +635,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
507635
g.Expect(got).To(Equal(tt.want))
508636

509637
if tt.afterFunc != nil {
510-
tt.afterFunc(g, obj)
638+
tt.afterFunc(g, obj, artifact, chartRepo)
511639
}
512640
})
513641
}

0 commit comments

Comments
 (0)