Skip to content

Commit eeaa958

Browse files
committed
helmrepo: same revision different checksum condn
This change prevents Reconciling and ArtifactOutdated conditions to be set on HelmRepo when the checksum of a cached repo index changes. Adds some tests to ensure that when the repo index is cached, the revision and checksum of the returned artifact are the same as on the existing object status. Also adds checks for the returned artifact and chartRepo from reconcileSource, to ensure that chartRepo is populated and the checksum of a new potential artifact is always empty, as it's populated when the artifact is written in the storage. Signed-off-by: Sunny <[email protected]>
1 parent dd40748 commit eeaa958

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)