internal/helm: divide into subpackages

With all the logic that used to reside in the `controllers` package
factored into this package, it became cluttered. This commit tries to
bring a bit more structure in place.

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2021-11-15 22:31:33 +01:00
parent 9abbdd80a6
commit 7d0f79f41b
20 changed files with 397 additions and 364 deletions

View File

@ -28,7 +28,7 @@ import (
securejoin "github.com/cyphar/filepath-securejoin" securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/getter" extgetter "helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
@ -54,7 +54,9 @@ import (
"github.com/fluxcd/pkg/untar" "github.com/fluxcd/pkg/untar"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/internal/helm" "github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/repository"
) )
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts,verbs=get;list;watch;create;update;patch;delete
@ -67,7 +69,7 @@ type HelmChartReconciler struct {
client.Client client.Client
Scheme *runtime.Scheme Scheme *runtime.Scheme
Storage *Storage Storage *Storage
Getters getter.Providers Getters extgetter.Providers
EventRecorder kuberecorder.EventRecorder EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *events.Recorder ExternalEventRecorder *events.Recorder
MetricsRecorder *metrics.Recorder MetricsRecorder *metrics.Recorder
@ -304,218 +306,218 @@ func (r *HelmChartReconciler) getSource(ctx context.Context, chart sourcev1.Helm
return source, nil return source, nil
} }
func (r *HelmChartReconciler) fromHelmRepository(ctx context.Context, repository sourcev1.HelmRepository, func (r *HelmChartReconciler) fromHelmRepository(ctx context.Context, repo sourcev1.HelmRepository, c sourcev1.HelmChart,
chart sourcev1.HelmChart, workDir string, force bool) (sourcev1.HelmChart, error) { workDir string, force bool) (sourcev1.HelmChart, error) {
// Configure ChartRepository getter options // Configure Index getter options
clientOpts := []getter.Option{ clientOpts := []extgetter.Option{
getter.WithURL(repository.Spec.URL), extgetter.WithURL(repo.Spec.URL),
getter.WithTimeout(repository.Spec.Timeout.Duration), extgetter.WithTimeout(repo.Spec.Timeout.Duration),
getter.WithPassCredentialsAll(repository.Spec.PassCredentials), extgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
} }
if secret, err := r.getHelmRepositorySecret(ctx, &repository); err != nil { if secret, err := r.getHelmRepositorySecret(ctx, &repo); err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.AuthenticationFailedReason, err.Error()), err
} else if secret != nil { } else if secret != nil {
// Create temporary working directory for credentials // Create temporary working directory for credentials
authDir := filepath.Join(workDir, "creds") authDir := filepath.Join(workDir, "creds")
if err := os.Mkdir(authDir, 0700); err != nil { if err := os.Mkdir(authDir, 0700); err != nil {
err = fmt.Errorf("failed to create temporary directory for repository credentials: %w", err) err = fmt.Errorf("failed to create temporary directory for repository credentials: %w", err)
} }
opts, err := helm.ClientOptionsFromSecret(authDir, *secret) opts, err := getter.ClientOptionsFromSecret(authDir, *secret)
if err != nil { if err != nil {
err = fmt.Errorf("failed to create client options for HelmRepository '%s': %w", repository.Name, err) err = fmt.Errorf("failed to create client options for HelmRepository '%s': %w", repo.Name, err)
return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
clientOpts = append(clientOpts, opts...) clientOpts = append(clientOpts, opts...)
} }
// Initialize the chart repository // Initialize the chart repository
chartRepo, err := helm.NewChartRepository(repository.Spec.URL, r.Storage.LocalPath(*repository.GetArtifact()), r.Getters, clientOpts) chartRepo, err := repository.NewChartRepository(repo.Spec.URL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, clientOpts)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *url.Error: case *url.Error:
return sourcev1.HelmChartNotReady(chart, sourcev1.URLInvalidReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.URLInvalidReason, err.Error()), err
default: default:
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.ChartPullFailedReason, err.Error()), err
} }
} }
var cachedChart string var cachedChart string
if artifact := chart.GetArtifact(); artifact != nil { if artifact := c.GetArtifact(); artifact != nil {
cachedChart = artifact.Path cachedChart = artifact.Path
} }
// Build the chart // Build the chart
cBuilder := helm.NewRemoteChartBuilder(chartRepo) cBuilder := chart.NewRemoteBuilder(chartRepo)
ref := helm.RemoteChartReference{Name: chart.Spec.Chart, Version: chart.Spec.Version} ref := chart.RemoteReference{Name: c.Spec.Chart, Version: c.Spec.Version}
opts := helm.BuildOptions{ opts := chart.BuildOptions{
ValueFiles: chart.GetValuesFiles(), ValueFiles: c.GetValuesFiles(),
CachedChart: cachedChart, CachedChart: cachedChart,
Force: force, Force: force,
} }
build, err := cBuilder.Build(ctx, ref, filepath.Join(workDir, "chart.tgz"), opts) build, err := cBuilder.Build(ctx, ref, filepath.Join(workDir, "chart.tgz"), opts)
if err != nil { if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.ChartPullFailedReason, err.Error()), err
} }
newArtifact := r.Storage.NewArtifactFor(chart.Kind, chart.GetObjectMeta(), build.Version, newArtifact := r.Storage.NewArtifactFor(c.Kind, c.GetObjectMeta(), build.Version,
fmt.Sprintf("%s-%s.tgz", build.Name, build.Version)) fmt.Sprintf("%s-%s.tgz", build.Name, build.Version))
// If the path of the returned build equals the cache path, // If the path of the returned build equals the cache path,
// there are no changes to the chart // there are no changes to the chart
if build.Path == cachedChart { if build.Path == cachedChart {
// Ensure hostname is updated // Ensure hostname is updated
if chart.GetArtifact().URL != newArtifact.URL { if c.GetArtifact().URL != newArtifact.URL {
r.Storage.SetArtifactURL(chart.GetArtifact()) r.Storage.SetArtifactURL(c.GetArtifact())
chart.Status.URL = r.Storage.SetHostname(chart.Status.URL) c.Status.URL = r.Storage.SetHostname(c.Status.URL)
} }
return chart, nil return c, nil
} }
// Ensure artifact directory exists // Ensure artifact directory exists
err = r.Storage.MkdirAll(newArtifact) err = r.Storage.MkdirAll(newArtifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to create chart directory: %w", err) err = fmt.Errorf("unable to create chart directory: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Acquire a lock for the artifact // Acquire a lock for the artifact
unlock, err := r.Storage.Lock(newArtifact) unlock, err := r.Storage.Lock(newArtifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to acquire lock: %w", err) err = fmt.Errorf("unable to acquire lock: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
defer unlock() defer unlock()
// Copy the packaged chart to the artifact path // Copy the packaged chart to the artifact path
if err = r.Storage.CopyFromPath(&newArtifact, build.Path); err != nil { if err = r.Storage.CopyFromPath(&newArtifact, build.Path); err != nil {
err = fmt.Errorf("failed to write chart package to storage: %w", err) err = fmt.Errorf("failed to write chart package to storage: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Update symlink // Update symlink
cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", build.Name)) cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", build.Name))
if err != nil { if err != nil {
err = fmt.Errorf("storage error: %w", err) err = fmt.Errorf("storage error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
return sourcev1.HelmChartReady(chart, newArtifact, cUrl, sourcev1.ChartPullSucceededReason, build.Summary()), nil return sourcev1.HelmChartReady(c, newArtifact, cUrl, sourcev1.ChartPullSucceededReason, build.Summary()), nil
} }
func (r *HelmChartReconciler) fromTarballArtifact(ctx context.Context, source sourcev1.Artifact, func (r *HelmChartReconciler) fromTarballArtifact(ctx context.Context, source sourcev1.Artifact, c sourcev1.HelmChart,
chart sourcev1.HelmChart, workDir string, force bool) (sourcev1.HelmChart, error) { workDir string, force bool) (sourcev1.HelmChart, error) {
// Create temporary working directory to untar into // Create temporary working directory to untar into
sourceDir := filepath.Join(workDir, "source") sourceDir := filepath.Join(workDir, "source")
if err := os.Mkdir(sourceDir, 0700); err != nil { if err := os.Mkdir(sourceDir, 0700); err != nil {
err = fmt.Errorf("failed to create temporary directory to untar source into: %w", err) err = fmt.Errorf("failed to create temporary directory to untar source into: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Open the tarball artifact file and untar files into working directory // Open the tarball artifact file and untar files into working directory
f, err := os.Open(r.Storage.LocalPath(source)) f, err := os.Open(r.Storage.LocalPath(source))
if err != nil { if err != nil {
err = fmt.Errorf("artifact open error: %w", err) err = fmt.Errorf("artifact open error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
if _, err = untar.Untar(f, sourceDir); err != nil { if _, err = untar.Untar(f, sourceDir); err != nil {
_ = f.Close() _ = f.Close()
err = fmt.Errorf("artifact untar error: %w", err) err = fmt.Errorf("artifact untar error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
if err =f.Close(); err != nil { if err =f.Close(); err != nil {
err = fmt.Errorf("artifact close error: %w", err) err = fmt.Errorf("artifact close error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
chartPath, err := securejoin.SecureJoin(sourceDir, chart.Spec.Chart) chartPath, err := securejoin.SecureJoin(sourceDir, c.Spec.Chart)
if err != nil { if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Setup dependency manager // Setup dependency manager
authDir := filepath.Join(workDir, "creds") authDir := filepath.Join(workDir, "creds")
if err = os.Mkdir(authDir, 0700); err != nil { if err = os.Mkdir(authDir, 0700); err != nil {
err = fmt.Errorf("failed to create temporaRy directory for dependency credentials: %w", err) err = fmt.Errorf("failed to create temporaRy directory for dependency credentials: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
dm := helm.NewDependencyManager( dm := chart.NewDependencyManager(
helm.WithRepositoryCallback(r.getNamespacedChartRepositoryCallback(ctx, authDir, chart.GetNamespace())), chart.WithRepositoryCallback(r.getNamespacedChartRepositoryCallback(ctx, authDir, c.GetNamespace())),
) )
defer dm.Clear() defer dm.Clear()
// Get any cached chart // Get any cached chart
var cachedChart string var cachedChart string
if artifact := chart.Status.Artifact; artifact != nil { if artifact := c.Status.Artifact; artifact != nil {
cachedChart = artifact.Path cachedChart = artifact.Path
} }
buildsOpts := helm.BuildOptions{ buildsOpts := chart.BuildOptions{
ValueFiles: chart.GetValuesFiles(), ValueFiles: c.GetValuesFiles(),
CachedChart: cachedChart, CachedChart: cachedChart,
Force: force, Force: force,
} }
// Add revision metadata to chart build // Add revision metadata to chart build
if chart.Spec.ReconcileStrategy == sourcev1.ReconcileStrategyRevision { if c.Spec.ReconcileStrategy == sourcev1.ReconcileStrategyRevision {
// Isolate the commit SHA from GitRepository type artifacts by removing the branch/ prefix. // Isolate the commit SHA from GitRepository type artifacts by removing the branch/ prefix.
splitRev := strings.Split(source.Revision, "/") splitRev := strings.Split(source.Revision, "/")
buildsOpts.VersionMetadata = splitRev[len(splitRev)-1] buildsOpts.VersionMetadata = splitRev[len(splitRev)-1]
} }
// Build chart // Build chart
chartB := helm.NewLocalChartBuilder(dm) chartB := chart.NewLocalBuilder(dm)
build, err := chartB.Build(ctx, helm.LocalChartReference{BaseDir: sourceDir, Path: chartPath}, filepath.Join(workDir, "chart.tgz"), buildsOpts) build, err := chartB.Build(ctx, chart.LocalReference{BaseDir: sourceDir, Path: chartPath}, filepath.Join(workDir, "chart.tgz"), buildsOpts)
if err != nil { if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.ChartPackageFailedReason, err.Error()), err
} }
newArtifact := r.Storage.NewArtifactFor(chart.Kind, chart.GetObjectMeta(), build.Version, newArtifact := r.Storage.NewArtifactFor(c.Kind, c.GetObjectMeta(), build.Version,
fmt.Sprintf("%s-%s.tgz", build.Name, build.Version)) fmt.Sprintf("%s-%s.tgz", build.Name, build.Version))
// If the path of the returned build equals the cache path, // If the path of the returned build equals the cache path,
// there are no changes to the chart // there are no changes to the chart
if build.Path == cachedChart { if build.Path == cachedChart {
// Ensure hostname is updated // Ensure hostname is updated
if chart.GetArtifact().URL != newArtifact.URL { if c.GetArtifact().URL != newArtifact.URL {
r.Storage.SetArtifactURL(chart.GetArtifact()) r.Storage.SetArtifactURL(c.GetArtifact())
chart.Status.URL = r.Storage.SetHostname(chart.Status.URL) c.Status.URL = r.Storage.SetHostname(c.Status.URL)
} }
return chart, nil return c, nil
} }
// Ensure artifact directory exists // Ensure artifact directory exists
err = r.Storage.MkdirAll(newArtifact) err = r.Storage.MkdirAll(newArtifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to create chart directory: %w", err) err = fmt.Errorf("unable to create chart directory: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Acquire a lock for the artifact // Acquire a lock for the artifact
unlock, err := r.Storage.Lock(newArtifact) unlock, err := r.Storage.Lock(newArtifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to acquire lock: %w", err) err = fmt.Errorf("unable to acquire lock: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
defer unlock() defer unlock()
// Copy the packaged chart to the artifact path // Copy the packaged chart to the artifact path
if err = r.Storage.CopyFromPath(&newArtifact, build.Path); err != nil { if err = r.Storage.CopyFromPath(&newArtifact, build.Path); err != nil {
err = fmt.Errorf("failed to write chart package to storage: %w", err) err = fmt.Errorf("failed to write chart package to storage: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Update symlink // Update symlink
cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", chart.Name)) cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", build.Name))
if err != nil { if err != nil {
err = fmt.Errorf("storage error: %w", err) err = fmt.Errorf("storage error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
return sourcev1.HelmChartReady(chart, newArtifact, cUrl, sourcev1.ChartPackageSucceededReason, build.Summary()), nil return sourcev1.HelmChartReady(c, newArtifact, cUrl, sourcev1.ChartPackageSucceededReason, build.Summary()), nil
} }
// TODO(hidde): factor out to helper? // TODO(hidde): factor out to helper?
func (r *HelmChartReconciler) getNamespacedChartRepositoryCallback(ctx context.Context, dir, namespace string) helm.GetChartRepositoryCallback { func (r *HelmChartReconciler) getNamespacedChartRepositoryCallback(ctx context.Context, dir, namespace string) chart.GetChartRepositoryCallback {
return func(url string) (*helm.ChartRepository, error) { return func(url string) (*repository.ChartRepository, error) {
repo, err := r.resolveDependencyRepository(ctx, url, namespace) repo, err := r.resolveDependencyRepository(ctx, url, namespace)
if err != nil { if err != nil {
if errors.ReasonForError(err) != metav1.StatusReasonUnknown { if errors.ReasonForError(err) != metav1.StatusReasonUnknown {
@ -528,21 +530,21 @@ func (r *HelmChartReconciler) getNamespacedChartRepositoryCallback(ctx context.C
}, },
} }
} }
clientOpts := []getter.Option{ clientOpts := []extgetter.Option{
getter.WithURL(repo.Spec.URL), extgetter.WithURL(repo.Spec.URL),
getter.WithTimeout(repo.Spec.Timeout.Duration), extgetter.WithTimeout(repo.Spec.Timeout.Duration),
getter.WithPassCredentialsAll(repo.Spec.PassCredentials), extgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
} }
if secret, err := r.getHelmRepositorySecret(ctx, repo); err != nil { if secret, err := r.getHelmRepositorySecret(ctx, repo); err != nil {
return nil, err return nil, err
} else if secret != nil { } else if secret != nil {
opts, err := helm.ClientOptionsFromSecret(dir, *secret) opts, err := getter.ClientOptionsFromSecret(dir, *secret)
if err != nil { if err != nil {
return nil, err return nil, err
} }
clientOpts = append(clientOpts, opts...) clientOpts = append(clientOpts, opts...)
} }
chartRepo, err := helm.NewChartRepository(repo.Spec.URL, "", r.Getters, clientOpts) chartRepo, err := repository.NewChartRepository(repo.Spec.URL, "", r.Getters, clientOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -663,7 +665,7 @@ func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string
if !ok { if !ok {
panic(fmt.Sprintf("Expected a HelmRepository, got %T", o)) panic(fmt.Sprintf("Expected a HelmRepository, got %T", o))
} }
u := helm.NormalizeChartRepositoryURL(repo.Spec.URL) u := repository.NormalizeURL(repo.Spec.URL)
if u != "" { if u != "" {
return []string{u} return []string{u}
} }

View File

@ -23,12 +23,8 @@ import (
"os" "os"
"time" "time"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/getter" extgetter "helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -42,8 +38,14 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/predicate"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/repository"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/internal/helm"
) )
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmrepositories,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmrepositories,verbs=get;list;watch;create;update;patch;delete
@ -56,7 +58,7 @@ type HelmRepositoryReconciler struct {
client.Client client.Client
Scheme *runtime.Scheme Scheme *runtime.Scheme
Storage *Storage Storage *Storage
Getters getter.Providers Getters extgetter.Providers
EventRecorder kuberecorder.EventRecorder EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *events.Recorder ExternalEventRecorder *events.Recorder
MetricsRecorder *metrics.Recorder MetricsRecorder *metrics.Recorder
@ -168,74 +170,74 @@ func (r *HelmRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reque
return ctrl.Result{RequeueAfter: repository.GetInterval().Duration}, nil return ctrl.Result{RequeueAfter: repository.GetInterval().Duration}, nil
} }
func (r *HelmRepositoryReconciler) reconcile(ctx context.Context, repository sourcev1.HelmRepository) (sourcev1.HelmRepository, error) { func (r *HelmRepositoryReconciler) reconcile(ctx context.Context, repo sourcev1.HelmRepository) (sourcev1.HelmRepository, error) {
clientOpts := []getter.Option{ clientOpts := []extgetter.Option{
getter.WithURL(repository.Spec.URL), extgetter.WithURL(repo.Spec.URL),
getter.WithTimeout(repository.Spec.Timeout.Duration), extgetter.WithTimeout(repo.Spec.Timeout.Duration),
getter.WithPassCredentialsAll(repository.Spec.PassCredentials), extgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
} }
if repository.Spec.SecretRef != nil { if repo.Spec.SecretRef != nil {
name := types.NamespacedName{ name := types.NamespacedName{
Namespace: repository.GetNamespace(), Namespace: repo.GetNamespace(),
Name: repository.Spec.SecretRef.Name, Name: repo.Spec.SecretRef.Name,
} }
var secret corev1.Secret var secret corev1.Secret
err := r.Client.Get(ctx, name, &secret) err := r.Client.Get(ctx, name, &secret)
if err != nil { if err != nil {
err = fmt.Errorf("auth secret error: %w", err) err = fmt.Errorf("auth secret error: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
authDir, err := os.MkdirTemp("", "helm-repository-") authDir, err := os.MkdirTemp("", "helm-repository-")
if err != nil { if err != nil {
err = fmt.Errorf("failed to create temporary working directory for credentials: %w", err) err = fmt.Errorf("failed to create temporary working directory for credentials: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
defer os.RemoveAll(authDir) defer os.RemoveAll(authDir)
opts, err := helm.ClientOptionsFromSecret(authDir, secret) opts, err := getter.ClientOptionsFromSecret(authDir, secret)
if err != nil { if err != nil {
err = fmt.Errorf("auth options error: %w", err) err = fmt.Errorf("auth options error: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
clientOpts = append(clientOpts, opts...) clientOpts = append(clientOpts, opts...)
} }
chartRepo, err := helm.NewChartRepository(repository.Spec.URL, "", r.Getters, clientOpts) chartRepo, err := repository.NewChartRepository(repo.Spec.URL, "", r.Getters, clientOpts)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *url.Error: case *url.Error:
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.URLInvalidReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.URLInvalidReason, err.Error()), err
default: default:
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.IndexationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.IndexationFailedReason, err.Error()), err
} }
} }
revision, err := chartRepo.CacheIndex() revision, err := chartRepo.CacheIndex()
if err != nil { if err != nil {
err = fmt.Errorf("failed to download repository index: %w", err) err = fmt.Errorf("failed to download repository index: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.IndexationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.IndexationFailedReason, err.Error()), err
} }
defer chartRepo.RemoveCache() defer chartRepo.RemoveCache()
artifact := r.Storage.NewArtifactFor(repository.Kind, artifact := r.Storage.NewArtifactFor(repo.Kind,
repository.ObjectMeta.GetObjectMeta(), repo.ObjectMeta.GetObjectMeta(),
revision, revision,
fmt.Sprintf("index-%s.yaml", revision)) fmt.Sprintf("index-%s.yaml", revision))
// Return early on unchanged index // Return early on unchanged index
if apimeta.IsStatusConditionTrue(repository.Status.Conditions, meta.ReadyCondition) && if apimeta.IsStatusConditionTrue(repo.Status.Conditions, meta.ReadyCondition) &&
repository.GetArtifact().HasRevision(artifact.Revision) { repo.GetArtifact().HasRevision(artifact.Revision) {
if artifact.URL != repository.GetArtifact().URL { if artifact.URL != repo.GetArtifact().URL {
r.Storage.SetArtifactURL(repository.GetArtifact()) r.Storage.SetArtifactURL(repo.GetArtifact())
repository.Status.URL = r.Storage.SetHostname(repository.Status.URL) repo.Status.URL = r.Storage.SetHostname(repo.Status.URL)
} }
return repository, nil return repo, nil
} }
// Load the cached repository index to ensure it passes validation // Load the cached repository index to ensure it passes validation
if err := chartRepo.LoadFromCache(); err != nil { if err := chartRepo.LoadFromCache(); err != nil {
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.IndexationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.IndexationFailedReason, err.Error()), err
} }
defer chartRepo.Unload() defer chartRepo.Unload()
@ -243,14 +245,14 @@ func (r *HelmRepositoryReconciler) reconcile(ctx context.Context, repository sou
err = r.Storage.MkdirAll(artifact) err = r.Storage.MkdirAll(artifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to create repository index directory: %w", err) err = fmt.Errorf("unable to create repository index directory: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// Acquire lock // Acquire lock
unlock, err := r.Storage.Lock(artifact) unlock, err := r.Storage.Lock(artifact)
if err != nil { if err != nil {
err = fmt.Errorf("unable to acquire lock: %w", err) err = fmt.Errorf("unable to acquire lock: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
defer unlock() defer unlock()
@ -258,10 +260,10 @@ func (r *HelmRepositoryReconciler) reconcile(ctx context.Context, repository sou
storageTarget := r.Storage.LocalPath(artifact) storageTarget := r.Storage.LocalPath(artifact)
if storageTarget == "" { if storageTarget == "" {
err := fmt.Errorf("failed to calcalute local storage path to store artifact to") err := fmt.Errorf("failed to calcalute local storage path to store artifact to")
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
if err = chartRepo.Index.WriteFile(storageTarget, 0644); err != nil { if err = chartRepo.Index.WriteFile(storageTarget, 0644); err != nil {
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
// TODO(hidde): it would be better to make the Storage deal with this // TODO(hidde): it would be better to make the Storage deal with this
artifact.Checksum = chartRepo.Checksum artifact.Checksum = chartRepo.Checksum
@ -271,11 +273,11 @@ func (r *HelmRepositoryReconciler) reconcile(ctx context.Context, repository sou
indexURL, err := r.Storage.Symlink(artifact, "index.yaml") indexURL, err := r.Storage.Symlink(artifact, "index.yaml")
if err != nil { if err != nil {
err = fmt.Errorf("storage error: %w", err) err = fmt.Errorf("storage error: %w", err)
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err return sourcev1.HelmRepositoryNotReady(repo, sourcev1.StorageOperationFailedReason, err.Error()), err
} }
message := fmt.Sprintf("Fetched revision: %s", artifact.Revision) message := fmt.Sprintf("Fetched revision: %s", artifact.Revision)
return sourcev1.HelmRepositoryReady(repository, artifact, indexURL, sourcev1.IndexationSucceededReason, message), nil return sourcev1.HelmRepositoryReady(repo, artifact, indexURL, sourcev1.IndexationSucceededReason, message), nil
} }
func (r *HelmRepositoryReconciler) reconcileDelete(ctx context.Context, repository sourcev1.HelmRepository) (ctrl.Result, error) { func (r *HelmRepositoryReconciler) reconcileDelete(ctx context.Context, repository sourcev1.HelmRepository) (ctrl.Result, error) {

View File

@ -14,49 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/fluxcd/source-controller/internal/fs"
helmchart "helm.sh/helm/v3/pkg/chart" helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"github.com/fluxcd/source-controller/internal/fs"
) )
// ChartReference holds information to locate a chart. // Reference holds information to locate a chart.
type ChartReference interface { type Reference interface {
// Validate returns an error if the ChartReference is not valid according // Validate returns an error if the Reference is not valid according
// to the spec of the interface implementation. // to the spec of the interface implementation.
Validate() error Validate() error
} }
// LocalChartReference contains sufficient information to locate a chart on the // LocalReference contains sufficient information to locate a chart on the
// local filesystem. // local filesystem.
type LocalChartReference struct { type LocalReference struct {
// BaseDir used as chroot during build operations. // WorkDir used as chroot during build operations.
// File references are not allowed to traverse outside it. // File references are not allowed to traverse outside it.
BaseDir string WorkDir string
// Path of the chart on the local filesystem. // Path of the chart on the local filesystem.
Path string Path string
} }
// Validate returns an error if the LocalChartReference does not have // Validate returns an error if the LocalReference does not have
// a Path set. // a Path set.
func (r LocalChartReference) Validate() error { func (r LocalReference) Validate() error {
if r.Path == "" { if r.Path == "" {
return fmt.Errorf("no path set for local chart reference") return fmt.Errorf("no path set for local chart reference")
} }
return nil return nil
} }
// RemoteChartReference contains sufficient information to look up a chart in // RemoteReference contains sufficient information to look up a chart in
// a ChartRepository. // a ChartRepository.
type RemoteChartReference struct { type RemoteReference struct {
// Name of the chart. // Name of the chart.
Name string Name string
// Version of the chart. // Version of the chart.
@ -64,25 +66,29 @@ type RemoteChartReference struct {
Version string Version string
} }
// Validate returns an error if the RemoteChartReference does not have // Validate returns an error if the RemoteReference does not have
// a Name set. // a Name set.
func (r RemoteChartReference) Validate() error { func (r RemoteReference) Validate() error {
if r.Name == "" { if r.Name == "" {
return fmt.Errorf("no name set for remote chart reference") return fmt.Errorf("no name set for remote chart reference")
} }
name := regexp.MustCompile("^([-a-z0-9]*)$")
if !name.MatchString(r.Name) {
return fmt.Errorf("invalid chart name '%s': a valid name must be lower case letters and numbers and MAY be separated with dashes (-)", r.Name)
}
return nil return nil
} }
// ChartBuilder is capable of building a (specific) ChartReference. // Builder is capable of building a (specific) chart Reference.
type ChartBuilder interface { type Builder interface {
// Build builds and packages a Helm chart with the given ChartReference // Build builds and packages a Helm chart with the given Reference
// and BuildOptions and writes it to p. It returns the ChartBuild result, // and BuildOptions and writes it to p. It returns the Build result,
// or an error. It may return an error for unsupported ChartReference // or an error. It may return an error for unsupported Reference
// implementations. // implementations.
Build(ctx context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error)
} }
// BuildOptions provides a list of options for ChartBuilder.Build. // BuildOptions provides a list of options for Builder.Build.
type BuildOptions struct { type BuildOptions struct {
// VersionMetadata can be set to SemVer build metadata as defined in // VersionMetadata can be set to SemVer build metadata as defined in
// the spec, and is included during packaging. // the spec, and is included during packaging.
@ -109,9 +115,9 @@ func (o BuildOptions) GetValueFiles() []string {
return o.ValueFiles return o.ValueFiles
} }
// ChartBuild contains the ChartBuilder.Build result, including specific // Build contains the Builder.Build result, including specific
// information about the built chart like ResolvedDependencies. // information about the built chart like ResolvedDependencies.
type ChartBuild struct { type Build struct {
// Path is the absolute path to the packaged chart. // Path is the absolute path to the packaged chart.
Path string Path string
// Name of the packaged chart. // Name of the packaged chart.
@ -124,14 +130,14 @@ type ChartBuild struct {
// ResolvedDependencies is the number of local and remote dependencies // ResolvedDependencies is the number of local and remote dependencies
// collected by the DependencyManager before building the chart. // collected by the DependencyManager before building the chart.
ResolvedDependencies int ResolvedDependencies int
// Packaged indicates if the ChartBuilder has packaged the chart. // Packaged indicates if the Builder has packaged the chart.
// This can for example be false if ValueFiles is empty and the chart // This can for example be false if ValueFiles is empty and the chart
// source was already packaged. // source was already packaged.
Packaged bool Packaged bool
} }
// Summary returns a human-readable summary of the ChartBuild. // Summary returns a human-readable summary of the Build.
func (b *ChartBuild) Summary() string { func (b *Build) Summary() string {
if b == nil { if b == nil {
return "no chart build" return "no chart build"
} }
@ -155,15 +161,15 @@ func (b *ChartBuild) Summary() string {
return s.String() return s.String()
} }
// String returns the Path of the ChartBuild. // String returns the Path of the Build.
func (b *ChartBuild) String() string { func (b *Build) String() string {
if b != nil { if b != nil {
return b.Path return b.Path
} }
return "" return ""
} }
// packageToPath attempts to package the given chart.Chart to the out filepath. // packageToPath attempts to package the given chart to the out filepath.
func packageToPath(chart *helmchart.Chart, out string) error { func packageToPath(chart *helmchart.Chart, out string) error {
o, err := os.MkdirTemp("", "chart-build-*") o, err := os.MkdirTemp("", "chart-build-*")
if err != nil { if err != nil {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"context" "context"
@ -24,27 +24,28 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
securejoin "github.com/cyphar/filepath-securejoin" securejoin "github.com/cyphar/filepath-securejoin"
"github.com/fluxcd/pkg/runtime/transform"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/runtime/transform"
) )
type localChartBuilder struct { type localChartBuilder struct {
dm *DependencyManager dm *DependencyManager
} }
// NewLocalChartBuilder returns a ChartBuilder capable of building a Helm // NewLocalBuilder returns a Builder capable of building a Helm
// chart with a LocalChartReference. For chart references pointing to a // chart with a LocalReference. For chart references pointing to a
// directory, the DependencyManager is used to resolve missing local and // directory, the DependencyManager is used to resolve missing local and
// remote dependencies. // remote dependencies.
func NewLocalChartBuilder(dm *DependencyManager) ChartBuilder { func NewLocalBuilder(dm *DependencyManager) Builder {
return &localChartBuilder{ return &localChartBuilder{
dm: dm, dm: dm,
} }
} }
func (b *localChartBuilder) Build(ctx context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error) { func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) {
localRef, ok := ref.(LocalChartReference) localRef, ok := ref.(LocalReference)
if !ok { if !ok {
return nil, fmt.Errorf("expected local chart reference") return nil, fmt.Errorf("expected local chart reference")
} }
@ -53,14 +54,14 @@ func (b *localChartBuilder) Build(ctx context.Context, ref ChartReference, p str
return nil, err return nil, err
} }
// Load the chart metadata from the LocalChartReference to ensure it points // Load the chart metadata from the LocalReference to ensure it points
// to a chart // to a chart
curMeta, err := LoadChartMetadata(localRef.Path) curMeta, err := LoadChartMetadata(localRef.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := &ChartBuild{} result := &Build{}
result.Name = curMeta.Name result.Name = curMeta.Name
// Set build specific metadata if instructed // Set build specific metadata if instructed
@ -101,7 +102,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref ChartReference, p str
// Merge chart values, if instructed // Merge chart values, if instructed
var mergedValues map[string]interface{} var mergedValues map[string]interface{}
if len(opts.GetValueFiles()) > 0 { if len(opts.GetValueFiles()) > 0 {
if mergedValues, err = mergeFileValues(localRef.BaseDir, opts.ValueFiles); err != nil { if mergedValues, err = mergeFileValues(localRef.WorkDir, opts.ValueFiles); err != nil {
return nil, fmt.Errorf("failed to merge value files: %w", err) return nil, fmt.Errorf("failed to merge value files: %w", err)
} }
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"os" "os"
@ -99,16 +99,16 @@ func Test_copyFileToPath(t *testing.T) {
}{ }{
{ {
name: "copies input file", name: "copies input file",
in: "testdata/local-index.yaml", in: "../testdata/local-index.yaml",
}, },
{ {
name: "invalid input file", name: "invalid input file",
in: "testdata/invalid.tgz", in: "../testdata/invalid.tgz",
wantErr: "failed to open file to copy from", wantErr: "failed to open file to copy from",
}, },
{ {
name: "invalid input directory", name: "invalid input directory",
in: "testdata/charts", in: "../testdata/charts",
wantErr: "failed to read from source during copy", wantErr: "failed to read from source during copy",
}, },
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"context" "context"
@ -24,28 +24,31 @@ import (
"path/filepath" "path/filepath"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/fluxcd/pkg/runtime/transform"
"github.com/fluxcd/source-controller/internal/fs"
helmchart "helm.sh/helm/v3/pkg/chart" helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/runtime/transform"
"github.com/fluxcd/source-controller/internal/fs"
"github.com/fluxcd/source-controller/internal/helm/repository"
) )
type remoteChartBuilder struct { type remoteChartBuilder struct {
remote *ChartRepository remote *repository.ChartRepository
} }
// NewRemoteChartBuilder returns a ChartBuilder capable of building a Helm // NewRemoteBuilder returns a Builder capable of building a Helm
// chart with a RemoteChartReference from the given ChartRepository. // chart with a RemoteReference from the given Index.
func NewRemoteChartBuilder(repository *ChartRepository) ChartBuilder { func NewRemoteBuilder(repository *repository.ChartRepository) Builder {
return &remoteChartBuilder{ return &remoteChartBuilder{
remote: repository, remote: repository,
} }
} }
func (b *remoteChartBuilder) Build(_ context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error) { func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) {
remoteRef, ok := ref.(RemoteChartReference) remoteRef, ok := ref.(RemoteReference)
if !ok { if !ok {
return nil, fmt.Errorf("expected remote chart reference") return nil, fmt.Errorf("expected remote chart reference")
} }
@ -59,13 +62,13 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref ChartReference, p stri
} }
defer b.remote.Unload() defer b.remote.Unload()
// Get the current version for the RemoteChartReference // Get the current version for the RemoteReference
cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version) cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get chart version for remote reference: %w", err) return nil, fmt.Errorf("failed to get chart version for remote reference: %w", err)
} }
result := &ChartBuild{} result := &Build{}
result.Name = cv.Name result.Name = cv.Name
result.Version = cv.Version result.Version = cv.Version
// Set build specific metadata if instructed // Set build specific metadata if instructed

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"testing" "testing"
@ -104,9 +104,9 @@ func Test_pathIsDir(t *testing.T) {
p string p string
want bool want bool
}{ }{
{name: "directory", p: "testdata/", want: true}, {name: "directory", p: "../testdata/", want: true},
{name: "file", p: "testdata/local-index.yaml", want: false}, {name: "file", p: "../testdata/local-index.yaml", want: false},
{name: "not found error", p: "testdata/does-not-exist.yaml", want: false}, {name: "not found error", p: "../testdata/does-not-exist.yaml", want: false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"encoding/hex" "encoding/hex"
@ -30,18 +30,18 @@ import (
func TestChartBuildResult_String(t *testing.T) { func TestChartBuildResult_String(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
var result *ChartBuild var result *Build
g.Expect(result.String()).To(Equal("")) g.Expect(result.String()).To(Equal(""))
result = &ChartBuild{} result = &Build{}
g.Expect(result.String()).To(Equal("")) g.Expect(result.String()).To(Equal(""))
result = &ChartBuild{Path: "/foo/"} result = &Build{Path: "/foo/"}
g.Expect(result.String()).To(Equal("/foo/")) g.Expect(result.String()).To(Equal("/foo/"))
} }
func Test_packageToPath(t *testing.T) { func Test_packageToPath(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
chart, err := loader.Load("testdata/charts/helmchart-0.1.0.tgz") chart, err := loader.Load("../testdata/charts/helmchart-0.1.0.tgz")
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
g.Expect(chart).ToNot(BeNil()) g.Expect(chart).ToNot(BeNil())

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"context" "context"
@ -31,18 +31,20 @@ import (
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
helmchart "helm.sh/helm/v3/pkg/chart" helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"github.com/fluxcd/source-controller/internal/helm/repository"
) )
// GetChartRepositoryCallback must return a ChartRepository for the URL, // GetChartRepositoryCallback must return a repository.ChartRepository for the
// or an error describing why it could not be returned. // URL, or an error describing why it could not be returned.
type GetChartRepositoryCallback func(url string) (*ChartRepository, error) type GetChartRepositoryCallback func(url string) (*repository.ChartRepository, error)
// DependencyManager manages dependencies for a Helm chart. // DependencyManager manages dependencies for a Helm chart.
type DependencyManager struct { type DependencyManager struct {
// repositories contains a map of ChartRepository indexed by their // repositories contains a map of Index indexed by their
// normalized URL. It is used as a lookup table for missing // normalized URL. It is used as a lookup table for missing
// dependencies. // dependencies.
repositories map[string]*ChartRepository repositories map[string]*repository.ChartRepository
// getRepositoryCallback can be set to an on-demand GetChartRepositoryCallback // getRepositoryCallback can be set to an on-demand GetChartRepositoryCallback
// which returned result is cached to repositories. // which returned result is cached to repositories.
@ -56,11 +58,12 @@ type DependencyManager struct {
mu sync.Mutex mu sync.Mutex
} }
// DependencyManagerOption configures an option on a DependencyManager.
type DependencyManagerOption interface { type DependencyManagerOption interface {
applyToDependencyManager(dm *DependencyManager) applyToDependencyManager(dm *DependencyManager)
} }
type WithRepositories map[string]*ChartRepository type WithRepositories map[string]*repository.ChartRepository
func (o WithRepositories) applyToDependencyManager(dm *DependencyManager) { func (o WithRepositories) applyToDependencyManager(dm *DependencyManager) {
dm.repositories = o dm.repositories = o
@ -98,9 +101,9 @@ func (dm *DependencyManager) Clear() []error {
} }
// Build compiles a set of missing dependencies from chart.Chart, and attempts to // Build compiles a set of missing dependencies from chart.Chart, and attempts to
// resolve and build them using the information from ChartReference. // resolve and build them using the information from Reference.
// It returns the number of resolved local and remote dependencies, or an error. // It returns the number of resolved local and remote dependencies, or an error.
func (dm *DependencyManager) Build(ctx context.Context, ref ChartReference, chart *helmchart.Chart) (int, error) { func (dm *DependencyManager) Build(ctx context.Context, ref Reference, chart *helmchart.Chart) (int, error) {
// Collect dependency metadata // Collect dependency metadata
var ( var (
deps = chart.Dependencies() deps = chart.Dependencies()
@ -132,9 +135,9 @@ type chartWithLock struct {
// build adds the given list of deps to the chart with the configured number of // build adds the given list of deps to the chart with the configured number of
// concurrent workers. If the chart.Chart references a local dependency but no // concurrent workers. If the chart.Chart references a local dependency but no
// LocalChartReference is given, or any dependency could not be added, an error // LocalReference is given, or any dependency could not be added, an error
// is returned. The first error it encounters cancels all other workers. // is returned. The first error it encounters cancels all other workers.
func (dm *DependencyManager) build(ctx context.Context, ref ChartReference, chart *helmchart.Chart, deps map[string]*helmchart.Dependency) error { func (dm *DependencyManager) build(ctx context.Context, ref Reference, c *helmchart.Chart, deps map[string]*helmchart.Dependency) error {
current := dm.concurrent current := dm.concurrent
if current <= 0 { if current <= 0 {
current = 1 current = 1
@ -143,7 +146,7 @@ func (dm *DependencyManager) build(ctx context.Context, ref ChartReference, char
group, groupCtx := errgroup.WithContext(ctx) group, groupCtx := errgroup.WithContext(ctx)
group.Go(func() error { group.Go(func() error {
sem := semaphore.NewWeighted(current) sem := semaphore.NewWeighted(current)
chart := &chartWithLock{Chart: chart} c := &chartWithLock{Chart: c}
for name, dep := range deps { for name, dep := range deps {
name, dep := name, dep name, dep := name, dep
if err := sem.Acquire(groupCtx, 1); err != nil { if err := sem.Acquire(groupCtx, 1); err != nil {
@ -152,17 +155,17 @@ func (dm *DependencyManager) build(ctx context.Context, ref ChartReference, char
group.Go(func() (err error) { group.Go(func() (err error) {
defer sem.Release(1) defer sem.Release(1)
if isLocalDep(dep) { if isLocalDep(dep) {
localRef, ok := ref.(LocalChartReference) localRef, ok := ref.(LocalReference)
if !ok { if !ok {
err = fmt.Errorf("failed to add local dependency '%s': no local chart reference", name) err = fmt.Errorf("failed to add local dependency '%s': no local chart reference", name)
return return
} }
if err = dm.addLocalDependency(localRef, chart, dep); err != nil { if err = dm.addLocalDependency(localRef, c, dep); err != nil {
err = fmt.Errorf("failed to add local dependency '%s': %w", name, err) err = fmt.Errorf("failed to add local dependency '%s': %w", name, err)
} }
return return
} }
if err = dm.addRemoteDependency(chart, dep); err != nil { if err = dm.addRemoteDependency(c, dep); err != nil {
err = fmt.Errorf("failed to add remote dependency '%s': %w", name, err) err = fmt.Errorf("failed to add remote dependency '%s': %w", name, err)
} }
return return
@ -175,7 +178,7 @@ func (dm *DependencyManager) build(ctx context.Context, ref ChartReference, char
// addLocalDependency attempts to resolve and add the given local chart.Dependency // addLocalDependency attempts to resolve and add the given local chart.Dependency
// to the chart. // to the chart.
func (dm *DependencyManager) addLocalDependency(ref LocalChartReference, chart *chartWithLock, dep *helmchart.Dependency) error { func (dm *DependencyManager) addLocalDependency(ref LocalReference, c *chartWithLock, dep *helmchart.Dependency) error {
sLocalChartPath, err := dm.secureLocalChartPath(ref, dep) sLocalChartPath, err := dm.secureLocalChartPath(ref, dep)
if err != nil { if err != nil {
return err return err
@ -197,7 +200,7 @@ func (dm *DependencyManager) addLocalDependency(ref LocalChartReference, chart *
ch, err := loader.Load(sLocalChartPath) ch, err := loader.Load(sLocalChartPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load chart from '%s' (reference '%s'): %w", return fmt.Errorf("failed to load chart from '%s' (reference '%s'): %w",
strings.TrimPrefix(sLocalChartPath, ref.BaseDir), dep.Repository, err) strings.TrimPrefix(sLocalChartPath, ref.WorkDir), dep.Repository, err)
} }
ver, err := semver.NewVersion(ch.Metadata.Version) ver, err := semver.NewVersion(ch.Metadata.Version)
@ -210,9 +213,9 @@ func (dm *DependencyManager) addLocalDependency(ref LocalChartReference, chart *
return err return err
} }
chart.mu.Lock() c.mu.Lock()
chart.AddDependency(ch) c.AddDependency(ch)
chart.mu.Unlock() c.mu.Unlock()
return nil return nil
} }
@ -249,19 +252,19 @@ func (dm *DependencyManager) addRemoteDependency(chart *chartWithLock, dep *helm
} }
// resolveRepository first attempts to resolve the url from the repositories, falling back // resolveRepository first attempts to resolve the url from the repositories, falling back
// to getRepositoryCallback if set. It returns the resolved ChartRepository, or an error. // to getRepositoryCallback if set. It returns the resolved Index, or an error.
func (dm *DependencyManager) resolveRepository(url string) (_ *ChartRepository, err error) { func (dm *DependencyManager) resolveRepository(url string) (_ *repository.ChartRepository, err error) {
dm.mu.Lock() dm.mu.Lock()
defer dm.mu.Unlock() defer dm.mu.Unlock()
nUrl := NormalizeChartRepositoryURL(url) nUrl := repository.NormalizeURL(url)
if _, ok := dm.repositories[nUrl]; !ok { if _, ok := dm.repositories[nUrl]; !ok {
if dm.getRepositoryCallback == nil { if dm.getRepositoryCallback == nil {
err = fmt.Errorf("no chart repository for URL '%s'", nUrl) err = fmt.Errorf("no chart repository for URL '%s'", nUrl)
return return
} }
if dm.repositories == nil { if dm.repositories == nil {
dm.repositories = map[string]*ChartRepository{} dm.repositories = map[string]*repository.ChartRepository{}
} }
if dm.repositories[nUrl], err = dm.getRepositoryCallback(nUrl); err != nil { if dm.repositories[nUrl], err = dm.getRepositoryCallback(nUrl); err != nil {
err = fmt.Errorf("failed to get chart repository for URL '%s': %w", nUrl, err) err = fmt.Errorf("failed to get chart repository for URL '%s': %w", nUrl, err)
@ -273,8 +276,8 @@ func (dm *DependencyManager) resolveRepository(url string) (_ *ChartRepository,
// secureLocalChartPath returns the secure absolute path of a local dependency. // secureLocalChartPath returns the secure absolute path of a local dependency.
// It does not allow the dependency's path to be outside the scope of // It does not allow the dependency's path to be outside the scope of
// LocalChartReference.BaseDir. // LocalReference.WorkDir.
func (dm *DependencyManager) secureLocalChartPath(ref LocalChartReference, dep *helmchart.Dependency) (string, error) { func (dm *DependencyManager) secureLocalChartPath(ref LocalReference, dep *helmchart.Dependency) (string, error) {
localUrl, err := url.Parse(dep.Repository) localUrl, err := url.Parse(dep.Repository)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse alleged local chart reference: %w", err) return "", fmt.Errorf("failed to parse alleged local chart reference: %w", err)
@ -282,11 +285,11 @@ func (dm *DependencyManager) secureLocalChartPath(ref LocalChartReference, dep *
if localUrl.Scheme != "" && localUrl.Scheme != "file" { if localUrl.Scheme != "" && localUrl.Scheme != "file" {
return "", fmt.Errorf("'%s' is not a local chart reference", dep.Repository) return "", fmt.Errorf("'%s' is not a local chart reference", dep.Repository)
} }
relPath, err := filepath.Rel(ref.BaseDir, ref.Path) relPath, err := filepath.Rel(ref.WorkDir, ref.Path)
if err != nil { if err != nil {
return "", err relPath = ref.Path
} }
return securejoin.SecureJoin(ref.BaseDir, filepath.Join(relPath, localUrl.Host, localUrl.Path)) return securejoin.SecureJoin(ref.WorkDir, filepath.Join(relPath, localUrl.Host, localUrl.Path))
} }
// collectMissing returns a map with reqs that are missing from current, // collectMissing returns a map with reqs that are missing from current,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"context" "context"
@ -29,26 +29,9 @@ import (
helmchart "helm.sh/helm/v3/pkg/chart" helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/repo"
)
var ( "github.com/fluxcd/source-controller/internal/helm/getter"
// helmPackageFile contains the path to a Helm package in the v2 format "github.com/fluxcd/source-controller/internal/helm/repository"
// without any dependencies
helmPackageFile = "testdata/charts/helmchart-0.1.0.tgz"
chartName = "helmchart"
chartVersion = "0.1.0"
chartLocalRepository = "file://../helmchart"
remoteDepFixture = helmchart.Dependency{
Name: chartName,
Version: chartVersion,
Repository: "https://example.com/charts",
}
// helmPackageV1File contains the path to a Helm package in the v1 format,
// including dependencies in a requirements.yaml file which should be
// loaded
helmPackageV1File = "testdata/charts/helmchartwithdeps-v1-0.3.0.tgz"
chartNameV1 = "helmchartwithdeps-v1"
chartVersionV1 = "0.3.0"
) )
func TestDependencyManager_Build(t *testing.T) { func TestDependencyManager_Build(t *testing.T) {
@ -56,7 +39,7 @@ func TestDependencyManager_Build(t *testing.T) {
name string name string
baseDir string baseDir string
path string path string
repositories map[string]*ChartRepository repositories map[string]*repository.ChartRepository
getChartRepositoryCallback GetChartRepositoryCallback getChartRepositoryCallback GetChartRepositoryCallback
want int want int
wantChartFunc func(g *WithT, c *helmchart.Chart) wantChartFunc func(g *WithT, c *helmchart.Chart)
@ -70,13 +53,13 @@ func TestDependencyManager_Build(t *testing.T) {
//}, //},
{ {
name: "build failure returns error", name: "build failure returns error",
baseDir: "testdata/charts", baseDir: "./../testdata/charts",
path: "helmchartwithdeps", path: "helmchartwithdeps",
wantErr: "failed to add remote dependency 'grafana': no chart repository for URL", wantErr: "failed to add remote dependency 'grafana': no chart repository for URL",
}, },
{ {
name: "no dependencies returns zero", name: "no dependencies returns zero",
baseDir: "testdata/charts", baseDir: "./../testdata/charts",
path: "helmchart", path: "helmchart",
want: 0, want: 0,
}, },
@ -91,7 +74,7 @@ func TestDependencyManager_Build(t *testing.T) {
got, err := NewDependencyManager( got, err := NewDependencyManager(
WithRepositories(tt.repositories), WithRepositories(tt.repositories),
WithRepositoryCallback(tt.getChartRepositoryCallback), WithRepositoryCallback(tt.getChartRepositoryCallback),
).Build(context.TODO(), LocalChartReference{BaseDir: tt.baseDir, Path: tt.path}, chart) ).Build(context.TODO(), LocalReference{WorkDir: tt.baseDir, Path: tt.path}, chart)
if tt.wantErr != "" { if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
@ -135,7 +118,7 @@ func TestDependencyManager_build(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
dm := NewDependencyManager() dm := NewDependencyManager()
err := dm.build(context.TODO(), LocalChartReference{}, &helmchart.Chart{}, tt.deps) err := dm.build(context.TODO(), LocalReference{}, &helmchart.Chart{}, tt.deps)
if tt.wantErr != "" { if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
return return
@ -180,7 +163,7 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
Version: chartVersion, Version: chartVersion,
Repository: "file://../../../absolutely/invalid", Repository: "file://../../../absolutely/invalid",
}, },
wantErr: "no chart found at 'testdata/charts/absolutely/invalid'", wantErr: "no chart found at '../testdata/charts/absolutely/invalid'",
}, },
{ {
name: "invalid chart archive", name: "invalid chart archive",
@ -207,7 +190,7 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
dm := NewDependencyManager() dm := NewDependencyManager()
chart := &helmchart.Chart{} chart := &helmchart.Chart{}
err := dm.addLocalDependency(LocalChartReference{BaseDir: "testdata/charts", Path: "helmchartwithdeps"}, err := dm.addLocalDependency(LocalReference{WorkDir: "../testdata/charts", Path: "helmchartwithdeps"},
&chartWithLock{Chart: chart}, tt.dep) &chartWithLock{Chart: chart}, tt.dep)
if tt.wantErr != "" { if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
@ -222,23 +205,23 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
func TestDependencyManager_addRemoteDependency(t *testing.T) { func TestDependencyManager_addRemoteDependency(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
chartB, err := os.ReadFile("testdata/charts/helmchart-0.1.0.tgz") chartB, err := os.ReadFile("../testdata/charts/helmchart-0.1.0.tgz")
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
g.Expect(chartB).ToNot(BeEmpty()) g.Expect(chartB).ToNot(BeEmpty())
tests := []struct { tests := []struct {
name string name string
repositories map[string]*ChartRepository repositories map[string]*repository.ChartRepository
dep *helmchart.Dependency dep *helmchart.Dependency
wantFunc func(g *WithT, c *helmchart.Chart) wantFunc func(g *WithT, c *helmchart.Chart)
wantErr string wantErr string
}{ }{
{ {
name: "adds remote dependency", name: "adds remote dependency",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
Client: &mockGetter{ Client: &getter.MockGetter{
response: chartB, Response: chartB,
}, },
Index: &repo.IndexFile{ Index: &repo.IndexFile{
Entries: map[string]repo.ChartVersions{ Entries: map[string]repo.ChartVersions{
@ -266,7 +249,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "resolve repository error", name: "resolve repository error",
repositories: map[string]*ChartRepository{}, repositories: map[string]*repository.ChartRepository{},
dep: &helmchart.Dependency{ dep: &helmchart.Dependency{
Repository: "https://example.com", Repository: "https://example.com",
}, },
@ -274,7 +257,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "strategic load error", name: "strategic load error",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
CachePath: "/invalid/cache/path/foo", CachePath: "/invalid/cache/path/foo",
RWMutex: &sync.RWMutex{}, RWMutex: &sync.RWMutex{},
@ -287,7 +270,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "repository get error", name: "repository get error",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
Index: &repo.IndexFile{}, Index: &repo.IndexFile{},
RWMutex: &sync.RWMutex{}, RWMutex: &sync.RWMutex{},
@ -300,7 +283,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "repository version constraint error", name: "repository version constraint error",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
Index: &repo.IndexFile{ Index: &repo.IndexFile{
Entries: map[string]repo.ChartVersions{ Entries: map[string]repo.ChartVersions{
@ -326,7 +309,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "repository chart download error", name: "repository chart download error",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
Index: &repo.IndexFile{ Index: &repo.IndexFile{
Entries: map[string]repo.ChartVersions{ Entries: map[string]repo.ChartVersions{
@ -352,9 +335,9 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
}, },
{ {
name: "chart load error", name: "chart load error",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": { "https://example.com/": {
Client: &mockGetter{}, Client: &getter.MockGetter{},
Index: &repo.IndexFile{ Index: &repo.IndexFile{
Entries: map[string]repo.ChartVersions{ Entries: map[string]repo.ChartVersions{
chartName: { chartName: {
@ -404,40 +387,40 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
func TestDependencyManager_resolveRepository(t *testing.T) { func TestDependencyManager_resolveRepository(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
repositories map[string]*ChartRepository repositories map[string]*repository.ChartRepository
getChartRepositoryCallback GetChartRepositoryCallback getChartRepositoryCallback GetChartRepositoryCallback
url string url string
want *ChartRepository want *repository.ChartRepository
wantRepositories map[string]*ChartRepository wantRepositories map[string]*repository.ChartRepository
wantErr string wantErr string
}{ }{
{ {
name: "resolves from repositories index", name: "resolves from repositories index",
url: "https://example.com", url: "https://example.com",
repositories: map[string]*ChartRepository{ repositories: map[string]*repository.ChartRepository{
"https://example.com/": {URL: "https://example.com"}, "https://example.com/": {URL: "https://example.com"},
}, },
want: &ChartRepository{URL: "https://example.com"}, want: &repository.ChartRepository{URL: "https://example.com"},
}, },
{ {
name: "resolves from callback", name: "resolves from callback",
url: "https://example.com", url: "https://example.com",
getChartRepositoryCallback: func(url string) (*ChartRepository, error) { getChartRepositoryCallback: func(url string) (*repository.ChartRepository, error) {
return &ChartRepository{URL: "https://example.com"}, nil return &repository.ChartRepository{URL: "https://example.com"}, nil
}, },
want: &ChartRepository{URL: "https://example.com"}, want: &repository.ChartRepository{URL: "https://example.com"},
wantRepositories: map[string]*ChartRepository{ wantRepositories: map[string]*repository.ChartRepository{
"https://example.com/": {URL: "https://example.com"}, "https://example.com/": {URL: "https://example.com"},
}, },
}, },
{ {
name: "error from callback", name: "error from callback",
url: "https://example.com", url: "https://example.com",
getChartRepositoryCallback: func(url string) (*ChartRepository, error) { getChartRepositoryCallback: func(url string) (*repository.ChartRepository, error) {
return nil, errors.New("a very unique error") return nil, errors.New("a very unique error")
}, },
wantErr: "a very unique error", wantErr: "a very unique error",
wantRepositories: map[string]*ChartRepository{}, wantRepositories: map[string]*repository.ChartRepository{},
}, },
{ {
name: "error on not found", name: "error on not found",
@ -518,7 +501,7 @@ func TestDependencyManager_secureLocalChartPath(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
dm := NewDependencyManager() dm := NewDependencyManager()
got, err := dm.secureLocalChartPath(LocalChartReference{BaseDir: tt.baseDir, Path: tt.path}, tt.dep) got, err := dm.secureLocalChartPath(LocalReference{WorkDir: tt.baseDir, Path: tt.path}, tt.dep)
if tt.wantErr != "" { if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"archive/tar" "archive/tar"
@ -33,6 +33,8 @@ import (
helmchart "helm.sh/helm/v3/pkg/chart" helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/source-controller/internal/helm"
) )
// OverwriteChartDefaultValues overwrites the chart default values file with the given data. // OverwriteChartDefaultValues overwrites the chart default values file with the given data.
@ -115,8 +117,8 @@ func LoadChartMetadataFromDir(dir string) (*helmchart.Metadata, error) {
if stat.IsDir() { if stat.IsDir() {
return nil, fmt.Errorf("'%s' is a directory", stat.Name()) return nil, fmt.Errorf("'%s' is a directory", stat.Name())
} }
if stat.Size() > MaxChartFileSize { if stat.Size() > helm.MaxChartFileSize {
return nil, fmt.Errorf("size of '%s' exceeds '%d' limit", stat.Name(), MaxChartFileSize) return nil, fmt.Errorf("size of '%s' exceeds '%d' limit", stat.Name(), helm.MaxChartFileSize)
} }
} }
@ -142,8 +144,8 @@ func LoadChartMetadataFromArchive(archive string) (*helmchart.Metadata, error) {
} }
return nil, err return nil, err
} }
if stat.Size() > MaxChartSize { if stat.Size() > helm.MaxChartSize {
return nil, fmt.Errorf("size of chart '%s' exceeds '%d' limit", stat.Name(), MaxChartSize) return nil, fmt.Errorf("size of chart '%s' exceeds '%d' limit", stat.Name(), helm.MaxChartSize)
} }
f, err := os.Open(archive) f, err := os.Open(archive)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package chart
import ( import (
"testing" "testing"
@ -25,6 +25,19 @@ import (
) )
var ( var (
// helmPackageFile contains the path to a Helm package in the v2 format
// without any dependencies
helmPackageFile = "../testdata/charts/helmchart-0.1.0.tgz"
chartName = "helmchart"
chartVersion = "0.1.0"
// helmPackageV1File contains the path to a Helm package in the v1 format,
// including dependencies in a requirements.yaml file which should be
// loaded
helmPackageV1File = "../testdata/charts/helmchartwithdeps-v1-0.3.0.tgz"
chartNameV1 = "helmchartwithdeps-v1"
chartVersionV1 = "0.3.0"
originalValuesFixture = []byte(`override: original originalValuesFixture = []byte(`override: original
`) `)
chartFilesFixture = []*helmchart.File{ chartFilesFixture = []*helmchart.File{
@ -123,21 +136,21 @@ func TestLoadChartMetadataFromDir(t *testing.T) {
}{ }{
{ {
name: "Loads from dir", name: "Loads from dir",
dir: "testdata/charts/helmchart", dir: "../testdata/charts/helmchart",
wantName: "helmchart", wantName: "helmchart",
wantVersion: "0.1.0", wantVersion: "0.1.0",
}, },
{ {
name: "Loads from v1 dir including requirements.yaml", name: "Loads from v1 dir including requirements.yaml",
dir: "testdata/charts/helmchartwithdeps-v1", dir: "../testdata/charts/helmchartwithdeps-v1",
wantName: chartNameV1, wantName: chartNameV1,
wantVersion: chartVersionV1, wantVersion: chartVersionV1,
wantDependencyCount: 1, wantDependencyCount: 1,
}, },
{ {
name: "Error if no Chart.yaml", name: "Error if no Chart.yaml",
dir: "testdata/charts/", dir: "../testdata/charts/",
wantErr: "testdata/charts/Chart.yaml: no such file or directory", wantErr: "../testdata/charts/Chart.yaml: no such file or directory",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -186,12 +199,12 @@ func TestLoadChartMetadataFromArchive(t *testing.T) {
}, },
{ {
name: "Error on not found", name: "Error on not found",
archive: "testdata/invalid.tgz", archive: "../testdata/invalid.tgz",
wantErr: "no such file or directory", wantErr: "no such file or directory",
}, },
{ {
name: "Error if no Chart.yaml", name: "Error if no Chart.yaml",
archive: "testdata/charts/empty.tgz", archive: "../testdata/charts/empty.tgz",
wantErr: "no 'Chart.yaml' found", wantErr: "no 'Chart.yaml' found",
}, },
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package getter
import ( import (
"fmt" "fmt"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package getter
import ( import (
"os" "os"

View File

@ -0,0 +1,41 @@
/*
Copyright 2021 The Flux authors
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 getter
import (
"bytes"
"helm.sh/helm/v3/pkg/getter"
)
// MockGetter can be used as a simple mocking getter.Getter implementation.
type MockGetter struct {
Response []byte
requestedURL string
}
func (g *MockGetter) Get(u string, _ ...getter.Option) (*bytes.Buffer, error) {
g.requestedURL = u
r := g.Response
return bytes.NewBuffer(r), nil
}
// LastGet returns the last requested URL for Get.
func (g *MockGetter) LastGet() string {
return g.requestedURL
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package repository
import ( import (
"bytes" "bytes"
@ -36,6 +36,8 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/version" "github.com/fluxcd/pkg/version"
"github.com/fluxcd/source-controller/internal/helm"
) )
var ErrNoChartIndex = errors.New("no chart index") var ErrNoChartIndex = errors.New("no chart index")
@ -241,8 +243,8 @@ func (r *ChartRepository) LoadFromFile(path string) error {
} }
return err return err
} }
if stat.Size() > MaxIndexSize { if stat.Size() > helm.MaxIndexSize {
return fmt.Errorf("size of index '%s' exceeds '%d' limit", stat.Name(), MaxIndexSize) return fmt.Errorf("size of index '%s' exceeds '%d' limit", stat.Name(), helm.MaxIndexSize)
} }
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
@ -350,7 +352,7 @@ func (r *ChartRepository) HasCacheFile() bool {
} }
// Unload can be used to signal the Go garbage collector the Index can // Unload can be used to signal the Go garbage collector the Index can
// be freed from memory if the ChartRepository object is expected to // be freed from memory if the Index object is expected to
// continue to exist in the stack for some time. // continue to exist in the stack for some time.
func (r *ChartRepository) Unload() { func (r *ChartRepository) Unload() {
if r == nil { if r == nil {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package repository
import ( import (
"bytes" "bytes"
@ -27,39 +27,29 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/getter" helmgetter "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/repo"
"github.com/fluxcd/source-controller/internal/helm/getter"
) )
var now = time.Now() var now = time.Now()
const ( const (
testFile = "testdata/local-index.yaml" testFile = "../testdata/local-index.yaml"
chartmuseumTestFile = "testdata/chartmuseum-index.yaml" chartmuseumTestFile = "../testdata/chartmuseum-index.yaml"
unorderedTestFile = "testdata/local-index-unordered.yaml" unorderedTestFile = "../testdata/local-index-unordered.yaml"
) )
// mockGetter can be used as a simple mocking getter.Getter implementation.
type mockGetter struct {
requestedURL string
response []byte
}
func (g *mockGetter) Get(url string, _ ...getter.Option) (*bytes.Buffer, error) {
g.requestedURL = url
r := g.response
return bytes.NewBuffer(r), nil
}
func TestNewChartRepository(t *testing.T) { func TestNewChartRepository(t *testing.T) {
repositoryURL := "https://example.com" repositoryURL := "https://example.com"
providers := getter.Providers{ providers := helmgetter.Providers{
getter.Provider{ helmgetter.Provider{
Schemes: []string{"https"}, Schemes: []string{"https"},
New: getter.NewHTTPGetter, New: helmgetter.NewHTTPGetter,
}, },
} }
options := []getter.Option{getter.WithBasicAuth("username", "password")} options := []helmgetter.Option{helmgetter.WithBasicAuth("username", "password")}
t.Run("should construct chart repository", func(t *testing.T) { t.Run("should construct chart repository", func(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
@ -230,7 +220,7 @@ func TestChartRepository_DownloadChart(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
t.Parallel() t.Parallel()
mg := mockGetter{} mg := getter.MockGetter{}
r := &ChartRepository{ r := &ChartRepository{
URL: tt.url, URL: tt.url,
Client: &mg, Client: &mg,
@ -241,7 +231,7 @@ func TestChartRepository_DownloadChart(t *testing.T) {
g.Expect(res).To(BeNil()) g.Expect(res).To(BeNil())
return return
} }
g.Expect(mg.requestedURL).To(Equal(tt.wantURL)) g.Expect(mg.LastGet()).To(Equal(tt.wantURL))
g.Expect(res).ToNot(BeNil()) g.Expect(res).ToNot(BeNil())
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
}) })
@ -254,7 +244,7 @@ func TestChartRepository_DownloadIndex(t *testing.T) {
b, err := os.ReadFile(chartmuseumTestFile) b, err := os.ReadFile(chartmuseumTestFile)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
mg := mockGetter{response: b} mg := getter.MockGetter{Response: b}
r := &ChartRepository{ r := &ChartRepository{
URL: "https://example.com", URL: "https://example.com",
Client: &mg, Client: &mg,
@ -263,7 +253,7 @@ func TestChartRepository_DownloadIndex(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
g.Expect(r.DownloadIndex(buf)).To(Succeed()) g.Expect(r.DownloadIndex(buf)).To(Succeed())
g.Expect(buf.Bytes()).To(Equal(b)) g.Expect(buf.Bytes()).To(Equal(b))
g.Expect(mg.requestedURL).To(Equal(r.URL + "/index.yaml")) g.Expect(mg.LastGet()).To(Equal(r.URL + "/index.yaml"))
g.Expect(err).To(BeNil()) g.Expect(err).To(BeNil())
} }
@ -384,8 +374,8 @@ func TestChartRepository_LoadIndexFromFile(t *testing.T) {
func TestChartRepository_CacheIndex(t *testing.T) { func TestChartRepository_CacheIndex(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
mg := mockGetter{response: []byte("foo")} mg := getter.MockGetter{Response: []byte("foo")}
expectSum := fmt.Sprintf("%x", sha256.Sum256(mg.response)) expectSum := fmt.Sprintf("%x", sha256.Sum256(mg.Response))
r := newChartRepository() r := newChartRepository()
r.URL = "https://example.com" r.URL = "https://example.com"
@ -399,7 +389,7 @@ func TestChartRepository_CacheIndex(t *testing.T) {
g.Expect(r.CachePath).To(BeARegularFile()) g.Expect(r.CachePath).To(BeARegularFile())
b, _ := os.ReadFile(r.CachePath) b, _ := os.ReadFile(r.CachePath)
g.Expect(b).To(Equal(mg.response)) g.Expect(b).To(Equal(mg.Response))
g.Expect(sum).To(BeEquivalentTo(expectSum)) g.Expect(sum).To(BeEquivalentTo(expectSum))
} }

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Flux authors Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helm package repository
import "strings" import "strings"
// NormalizeChartRepositoryURL ensures repository urls are normalized // NormalizeURL normalizes a ChartRepository URL by ensuring it ends with a
func NormalizeChartRepositoryURL(url string) string { // single "/".
func NormalizeURL(url string) string {
if url != "" { if url != "" {
return strings.TrimRight(url, "/") + "/" return strings.TrimRight(url, "/") + "/"
} }

View File

@ -0,0 +1,44 @@
package repository
import (
"testing"
. "github.com/onsi/gomega"
)
func TestNormalizeURL(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "with slash",
url: "http://example.com/",
want: "http://example.com/",
},
{
name: "without slash",
url: "http://example.com",
want: "http://example.com/",
},
{
name: "double slash",
url: "http://example.com//",
want: "http://example.com/",
},
{
name: "empty",
url: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got := NormalizeURL(tt.url)
g.Expect(got).To(Equal(tt.want))
})
}
}

View File

@ -1,60 +0,0 @@
/*
Copyright 2021 The Flux authors
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 helm
import (
"testing"
. "github.com/onsi/gomega"
)
func TestNormalizeChartRepositoryURL(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "with slash",
url: "http://example.com/",
want: "http://example.com/",
},
{
name: "without slash",
url: "http://example.com",
want: "http://example.com/",
},
{
name: "double slash",
url: "http://example.com//",
want: "http://example.com/",
},
{
name: "empty",
url: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got := NormalizeChartRepositoryURL(tt.url)
g.Expect(got).To(Equal(tt.want))
})
}
}