Refactor and document DependencyManager

Mostly to re-use the fields of the structure instead of copying things
around.

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2020-12-15 15:00:37 +01:00
parent 8d0b54e431
commit 29a051c5f4
3 changed files with 101 additions and 63 deletions

View File

@ -515,7 +515,7 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context,
if strings.HasPrefix(dep.Repository, "file://") { if strings.HasPrefix(dep.Repository, "file://") {
dwr = append(dwr, &helm.DependencyWithRepository{ dwr = append(dwr, &helm.DependencyWithRepository{
Dependency: dep, Dependency: dep,
Repo: nil, Repository: nil,
}) })
continue continue
} }
@ -577,15 +577,15 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context,
dwr = append(dwr, &helm.DependencyWithRepository{ dwr = append(dwr, &helm.DependencyWithRepository{
Dependency: dep, Dependency: dep,
Repo: chartRepo, Repository: chartRepo,
}) })
} }
// Construct dependencies for chart if any // Construct dependencies for chart if any
if len(dwr) > 0 { if len(dwr) > 0 {
dm := &helm.DependencyManager{ dm := &helm.DependencyManager{
BaseDir: tmpDir, WorkingDir: tmpDir,
ChartPath: chartPath, ChartPath: chart.Spec.Chart,
Chart: helmChart, Chart: helmChart,
Dependencies: dwr, Dependencies: dwr,
} }

View File

@ -19,6 +19,7 @@ package helm
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -30,22 +31,35 @@ import (
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
) )
// DependencyWithRepository is a container for a dependency and its respective // DependencyWithRepository is a container for a Helm chart dependency
// repository // and its respective repository.
type DependencyWithRepository struct { type DependencyWithRepository struct {
// Dependency holds the reference to a chart.Chart dependency.
Dependency *helmchart.Dependency Dependency *helmchart.Dependency
Repo *ChartRepository // Repository is the ChartRepository the dependency should be
// available at and can be downloaded from. If there is none,
// a local ('file://') dependency is assumed.
Repository *ChartRepository
} }
// DependencyManager manages dependencies for helm charts // DependencyManager manages dependencies for a Helm chart.
type DependencyManager struct { type DependencyManager struct {
BaseDir string // WorkingDir is the chroot path for dependency manager operations,
ChartPath string // Dependencies that hold a local (relative) path reference are not
Chart *helmchart.Chart // allowed to traverse outside this directory.
WorkingDir string
// ChartPath is the path of the Chart relative to the WorkingDir,
// the combination of the WorkingDir and ChartPath is used to
// determine the absolute path of a local dependency.
ChartPath string
// Chart holds the loaded chart.Chart from the ChartPath.
Chart *helmchart.Chart
// Dependencies contains a list of dependencies, and the respective
// repository the dependency can be found at.
Dependencies []*DependencyWithRepository Dependencies []*DependencyWithRepository
} }
// Build compiles and builds the chart dependencies // Build compiles and builds the dependencies of the Chart.
func (dm *DependencyManager) Build(ctx context.Context) error { func (dm *DependencyManager) Build(ctx context.Context) error {
if len(dm.Dependencies) == 0 { if len(dm.Dependencies) == 0 {
return nil return nil
@ -60,85 +74,90 @@ func (dm *DependencyManager) Build(ctx context.Context) error {
default: default:
} }
var ( var err error
ch *helmchart.Chart switch item.Repository {
err error case nil:
) err = dm.addLocalDependency(item)
if strings.HasPrefix(item.Dependency.Repository, "file://") { default:
ch, err = chartForLocalDependency(item.Dependency, dm.BaseDir, dm.ChartPath) err = dm.addRemoteDependency(item)
} else {
ch, err = chartForRemoteDependency(item.Dependency, item.Repo)
} }
if err != nil { return err
return err
}
dm.Chart.AddDependency(ch)
return nil
}) })
} }
return errs.Wait() return errs.Wait()
} }
func chartForLocalDependency(dep *helmchart.Dependency, baseDir, chartPath string) (*helmchart.Chart, error) { func (dm *DependencyManager) addLocalDependency(dpr *DependencyWithRepository) error {
origPath, err := securejoin.SecureJoin(baseDir, sLocalChartPath, err := dm.secureLocalChartPath(dpr)
filepath.Join(strings.TrimPrefix(chartPath, baseDir), strings.TrimPrefix(dep.Repository, "file://")))
if err != nil { if err != nil {
return nil, err return err
} }
if _, err := os.Stat(origPath); os.IsNotExist(err) { if _, err := os.Stat(sLocalChartPath); err != nil {
err := fmt.Errorf("chart path %s not found: %w", origPath, err) if os.IsNotExist(err) {
return nil, err return fmt.Errorf("no chart found at '%s' (reference '%s') for dependency '%s'",
} else if err != nil { strings.TrimPrefix(sLocalChartPath, dm.WorkingDir), dpr.Dependency.Repository, dpr.Dependency.Name)
return nil, err }
return err
} }
ch, err := loader.Load(origPath) ch, err := loader.Load(sLocalChartPath)
if err != nil { if err != nil {
return nil, err return err
} }
constraint, err := semver.NewConstraint(dep.Version) constraint, err := semver.NewConstraint(dpr.Dependency.Version)
if err != nil { if err != nil {
err := fmt.Errorf("dependency %s has an invalid version/constraint format: %w", dep.Name, err) err := fmt.Errorf("dependency '%s' has an invalid version/constraint format: %w", dpr.Dependency.Name, err)
return nil, err return err
} }
v, err := semver.NewVersion(ch.Metadata.Version) v, err := semver.NewVersion(ch.Metadata.Version)
if err != nil { if err != nil {
return nil, err return err
} }
if !constraint.Check(v) { if !constraint.Check(v) {
err = fmt.Errorf("can't get a valid version for dependency %s", dep.Name) err = fmt.Errorf("can't get a valid version for dependency '%s'", dpr.Dependency.Name)
return nil, err return err
} }
return ch, nil dm.Chart.AddDependency(ch)
return nil
} }
func chartForRemoteDependency(dep *helmchart.Dependency, chartRepo *ChartRepository) (*helmchart.Chart, error) { func (dm *DependencyManager) addRemoteDependency(dpr *DependencyWithRepository) error {
if chartRepo == nil { if dpr.Repository == nil {
return nil, fmt.Errorf("chartrepo should not be nil") return fmt.Errorf("no ChartRepository given for '%s' dependency", dpr.Dependency.Name)
} }
// Lookup the chart version in the chart repository index chartVer, err := dpr.Repository.Get(dpr.Dependency.Name, dpr.Dependency.Version)
chartVer, err := chartRepo.Get(dep.Name, dep.Version)
if err != nil { if err != nil {
return nil, err return err
} }
// Download chart res, err := dpr.Repository.DownloadChart(chartVer)
res, err := chartRepo.DownloadChart(chartVer)
if err != nil { if err != nil {
return nil, err return err
} }
ch, err := loader.LoadArchive(res) ch, err := loader.LoadArchive(res)
if err != nil { if err != nil {
return nil, err return err
} }
return ch, nil dm.Chart.AddDependency(ch)
return nil
}
func (dm *DependencyManager) secureLocalChartPath(dep *DependencyWithRepository) (string, error) {
localUrl, err := url.Parse(dep.Dependency.Repository)
if err != nil {
return "", fmt.Errorf("failed to parse alleged local chart reference: %w", err)
}
if localUrl.Scheme != "file" {
return "", fmt.Errorf("'%s' is not a local chart reference", dep.Dependency.Repository)
}
return securejoin.SecureJoin(dm.WorkingDir, filepath.Join(dm.ChartPath, localUrl.Host, localUrl.Path))
} }

