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:
parent
9abbdd80a6
commit
7d0f79f41b
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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",
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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) {
|
|
@ -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())
|
||||||
|
|
|
@ -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,
|
|
@ -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))
|
|
@ -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)
|
|
@ -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",
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, "/") + "/"
|
||||||
}
|
}
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue