Skip to content

Commit 0c0095c

Browse files
committed
Adding helmChart reconciler test as well as e2e test
Signed-off-by: Soule BA <soule@weave.works>
1 parent 68da92e commit 0c0095c

File tree

8 files changed

+310
-34
lines changed

8 files changed

+310
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
apiVersion: source.toolkit.fluxcd.io/v1beta2
3+
kind: HelmRepository
4+
metadata:
5+
name: podinfo
6+
spec:
7+
url: oci://ghcr.io/stefanprodan/charts
8+
type: "oci"
9+
interval: 1m
10+
---
11+
apiVersion: source.toolkit.fluxcd.io/v1beta2
12+
kind: HelmChart
13+
metadata:
14+
name: podinfo
15+
spec:
16+
chart: podinfo
17+
sourceRef:
18+
kind: HelmRepository
19+
name: podinfo
20+
version: '6.1.*'
21+
interval: 1m

controllers/helmchart_controller.go

+52-24
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
453453
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
454454
helmgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
455455
}
456-
if secret, err := r.getHelmRepositorySecret(ctx, repo); secret != nil || err != nil {
456+
secret, err := r.getHelmRepositorySecret(ctx, repo)
457+
if secret != nil || err != nil {
457458
if err != nil {
458459
e := &serror.Event{
459460
Err: fmt.Errorf("failed to get secret '%s': %w", repo.Spec.SecretRef.Name, err),
@@ -491,15 +492,43 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
491492

492493
// Initialize the chart repository
493494
var chartRepo chart.Remote
494-
var err error
495-
if registry.IsOCI(repo.Spec.URL) {
496-
chartRepo, err = repository.NewOCIChartRepository(repo.Spec.URL, repository.WithOCIGetter(r.Getters), repository.WithOCIRegistryClient(r.RegistryClient))
495+
if repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
496+
if !registry.IsOCI(repo.Spec.URL) {
497+
err = fmt.Errorf("invalid OCI registry URL: %s", repo.Spec.URL)
498+
return chartRepoErrorReturn(err, obj)
499+
}
500+
// Tell the chart repository to use the OCI client with the configured getter
501+
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(r.RegistryClient))
502+
ociChartRepo, err := repository.NewOCIChartRepository(repo.Spec.URL, repository.WithOCIGetter(r.Getters), repository.WithOCIGetterOptions(clientOpts), repository.WithOCIRegistryClient(r.RegistryClient))
503+
if err != nil {
504+
return chartRepoErrorReturn(err, obj)
505+
}
506+
chartRepo = ociChartRepo
507+
508+
// If login options are configured, use them to login to the registry
509+
// The OCIGetter will later retrieve the stored credentials to pull the chart
510+
if secret != nil {
511+
// Construct actual options
512+
logOpt, err := loginOptionFromSecret(*secret)
513+
if err != nil {
514+
return chartRepoErrorReturn(err, obj)
515+
}
516+
517+
logOpts := append([]registry.LoginOption{}, logOpt)
518+
err = ociChartRepo.Login(logOpts...)
519+
if err != nil {
520+
return chartRepoErrorReturn(err, obj)
521+
}
522+
}
497523
} else {
498524
var httpChartRepo *repository.ChartRepository
499525
httpChartRepo, err = repository.NewChartRepository(repo.Spec.URL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts,
500526
repository.WithMemoryCache(r.Storage.LocalPath(*repo.GetArtifact()), r.Cache, r.TTL, func(event string) {
501527
r.IncCacheEvents(event, obj.Name, obj.Namespace)
502528
}))
529+
if err != nil {
530+
return chartRepoErrorReturn(err, obj)
531+
}
503532
chartRepo = httpChartRepo
504533
defer func() {
505534
if httpChartRepo == nil {
@@ -523,26 +552,6 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
523552
}
524553
}()
525554
}
526-
if err != nil {
527-
// Any error requires a change in generation,
528-
// which we should be informed about by the watcher
529-
switch err.(type) {
530-
case *url.Error:
531-
e := &serror.Stalling{
532-
Err: fmt.Errorf("invalid Helm repository URL: %w", err),
533-
Reason: sourcev1.URLInvalidReason,
534-
}
535-
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
536-
return sreconcile.ResultEmpty, e
537-
default:
538-
e := &serror.Stalling{
539-
Err: fmt.Errorf("failed to construct Helm client: %w", err),
540-
Reason: meta.FailedReason,
541-
}
542-
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
543-
return sreconcile.ResultEmpty, e
544-
}
545-
}
546555

547556
// Construct the chart builder with scoped configuration
548557
cb := chart.NewRemoteBuilder(chartRepo)
@@ -1106,3 +1115,22 @@ func reasonForBuild(build *chart.Build) string {
11061115
}
11071116
return sourcev1.ChartPullSucceededReason
11081117
}
1118+
1119+
func chartRepoErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.Result, error) {
1120+
switch err.(type) {
1121+
case *url.Error:
1122+
e := &serror.Stalling{
1123+
Err: fmt.Errorf("invalid Helm repository URL: %w", err),
1124+
Reason: sourcev1.URLInvalidReason,
1125+
}
1126+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
1127+
return sreconcile.ResultEmpty, e
1128+
default:
1129+
e := &serror.Stalling{
1130+
Err: fmt.Errorf("failed to construct Helm client: %w", err),
1131+
Reason: meta.FailedReason,
1132+
}
1133+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
1134+
return sreconcile.ResultEmpty, e
1135+
}
1136+
}

controllers/helmchart_controller_test.go

+225-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"errors"
2223
"fmt"
2324
"io"
25+
"io/ioutil"
2426
"net/http"
2527
"os"
2628
"path/filepath"
@@ -31,6 +33,8 @@ import (
3133

3234
"github.com/darkowlzz/controller-check/status"
3335
. "github.com/onsi/gomega"
36+
"helm.sh/helm/v3/pkg/chart/loader"
37+
"helm.sh/helm/v3/pkg/registry"
3438
corev1 "k8s.io/api/core/v1"
3539
apierrors "k8s.io/apimachinery/pkg/api/errors"
3640
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -45,12 +49,12 @@ import (
4549
"github.com/fluxcd/pkg/runtime/conditions"
4650
"github.com/fluxcd/pkg/runtime/patch"
4751
"github.com/fluxcd/pkg/testserver"
48-
4952
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
5053
serror "github.com/fluxcd/source-controller/internal/error"
5154
"github.com/fluxcd/source-controller/internal/helm/chart"
5255
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
5356
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
57+
hchart "helm.sh/helm/v3/pkg/chart"
5458
)
5559

5660
func TestHelmChartReconciler_Reconcile(t *testing.T) {
@@ -776,6 +780,217 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) {
776780
}
777781
}
778782

783+
func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
784+
g := NewWithT(t)
785+
786+
tmpDir := t.TempDir()
787+
788+
const (
789+
chartPath = "testdata/charts/helmchart-0.1.0.tgz"
790+
)
791+
792+
// Login to the registry
793+
err := testRegistryserver.RegistryClient.Login(testRegistryserver.DockerRegistryHost,
794+
registry.LoginOptBasicAuth(testUsername, testPassword),
795+
registry.LoginOptInsecure(true))
796+
g.Expect(err).NotTo(HaveOccurred())
797+
798+
// Load a test chart
799+
chartData, err := ioutil.ReadFile(chartPath)
800+
g.Expect(err).NotTo(HaveOccurred())
801+
metadata, err := extractChartMeta(chartData)
802+
g.Expect(err).NotTo(HaveOccurred())
803+
804+
// Upload the test chart
805+
ref := fmt.Sprintf("%s/testrepo/%s:%s", testRegistryserver.DockerRegistryHost, metadata.Name, metadata.Version)
806+
_, err = testRegistryserver.RegistryClient.Push(chartData, ref)
807+
g.Expect(err).NotTo(HaveOccurred())
808+
809+
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
810+
g.Expect(err).ToNot(HaveOccurred())
811+
812+
cachedArtifact := &sourcev1.Artifact{
813+
Revision: "0.1.0",
814+
Path: metadata.Name + "-" + metadata.Version + ".tgz",
815+
}
816+
g.Expect(storage.CopyFromPath(cachedArtifact, "testdata/charts/helmchart-0.1.0.tgz")).To(Succeed())
817+
818+
tests := []struct {
819+
name string
820+
secret *corev1.Secret
821+
beforeFunc func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository)
822+
want sreconcile.Result
823+
wantErr error
824+
assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build)
825+
cleanFunc func(g *WithT, build *chart.Build)
826+
}{
827+
{
828+
name: "Reconciles chart build with repository credentials",
829+
secret: &corev1.Secret{
830+
ObjectMeta: metav1.ObjectMeta{
831+
Name: "auth",
832+
},
833+
Data: map[string][]byte{
834+
"username": []byte(testUsername),
835+
"password": []byte(testPassword),
836+
},
837+
},
838+
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
839+
obj.Spec.Chart = metadata.Name
840+
obj.Spec.Version = metadata.Version
841+
repository.Spec.SecretRef = &meta.LocalObjectReference{Name: "auth"}
842+
},
843+
want: sreconcile.ResultSuccess,
844+
assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) {
845+
g.Expect(build.Name).To(Equal(metadata.Name))
846+
g.Expect(build.Version).To(Equal(metadata.Version))
847+
g.Expect(build.Path).ToNot(BeEmpty())
848+
g.Expect(build.Path).To(BeARegularFile())
849+
},
850+
cleanFunc: func(g *WithT, build *chart.Build) {
851+
g.Expect(os.Remove(build.Path)).To(Succeed())
852+
},
853+
},
854+
{
855+
name: "Uses artifact as build cache",
856+
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
857+
obj.Spec.Chart = metadata.Name
858+
obj.Spec.Version = metadata.Version
859+
obj.Status.Artifact = &sourcev1.Artifact{Path: metadata.Name + "-" + metadata.Version + ".tgz"}
860+
},
861+
want: sreconcile.ResultSuccess,
862+
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
863+
g.Expect(build.Name).To(Equal(metadata.Name))
864+
g.Expect(build.Version).To(Equal(metadata.Version))
865+
g.Expect(build.Path).To(Equal(storage.LocalPath(*cachedArtifact.DeepCopy())))
866+
g.Expect(build.Path).To(BeARegularFile())
867+
},
868+
},
869+
{
870+
name: "Forces build on generation change",
871+
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
872+
obj.Generation = 3
873+
obj.Spec.Chart = metadata.Name
874+
obj.Spec.Version = metadata.Version
875+
876+
obj.Status.ObservedGeneration = 2
877+
obj.Status.Artifact = &sourcev1.Artifact{Path: metadata.Name + "-" + metadata.Version + ".tgz"}
878+
},
879+
want: sreconcile.ResultSuccess,
880+
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
881+
g.Expect(build.Name).To(Equal(metadata.Name))
882+
g.Expect(build.Version).To(Equal(metadata.Version))
883+
fmt.Println("buildpath", build.Path)
884+
fmt.Println("storage Path", storage.LocalPath(*cachedArtifact.DeepCopy()))
885+
g.Expect(build.Path).ToNot(Equal(storage.LocalPath(*cachedArtifact.DeepCopy())))
886+
g.Expect(build.Path).To(BeARegularFile())
887+
},
888+
cleanFunc: func(g *WithT, build *chart.Build) {
889+
g.Expect(os.Remove(build.Path)).To(Succeed())
890+
},
891+
},
892+
{
893+
name: "Event on unsuccessful secret retrieval",
894+
beforeFunc: func(_ *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
895+
repository.Spec.SecretRef = &meta.LocalObjectReference{
896+
Name: "invalid",
897+
}
898+
},
899+
want: sreconcile.ResultEmpty,
900+
wantErr: &serror.Event{Err: errors.New("failed to get secret 'invalid'")},
901+
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
902+
g.Expect(build.Complete()).To(BeFalse())
903+
904+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
905+
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret 'invalid'"),
906+
}))
907+
},
908+
},
909+
{
910+
name: "Stalling on invalid client options",
911+
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
912+
repository.Spec.URL = "https://unsupported" // Unsupported protocol
913+
},
914+
want: sreconcile.ResultEmpty,
915+
wantErr: &serror.Stalling{Err: errors.New("failed to construct Helm client: invalid OCI registry URL: https://unsupported")},
916+
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
917+
g.Expect(build.Complete()).To(BeFalse())
918+
919+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
920+
*conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FailedReason, "failed to construct Helm client"),
921+
}))
922+
},
923+
},
924+
{
925+
name: "BuildError on temporary build error",
926+
beforeFunc: func(obj *sourcev1.HelmChart, _ *sourcev1.HelmRepository) {
927+
obj.Spec.Chart = "invalid"
928+
},
929+
want: sreconcile.ResultEmpty,
930+
wantErr: &chart.BuildError{Err: errors.New("failed to get chart version for remote reference")},
931+
},
932+
}
933+
for _, tt := range tests {
934+
t.Run(tt.name, func(t *testing.T) {
935+
g := NewWithT(t)
936+
937+
testRegistryClient, err = registry.NewClient(registry.ClientOptWriter(os.Stdout))
938+
g.Expect(err).To(BeNil())
939+
940+
clientBuilder := fake.NewClientBuilder()
941+
if tt.secret != nil {
942+
clientBuilder.WithObjects(tt.secret.DeepCopy())
943+
}
944+
945+
r := &HelmChartReconciler{
946+
Client: clientBuilder.Build(),
947+
EventRecorder: record.NewFakeRecorder(32),
948+
Getters: testGetters,
949+
Storage: storage,
950+
RegistryClient: testRegistryClient,
951+
}
952+
953+
repository := &sourcev1.HelmRepository{
954+
ObjectMeta: metav1.ObjectMeta{
955+
GenerateName: "helmrepository-",
956+
},
957+
Spec: sourcev1.HelmRepositorySpec{
958+
URL: fmt.Sprintf("oci://%s/testrepo", testRegistryserver.DockerRegistryHost),
959+
Timeout: &metav1.Duration{Duration: timeout},
960+
Type: sourcev1.HelmRepositoryTypeOCI,
961+
},
962+
}
963+
obj := &sourcev1.HelmChart{
964+
ObjectMeta: metav1.ObjectMeta{
965+
GenerateName: "helmrepository-",
966+
},
967+
Spec: sourcev1.HelmChartSpec{},
968+
}
969+
970+
if tt.beforeFunc != nil {
971+
tt.beforeFunc(obj, repository)
972+
}
973+
974+
var b chart.Build
975+
if tt.cleanFunc != nil {
976+
defer tt.cleanFunc(g, &b)
977+
}
978+
got, err := r.buildFromHelmRepository(context.TODO(), obj, repository, &b)
979+
980+
g.Expect(err != nil).To(Equal(tt.wantErr != nil))
981+
if tt.wantErr != nil {
982+
g.Expect(reflect.TypeOf(err).String()).To(Equal(reflect.TypeOf(tt.wantErr).String()))
983+
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
984+
}
985+
g.Expect(got).To(Equal(tt.want))
986+
987+
if tt.assertFunc != nil {
988+
tt.assertFunc(g, obj, b)
989+
}
990+
})
991+
}
992+
}
993+
779994
func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) {
780995
g := NewWithT(t)
781996

@@ -1690,3 +1905,12 @@ func TestHelmChartReconciler_notify(t *testing.T) {
16901905
})
16911906
}
16921907
}
1908+
1909+
// extractChartMeta is used to extract a chart metadata from a byte array
1910+
func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
1911+
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
1912+
if err != nil {
1913+
return nil, err
1914+
}
1915+
return ch.Metadata, nil
1916+
}

0 commit comments

Comments
 (0)