Skip to content

Commit 74aa43f

Browse files
somtochiamahiddeco
authored andcommitted
archive helm index in JSON format
Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>
1 parent 6377c6f commit 74aa43f

File tree

5 files changed

+185
-85
lines changed

5 files changed

+185
-85
lines changed

internal/controller/helmrepository_controller.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package controller
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"errors"
2223
"fmt"
@@ -449,11 +450,11 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
449450

450451
// Early comparison to current Artifact.
451452
if curArtifact := obj.GetArtifact(); curArtifact != nil {
452-
curDig := digest.Digest(curArtifact.Digest)
453-
if curDig.Validate() == nil {
453+
curRev := digest.Digest(curArtifact.Revision)
454+
if curRev.Validate() == nil {
454455
// Short-circuit based on the fetched index being an exact match to the
455456
// stored Artifact.
456-
if newDig := chartRepo.Digest(curDig.Algorithm()); newDig.Validate() == nil && (newDig == curDig) {
457+
if newRev := chartRepo.Digest(curRev.Algorithm()); newRev.Validate() == nil && (newRev == curRev) {
457458
*artifact = *curArtifact
458459
conditions.Delete(obj, sourcev1.FetchFailedCondition)
459460
return sreconcile.ResultSuccess, nil
@@ -473,13 +474,6 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
473474
// Delete any stale failure observation
474475
conditions.Delete(obj, sourcev1.FetchFailedCondition)
475476

476-
// Check if index has changed compared to current Artifact revision.
477-
var changed bool
478-
if artifact := obj.Status.Artifact; artifact != nil {
479-
curRev := digest.Digest(artifact.Revision)
480-
changed = curRev.Validate() != nil || curRev != chartRepo.Digest(curRev.Algorithm())
481-
}
482-
483477
// Calculate revision.
484478
revision := chartRepo.Digest(intdigest.Canonical)
485479
if revision.Validate() != nil {
@@ -492,16 +486,14 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
492486
}
493487

494488
// Mark observations about the revision on the object.
495-
if obj.Status.Artifact == nil || changed {
496-
message := fmt.Sprintf("new index revision '%s'", revision)
497-
if obj.GetArtifact() != nil {
498-
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message)
499-
}
500-
rreconcile.ProgressiveStatus(true, obj, meta.ProgressingReason, "building artifact: %s", message)
501-
if err := sp.Patch(ctx, obj, r.patchOptions...); err != nil {
502-
ctrl.LoggerFrom(ctx).Error(err, "failed to patch")
503-
return sreconcile.ResultEmpty, err
504-
}
489+
message := fmt.Sprintf("new index revision '%s'", revision)
490+
if obj.GetArtifact() != nil {
491+
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message)
492+
}
493+
rreconcile.ProgressiveStatus(true, obj, meta.ProgressingReason, "building artifact: %s", message)
494+
if err := sp.Patch(ctx, obj, r.patchOptions...); err != nil {
495+
ctrl.LoggerFrom(ctx).Error(err, "failed to patch")
496+
return sreconcile.ResultEmpty, err
505497
}
506498

507499
// Create potential new artifact.
@@ -566,8 +558,17 @@ func (r *HelmRepositoryReconciler) reconcileArtifact(ctx context.Context, sp *pa
566558
}
567559
defer unlock()
568560

569-
// Save artifact to storage.
570-
if err = r.Storage.CopyFromPath(artifact, chartRepo.Path); err != nil {
561+
// Save artifact to storage in JSON format.
562+
b, err := chartRepo.ToJSON()
563+
if err != nil {
564+
e := &serror.Event{
565+
Err: fmt.Errorf("unable to get JSON index from chart repo: %w", err),
566+
Reason: sourcev1.ArchiveOperationFailedReason,
567+
}
568+
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
569+
return sreconcile.ResultEmpty, e
570+
}
571+
if err = r.Storage.Copy(artifact, bytes.NewBuffer(b)); err != nil {
571572
e := &serror.Event{
572573
Err: fmt.Errorf("unable to save artifact to storage: %w", err),
573574
Reason: sourcev1.ArchiveOperationFailedReason,

internal/controller/helmrepository_controller_test.go

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"crypto/tls"
22+
"encoding/json"
2223
"errors"
2324
"fmt"
2425
"net/http"
@@ -417,7 +418,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
417418
server options
418419
url string
419420
secret *corev1.Secret
420-
beforeFunc func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest)
421+
beforeFunc func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest)
421422
afterFunc func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository)
422423
want sreconcile.Result
423424
wantErr bool
@@ -436,7 +437,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
436437
"caFile": tlsCA,
437438
},
438439
},
439-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
440+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
440441
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
441442
},
442443
assertConditions: []metav1.Condition{
@@ -474,7 +475,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
474475
"password": []byte("1234"),
475476
},
476477
},
477-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
478+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
478479
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
479480
},
480481
want: sreconcile.ResultSuccess,
@@ -504,7 +505,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
504505
"caFile": []byte("invalid"),
505506
},
506507
},
507-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
508+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
508509
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
509510
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
510511
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@@ -525,7 +526,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
525526
{
526527
name: "Invalid URL makes FetchFailed=True and returns stalling error",
527528
protocol: "http",
528-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
529+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
529530
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "")
530531
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
531532
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@@ -547,7 +548,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
547548
{
548549
name: "Unsupported scheme makes FetchFailed=True and returns stalling error",
549550
protocol: "http",
550-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
551+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
551552
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "ftp://")
552553
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
553554
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@@ -569,7 +570,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
569570
{
570571
name: "Missing secret returns FetchFailed=True and returns error",
571572
protocol: "http",
572-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
573+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
573574
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing"}
574575
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
575576
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@@ -598,7 +599,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
598599
"username": []byte("git"),
599600
},
600601
},
601-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
602+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
602603
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "malformed-basic-auth"}
603604
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
604605
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@@ -617,12 +618,11 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
617618
},
618619
},
619620
{
620-
name: "Stored index with same digest and revision",
621+
name: "Stored index with same revision",
621622
protocol: "http",
622-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
623+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
623624
obj.Status.Artifact = &sourcev1.Artifact{
624625
Revision: rev.String(),
625-
Digest: dig.String(),
626626
}
627627

628628
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
@@ -642,41 +642,15 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
642642
want: sreconcile.ResultSuccess,
643643
},
644644
{
645-
name: "Stored index with different digest and same revision",
645+
name: "Stored index with different revision",
646646
protocol: "http",
647-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
648-
obj.Status.Artifact = &sourcev1.Artifact{
649-
Revision: rev.String(),
650-
Digest: "sha256:80bb3dd67c63095d985850459834ea727603727a370079de90d221191d375a86",
651-
}
652-
653-
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
654-
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
655-
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "foo", "bar")
656-
},
657-
assertConditions: []metav1.Condition{
658-
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
659-
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
660-
},
661-
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
662-
t.Expect(chartRepo.Path).ToNot(BeEmpty())
663-
t.Expect(chartRepo.Index).ToNot(BeNil())
664-
665-
t.Expect(artifact.Revision).To(Equal(obj.Status.Artifact.Revision))
666-
t.Expect(artifact.Digest).ToNot(Equal(obj.Status.Artifact.Digest))
667-
},
668-
want: sreconcile.ResultSuccess,
669-
},
670-
{
671-
name: "Stored index with different revision and digest",
672-
protocol: "http",
673-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
647+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
674648
obj.Status.Artifact = &sourcev1.Artifact{
675649
Revision: "80bb3dd67c63095d985850459834ea727603727a370079de90d221191d375a86",
676-
Digest: "sha256:80bb3dd67c63095d985850459834ea727603727a370079de90d221191d375a86",
677650
}
678651
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
679652
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
653+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "foo", "bar")
680654
},
681655
assertConditions: []metav1.Condition{
682656
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
@@ -689,14 +663,13 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
689663

690664
t.Expect(artifact.Path).To(Not(BeEmpty()))
691665
t.Expect(artifact.Revision).ToNot(Equal(obj.Status.Artifact.Revision))
692-
t.Expect(artifact.Digest).ToNot(Equal(obj.Status.Artifact.Digest))
693666
},
694667
want: sreconcile.ResultSuccess,
695668
},
696669
{
697670
name: "Existing artifact makes ArtifactOutdated=True",
698671
protocol: "http",
699-
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
672+
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
700673
obj.Status.Artifact = &sourcev1.Artifact{
701674
Path: "some-path",
702675
Revision: "some-rev",
@@ -806,14 +779,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
806779
}
807780
g.Expect(err).ToNot(HaveOccurred())
808781

809-
// NOTE: digest will be empty in beforeFunc for invalid repo
810-
// configurations as the client can't get the repo.
811-
var rev, dig digest.Digest
782+
var rev digest.Digest
812783
if validSecret {
813784
g.Expect(newChartRepo.CacheIndex()).To(Succeed())
814-
dig = newChartRepo.Digest(intdigest.Canonical)
815-
816-
g.Expect(newChartRepo.LoadFromPath()).To(Succeed())
817785
rev = newChartRepo.Digest(intdigest.Canonical)
818786
}
819787

@@ -825,7 +793,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
825793
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
826794
}
827795
if tt.beforeFunc != nil {
828-
tt.beforeFunc(g, obj, rev, dig)
796+
tt.beforeFunc(g, obj, rev)
829797
}
830798