View File

@ -73,6 +73,15 @@ func TestBuild_WithLocalChart(t *testing.T) {
Repository: chartLocalRepository, Repository: chartLocalRepository,
}, },
}, },
{
name: "allowed traversing path",
dep: helmchart.Dependency{
Name: chartName,
Alias: "aliased",
Version: chartVersion,
Repository: "file://../../../testdata/charts/helmchartwithdeps/../helmchart",
},
},
{ {
name: "invalid path", name: "invalid path",
dep: helmchart.Dependency{ dep: helmchart.Dependency{
@ -81,7 +90,17 @@ func TestBuild_WithLocalChart(t *testing.T) {
Repository: "file://../invalid", Repository: "file://../invalid",
}, },
wantErr: true, wantErr: true,
errMsg: "no such file or directory", errMsg: "no chart found at",
},
{
name: "illegal traversing path",
dep: helmchart.Dependency{
Name: chartName,
Version: chartVersion,
Repository: "file://../../../../../controllers/testdata/charts/helmchart",
},
wantErr: true,
errMsg: "no chart found at",
}, },
{ {
name: "invalid version constraint format", name: "invalid version constraint format",
@ -109,13 +128,13 @@ func TestBuild_WithLocalChart(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := chartFixture c := chartFixture
dm := DependencyManager{ dm := DependencyManager{
BaseDir: "./", WorkingDir: "./",
ChartPath: "testdata/charts/helmchart", ChartPath: "testdata/charts/helmchart",
Chart: &c, Chart: &c,
Dependencies: []*DependencyWithRepository{ Dependencies: []*DependencyWithRepository{
{ {
Dependency: &tt.dep, Dependency: &tt.dep,
Repo: nil, Repository: nil,
}, },
}, },
} }
@ -132,7 +151,7 @@ func TestBuild_WithLocalChart(t *testing.T) {
} }
return return
} else if err != nil { } else if err != nil {
t.Errorf("Build() expected to return error") t.Errorf("Build() not expected to return an error: %s", err)
return return
} }
@ -168,7 +187,7 @@ func TestBuild_WithRemoteChart(t *testing.T) {
Dependencies: []*DependencyWithRepository{ Dependencies: []*DependencyWithRepository{
{ {
Dependency: &remoteDepFixture, Dependency: &remoteDepFixture,
Repo: cr, Repository: cr,
}, },
}, },
} }
@ -189,10 +208,10 @@ func TestBuild_WithRemoteChart(t *testing.T) {
} }
// When repo is not set // When repo is not set
dm.Dependencies[0].Repo = nil dm.Dependencies[0].Repository = nil
if err := dm.Build(context.TODO()); err == nil { if err := dm.Build(context.TODO()); err == nil {
t.Errorf("Build() expected to return error") t.Errorf("Build() expected to return error")
} else if !strings.Contains(err.Error(), "chartrepo should not be nil") { } else if !strings.Contains(err.Error(), "is not a local chart reference") {
t.Errorf("Build() expected to return different error, got: %s", err) t.Errorf("Build() expected to return different error, got: %s", err)
} }
} }