diff --git a/controllers/dependency_manager.go b/controllers/dependency_manager.go new file mode 100644 index 00000000..22a3dd8e --- /dev/null +++ b/controllers/dependency_manager.go @@ -0,0 +1,142 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/fluxcd/source-controller/internal/helm" + "golang.org/x/sync/errgroup" + helmchart "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" +) + +// DependencyWithRepository is a container for a dependency and its respective +// repository +type DependencyWithRepository struct { + Dependency *helmchart.Dependency + Repo *helm.ChartRepository +} + +// DependencyManager manages dependencies for helm charts +type DependencyManager struct { + Chart *helmchart.Chart + ChartPath string + Dependencies []*DependencyWithRepository +} + +// Build compiles and builds the chart dependencies +func (dm *DependencyManager) Build() error { + if dm.Dependencies == nil { + return nil + } + + ctx := context.Background() + errs, ctx := errgroup.WithContext(ctx) + + for _, item := range dm.Dependencies { + dep := item.Dependency + chartRepo := item.Repo + errs.Go(func() error { + var ( + ch *helmchart.Chart + err error + ) + if strings.HasPrefix(dep.Repository, "file://") { + ch, err = chartForLocalDependency(dep, dm.ChartPath) + } else { + ch, err = chartForRemoteDependency(dep, chartRepo) + } + if err != nil { + return err + } + dm.Chart.AddDependency(ch) + return nil + }) + } + + return errs.Wait() +} + +func chartForLocalDependency(dep *helmchart.Dependency, cp string) (*helmchart.Chart, error) { + origPath, err := filepath.Abs(path.Join(cp, strings.TrimPrefix(dep.Repository, "file://"))) + if err != nil { + return nil, err + } + + if _, err := os.Stat(origPath); os.IsNotExist(err) { + err := fmt.Errorf("chart path %s not found: %w", origPath, err) + return nil, err + } else if err != nil { + return nil, err + } + + ch, err := loader.Load(origPath) + if err != nil { + return nil, err + } + + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + err := fmt.Errorf("dependency %s has an invalid version/constraint format: %w", dep.Name, err) + return nil, err + } + + v, err := semver.NewVersion(ch.Metadata.Version) + if err != nil { + return nil, err + } + + if !constraint.Check(v) { + err = fmt.Errorf("can't get a valid version for dependency %s", dep.Name) + return nil, err + } + + return ch, nil +} + +func chartForRemoteDependency(dep *helmchart.Dependency, chartrepo *helm.ChartRepository) (*helmchart.Chart, error) { + if chartrepo == nil { + err := fmt.Errorf("chartrepo should not be nil") + return nil, err + } + + // Lookup the chart version in the chart repository index + chartVer, err := chartrepo.Get(dep.Name, dep.Version) + if err != nil { + return nil, err + } + + // Download chart + res, err := chartrepo.DownloadChart(chartVer) + if err != nil { + return nil, err + } + + ch, err := loader.LoadArchive(res) + if err != nil { + return nil, err + } + + return ch, nil +} diff --git a/controllers/dependency_manager_test.go b/controllers/dependency_manager_test.go new file mode 100644 index 00000000..80b1df91 --- /dev/null +++ b/controllers/dependency_manager_test.go @@ -0,0 +1,203 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" + + "github.com/fluxcd/source-controller/internal/helm" + helmchart "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" +) + +var ( + helmPackageFile = "testdata/charts/helmchart-0.1.0.tgz" + + localDepFixture helmchart.Dependency = helmchart.Dependency{ + Name: "helmchart", + Version: "0.1.0", + Repository: "file://../helmchart", + } + remoteDepFixture helmchart.Dependency = helmchart.Dependency{ + Name: "helmchart", + Version: "0.1.0", + Repository: "https://example.com/charts", + } + chartFixture helmchart.Chart = helmchart.Chart{ + Metadata: &helmchart.Metadata{ + Name: "test", + }, + } +) + +func TestBuild_WithEmptyDependencies(t *testing.T) { + dm := DependencyManager{ + Dependencies: nil, + } + if err := dm.Build(); err != nil { + t.Errorf("Build() should return nil") + } +} + +func TestBuild_WithLocalChart(t *testing.T) { + loc := localDepFixture + chart := chartFixture + dm := DependencyManager{ + Chart: &chart, + ChartPath: "testdata/charts/helmchart", + Dependencies: []*DependencyWithRepository{ + { + Dependency: &loc, + Repo: nil, + }, + }, + } + + if err := dm.Build(); err != nil { + t.Errorf("Build() expected to not return error: %s", err) + } + + deps := dm.Chart.Dependencies() + if len(deps) != 1 { + t.Fatalf("chart expected to have one dependency registered") + } + if deps[0].Metadata.Name != localDepFixture.Name { + t.Errorf("chart dependency has incorrect name, expected: %s, got: %s", localDepFixture.Name, deps[0].Metadata.Name) + } + if deps[0].Metadata.Version != localDepFixture.Version { + t.Errorf("chart dependency has incorrect version, expected: %s, got: %s", localDepFixture.Version, deps[0].Metadata.Version) + } + + tests := []struct { + name string + dep helmchart.Dependency + expectError string + }{ + { + name: "invalid path", + dep: helmchart.Dependency{ + Name: "helmchart", + Version: "0.1.0", + Repository: "file://../invalid", + }, + expectError: "no such file or directory", + }, + { + name: "invalid version constraint format", + dep: helmchart.Dependency{ + Name: "helmchart", + Version: "!2.0", + Repository: "file://../helmchart", + }, + expectError: "has an invalid version/constraint format", + }, + { + name: "invalid version", + dep: helmchart.Dependency{ + Name: "helmchart", + Version: "1.0.0", + Repository: "file://../helmchart", + }, + expectError: "can't get a valid version for dependency", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := chartFixture + dm = DependencyManager{ + Chart: &c, + ChartPath: "testdata/charts/helmchart", + Dependencies: []*DependencyWithRepository{ + { + Dependency: &tt.dep, + Repo: nil, + }, + }, + } + + if err := dm.Build(); err == nil { + t.Errorf("Build() expected to return error") + } else if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Build() expected to return error: %s, got: %s", tt.expectError, err) + } + if len(dm.Chart.Dependencies()) > 0 { + t.Fatalf("chart expected to have no dependencies registered") + } + }) + } +} + +func TestBuild_WithRemoteChart(t *testing.T) { + chart := chartFixture + b, err := ioutil.ReadFile(helmPackageFile) + if err != nil { + t.Fatal(err) + } + i := repo.NewIndexFile() + i.Add(&helmchart.Metadata{Name: "helmchart", Version: "0.1.0"}, "helmchart-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") + mg := mockGetter{response: b} + cr := &helm.ChartRepository{ + URL: remoteDepFixture.Repository, + Index: i, + Client: &mg, + } + dm := DependencyManager{ + Chart: &chart, + Dependencies: []*DependencyWithRepository{ + { + Dependency: &remoteDepFixture, + Repo: cr, + }, + }, + } + + if err := dm.Build(); err != nil { + t.Errorf("Build() expected to not return error: %s", err) + } + + deps := dm.Chart.Dependencies() + if len(deps) != 1 { + t.Fatalf("chart expected to have one dependency registered") + } + if deps[0].Metadata.Name != remoteDepFixture.Name { + t.Errorf("chart dependency has incorrect name, expected: %s, got: %s", remoteDepFixture.Name, deps[0].Metadata.Name) + } + if deps[0].Metadata.Version != remoteDepFixture.Version { + t.Errorf("chart dependency has incorrect version, expected: %s, got: %s", remoteDepFixture.Version, deps[0].Metadata.Version) + } + + // When repo is not set + dm.Dependencies[0].Repo = nil + if err := dm.Build(); err == nil { + t.Errorf("Build() expected to return error") + } else if !strings.Contains(err.Error(), "chartrepo should not be nil") { + t.Errorf("Build() expected to return different error, got: %s", err) + } +} + +type mockGetter struct { + response []byte +} + +func (g *mockGetter) Get(url string, options ...getter.Option) (*bytes.Buffer, error) { + return bytes.NewBuffer(g.response), nil +} diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 3567f5de..398a8860 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -28,6 +28,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/go-logr/logr" + helmchart "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/getter" @@ -433,18 +434,130 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context, // Either (re)package the chart with the declared default values file, // or write the chart directly to storage. pkgPath := chartPath - isDir := chartFileInfo.IsDir() - if isDir || (chart.Spec.ValuesFile != "" && chart.Spec.ValuesFile != chartutil.ValuesfileName) { + isValuesFileOverriden := false + if chart.Spec.ValuesFile != "" && chart.Spec.ValuesFile != chartutil.ValuesfileName { // Overwrite default values if configured - if changed, err := helm.OverwriteChartDefaultValues(helmChart, chart.Spec.ValuesFile); err != nil { + isValuesFileOverriden, err = helm.OverwriteChartDefaultValues(helmChart, chart.Spec.ValuesFile) + if err != nil { return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err - } else if isDir || changed { - // Package the chart - pkgPath, err = chartutil.Save(helmChart, tmpDir) - if err != nil { - err = fmt.Errorf("chart package error: %w", err) - return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err + } + } + + isDir := chartFileInfo.IsDir() + switch { + case isDir: + // Load dependencies + if err = chartutil.ProcessDependencies(helmChart, helmChart.Values); err != nil { + err = fmt.Errorf("failed to process chart dependencies: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + // Determine chart dependencies + deps := helmChart.Dependencies() + reqs := helmChart.Metadata.Dependencies + lock := helmChart.Lock + if lock != nil { + // Load from lockfile if exists + reqs = lock.Dependencies + } + var dwr []*DependencyWithRepository + for _, dep := range reqs { + // Exclude existing dependencies + for _, existing := range deps { + if existing.Name() == dep.Name { + continue + } } + + // Continue loop if file scheme detected + if strings.HasPrefix(dep.Repository, "file://") { + dwr = append(dwr, &DependencyWithRepository{ + Dependency: dep, + Repo: nil, + }) + continue + } + + // Discover existing HelmRepository by URL + repository, err := r.resolveDependencyRepository(ctx, dep, chart.Namespace) + if err != nil { + repository = &sourcev1.HelmRepository{ + Spec: sourcev1.HelmRepositorySpec{ + URL: dep.Repository, + }, + } + } + + // Configure ChartRepository getter options + var clientOpts []getter.Option + if secret, err := r.getHelmRepositorySecret(ctx, repository); err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err + } else if secret != nil { + opts, cleanup, err := helm.ClientOptionsFromSecret(*secret) + if err != nil { + err = fmt.Errorf("auth options error: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err + } + defer cleanup() + + clientOpts = opts + } + + // Initialize the chart repository and load the index file + chartRepo, err := helm.NewChartRepository(repository.Spec.URL, r.Getters, clientOpts) + if err != nil { + switch err.(type) { + case *url.Error: + return sourcev1.HelmChartNotReady(chart, sourcev1.URLInvalidReason, err.Error()), err + default: + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + } + if repository.Status.Artifact != nil { + indexFile, err := os.Open(r.Storage.LocalPath(*repository.GetArtifact())) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + b, err := ioutil.ReadAll(indexFile) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + if err = chartRepo.LoadIndex(b); err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + } else { + // Download index + err = chartRepo.DownloadIndex() + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + } + + dwr = append(dwr, &DependencyWithRepository{ + Dependency: dep, + Repo: chartRepo, + }) + } + + // Construct dependencies for chart if any + if len(dwr) > 0 { + dm := &DependencyManager{ + Chart: helmChart, + ChartPath: chartPath, + Dependencies: dwr, + } + err = dm.Build() + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + } + + fallthrough + case isValuesFileOverriden: + pkgPath, err = chartutil.Save(helmChart, tmpDir) + if err != nil { + err = fmt.Errorf("chart package error: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err } } @@ -590,6 +703,28 @@ func (r *HelmChartReconciler) indexHelmRepositoryByURL(o runtime.Object) []strin return nil } +func (r *HelmChartReconciler) resolveDependencyRepository(ctx context.Context, dep *helmchart.Dependency, namespace string) (*sourcev1.HelmRepository, error) { + url := helm.NormalizeChartRepositoryURL(dep.Repository) + if url == "" { + return nil, fmt.Errorf("invalid repository URL") + } + + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingField(sourcev1.HelmRepositoryURLIndexKey, url), + } + var list sourcev1.HelmRepositoryList + err := r.Client.List(ctx, &list, listOpts...) + if err != nil { + return nil, fmt.Errorf("unable to retrieve HelmRepositoryList: %w", err) + } + if len(list.Items) > 0 { + return &list.Items[0], nil + } + + return nil, fmt.Errorf("no HelmRepository found") +} + func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *sourcev1.HelmRepository) (*corev1.Secret, error) { if repository.Spec.SecretRef != nil { name := types.NamespacedName{ diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 24824ccf..d5e5c500 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -693,4 +693,247 @@ var _ = Describe("HelmChartReconciler", func() { }, timeout, interval).Should(BeTrue()) }) }) + + Context("HelmChart from GitRepository with HelmRepository dependency", func() { + var ( + namespace *corev1.Namespace + gitServer *gittestserver.GitServer + helmServer *helmtestserver.HelmServer + err error + ) + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-git-repository-" + randStringRunes(5)}, + } + err = k8sClient.Create(context.Background(), namespace) + Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + gitServer, err = gittestserver.NewTempGitServer() + Expect(err).NotTo(HaveOccurred()) + gitServer.AutoCreate() + Expect(gitServer.StartHTTP()).To(Succeed()) + + helmServer, err = helmtestserver.NewTempHelmServer() + Expect(err).To(Succeed()) + helmServer.Start() + }) + + AfterEach(func() { + gitServer.StopHTTP() + os.RemoveAll(gitServer.Root()) + + os.RemoveAll(helmServer.Root()) + helmServer.Stop() + + err = k8sClient.Delete(context.Background(), namespace) + Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") + }) + + It("Creates artifacts for", func() { + helmServer.Stop() + var username, password = "john", "doe" + helmServer.WithMiddleware(func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u, p, ok := r.BasicAuth() + if !ok || username != u || password != p { + w.WriteHeader(401) + return + } + handler.ServeHTTP(w, r) + }) + }) + helmServer.Start() + + Expect(helmServer.PackageChart(path.Join("testdata/charts/helmchart"))).Should(Succeed()) + Expect(helmServer.GenerateIndex()).Should(Succeed()) + + secretKey := types.NamespacedName{ + Name: "helmrepository-auth-" + randStringRunes(5), + Namespace: namespace.Name, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretKey.Name, + Namespace: secretKey.Namespace, + }, + Data: map[string][]byte{ + "username": []byte(username), + "password": []byte(password), + }, + } + Expect(k8sClient.Create(context.Background(), secret)).Should(Succeed()) + + By("Creating repository and waiting for artifact") + helmRepositoryKey := types.NamespacedName{ + Name: "helmrepository-sample-" + randStringRunes(5), + Namespace: namespace.Name, + } + helmRepository := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: helmRepositoryKey.Name, + Namespace: helmRepositoryKey.Namespace, + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: helmServer.URL(), + SecretRef: &corev1.LocalObjectReference{ + Name: secretKey.Name, + }, + Interval: metav1.Duration{Duration: pullInterval}, + }, + } + Expect(k8sClient.Create(context.Background(), helmRepository)).Should(Succeed()) + defer k8sClient.Delete(context.Background(), helmRepository) + + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), helmRepositoryKey, helmRepository) + return helmRepository.Status.Artifact != nil + }, timeout, interval).Should(BeTrue()) + + fs := memfs.New() + gitrepo, err := git.Init(memory.NewStorage(), fs) + Expect(err).NotTo(HaveOccurred()) + + wt, err := gitrepo.Worktree() + Expect(err).NotTo(HaveOccurred()) + + u, err := url.Parse(gitServer.HTTPAddress()) + Expect(err).NotTo(HaveOccurred()) + u.Path = path.Join(u.Path, fmt.Sprintf("repository-%s.git", randStringRunes(5))) + + _, err = gitrepo.CreateRemote(&config.RemoteConfig{ + Name: "origin", + URLs: []string{u.String()}, + }) + Expect(err).NotTo(HaveOccurred()) + + chartDir := "testdata/charts/helmchartwithdeps" + Expect(filepath.Walk(chartDir, func(p string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + switch { + case fi.Mode().IsDir(): + return fs.MkdirAll(p, os.ModeDir) + case !fi.Mode().IsRegular(): + return nil + } + + b, err := ioutil.ReadFile(p) + if err != nil { + return err + } + + ff, err := fs.Create(p) + if err != nil { + return err + } + if _, err := ff.Write(b); err != nil { + return err + } + _ = ff.Close() + _, err = wt.Add(p) + + return err + })).To(Succeed()) + + By("Configuring the chart dependency") + filePath := fs.Join(chartDir, chartutil.ChartfileName) + f, err := fs.OpenFile(filePath, os.O_RDWR, os.FileMode(0600)) + Expect(err).NotTo(HaveOccurred()) + + b := make([]byte, 2048) + n, err := f.Read(b) + Expect(err).NotTo(HaveOccurred()) + b = b[0:n] + + err = f.Close() + Expect(err).NotTo(HaveOccurred()) + + y := new(helmchart.Metadata) + err = yaml.Unmarshal(b, y) + Expect(err).NotTo(HaveOccurred()) + + y.Dependencies = []*helmchart.Dependency{ + { + Name: "helmchart", + Version: ">=0.1.0", + Repository: helmRepository.Spec.URL, + }, + } + + b, err = yaml.Marshal(y) + Expect(err).NotTo(HaveOccurred()) + + ff, err := fs.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + + _, err = ff.Write(b) + Expect(err).NotTo(HaveOccurred()) + + err = ff.Close() + Expect(err).NotTo(HaveOccurred()) + + _, err = wt.Commit("Helm charts", &git.CommitOptions{ + Author: &object.Signature{ + Name: "John Doe", + Email: "john@example.com", + When: time.Now(), + }, + All: true, + }) + Expect(err).NotTo(HaveOccurred()) + + err = gitrepo.Push(&git.PushOptions{}) + Expect(err).NotTo(HaveOccurred()) + + repositoryKey := types.NamespacedName{ + Name: fmt.Sprintf("git-repository-sample-%s", randStringRunes(5)), + Namespace: namespace.Name, + } + repository := &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repositoryKey.Name, + Namespace: repositoryKey.Namespace, + }, + Spec: sourcev1.GitRepositorySpec{ + URL: u.String(), + Interval: metav1.Duration{Duration: indexInterval}, + }, + } + Expect(k8sClient.Create(context.Background(), repository)).Should(Succeed()) + defer k8sClient.Delete(context.Background(), repository) + + key := types.NamespacedName{ + Name: "helmchart-sample-" + randStringRunes(5), + Namespace: namespace.Name, + } + chart := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: sourcev1.HelmChartSpec{ + Chart: "testdata/charts/helmchartwithdeps", + Version: "*", + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repositoryKey.Name, + }, + Interval: metav1.Duration{Duration: pullInterval}, + }, + } + Expect(k8sClient.Create(context.Background(), chart)).Should(Succeed()) + defer k8sClient.Delete(context.Background(), chart) + + By("Expecting artifact") + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact != nil && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + }) + }) }) diff --git a/controllers/testdata/charts/helmchart-0.1.0.tgz b/controllers/testdata/charts/helmchart-0.1.0.tgz new file mode 100644 index 00000000..f64a32ee Binary files /dev/null and b/controllers/testdata/charts/helmchart-0.1.0.tgz differ diff --git a/go.mod b/go.mod index 2f60de8e..7cc9fa3c 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/minio/minio-go/v7 v7.0.5 github.com/onsi/ginkgo v1.12.1 github.com/onsi/gomega v1.10.1 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e helm.sh/helm/v3 v3.3.4 k8s.io/api v0.18.9 k8s.io/apimachinery v0.18.9