831799
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
@@ -866,11 +834,17 @@ func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
866834
assertConditions []metav1.Condition
867835
}{
868836
{
869-
name: "Archiving artifact to storage makes ArtifactInStorage=True",
837+
name: "Archiving artifact to storage makes ArtifactInStorage=True and artifact is stored as JSON",
870838
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository) {
871839
obj.Spec.Interval = metav1.Duration{Duration: interval}
872840
},
873841
want: sreconcile.ResultSuccess,
842+
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, cache *cache.Cache) {
843+
localPath := testStorage.LocalPath(*obj.GetArtifact())
844+
b, err := os.ReadFile(localPath)
845+
t.Expect(err).To(Not(HaveOccurred()))
846+
t.Expect(json.Valid(b)).To(BeTrue())
847+
},
874848
assertConditions: []metav1.Condition{
875849
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact: revision 'existing'"),
876850
},
@@ -970,17 +944,9 @@ func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
970944
},
971945
}
972946

973-
tmpDir := t.TempDir()
974-
975-
// Create an empty cache file.
976-
cachePath := filepath.Join(tmpDir, "index.yaml")
977-
cacheFile, err := os.Create(cachePath)
978-
g.Expect(err).ToNot(HaveOccurred())
979-
g.Expect(cacheFile.Close()).ToNot(HaveOccurred())
980-
981947
chartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
982948
g.Expect(err).ToNot(HaveOccurred())
983-
chartRepo.Path = cachePath
949+
chartRepo.Index = &repo.IndexFile{}
984950

985951
artifact := testStorage.NewArtifactFor(obj.Kind, obj, "existing", "foo.tar.gz")
986952
// Digest of the index file calculated by the ChartRepository.

internal/helm/repository/chart_repository.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"context"
2222
"crypto/tls"
23+
"encoding/json"
2324
"errors"
2425
"fmt"
2526
"io"
@@ -76,7 +77,7 @@ func IndexFromBytes(b []byte) (*repo.IndexFile, error) {
7677
}
7778

7879
i := &repo.IndexFile{}
79-
if err := yaml.UnmarshalStrict(b, i); err != nil {
80+
if err := jsonOrYamlUnmarshal(b, i); err != nil {
8081
return nil, err
8182
}
8283

@@ -401,6 +402,15 @@ func (r *ChartRepository) Digest(algorithm digest.Algorithm) digest.Digest {
401402
return r.digests[algorithm]
402403
}
403404

405+
// ToJSON returns the index formatted as JSON.
406+
func (r *ChartRepository) ToJSON() ([]byte, error) {
407+
if !r.HasIndex() {
408+
return nil, fmt.Errorf("index not loaded yet")
409+
}
410+
411+
return json.MarshalIndent(r.Index, "", " ")
412+
}
413+
404414
// HasIndex returns true if the Index is not nil.
405415
func (r *ChartRepository) HasIndex() bool {
406416
r.RLock()
@@ -459,3 +469,20 @@ func (r *ChartRepository) VerifyChart(_ context.Context, _ *repo.ChartVersion) e
459469
// this is a no-op because this is not implemented yet.
460470
return fmt.Errorf("not implemented")
461471
}
472+
473+
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
474+
// into the provided interface.
475+
//
476+
// It automatically detects whether the data is in JSON or YAML format by
477+
// checking its validity as JSON. If the data is valid JSON, it will use the
478+
// `encoding/json` package to unmarshal it. Otherwise, it will use the
479+
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
480+
//
481+
// Can potentially be replaced when Helm PR for JSON support has been merged.
482+
// xref: https://github.com/helm/helm/pull/12245
483+
func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
484+
if json.Valid(b) {
485+
return json.Unmarshal(b, i)
486+
}
487+
return yaml.UnmarshalStrict(b, i)
488+
}

0 commit comments

Comments
 (0)