internal/helm: make ChartBuilder an interface
This commit refactors the `ChartBuilder` that used to be a do-it-all struct into an interace with two implementations: - `LocalChartBuilder`: to build charts from a source on the local filesystem, either from a directory or from a packaged chart. - `RemoteChartBuilder`: to build charts from a remote Helm repository index. The new logic within the builders validates the size of the Helm size it works with based on the `Max*Size` global variables in the internal `helm` package, to address the recommendation from the security audit. In addition, changes `ClientOptionsFromSecret` takes now a directory argument which temporary files are placed in, making it easier to perform a garbage collection of the whole directory at the end of a reconcile run. Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
parent
d23bcbb5db
commit
52459c899d
|
|
@ -19,6 +19,7 @@ package helm
|
|||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -35,30 +36,35 @@ import (
|
|||
)
|
||||
|
||||
// OverwriteChartDefaultValues overwrites the chart default values file with the given data.
|
||||
func OverwriteChartDefaultValues(chart *helmchart.Chart, data []byte) (bool, error) {
|
||||
// Read override values file data
|
||||
values, err := chartutil.ReadValues(data)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse provided override values file data")
|
||||
func OverwriteChartDefaultValues(chart *helmchart.Chart, vals chartutil.Values) (bool, error) {
|
||||
if vals == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var bVals bytes.Buffer
|
||||
if len(vals) > 0 {
|
||||
if err := vals.Encode(&bVals); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Replace current values file in Raw field
|
||||
for _, f := range chart.Raw {
|
||||
if f.Name == chartutil.ValuesfileName {
|
||||
// Do nothing if contents are equal
|
||||
if reflect.DeepEqual(f.Data, data) {
|
||||
if reflect.DeepEqual(f.Data, bVals.Bytes()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Replace in Files field
|
||||
for _, f := range chart.Files {
|
||||
if f.Name == chartutil.ValuesfileName {
|
||||
f.Data = data
|
||||
f.Data = bVals.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
f.Data = data
|
||||
chart.Values = values
|
||||
f.Data = bVals.Bytes()
|
||||
chart.Values = vals.AsMap()
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +106,21 @@ func LoadChartMetadataFromDir(dir string) (*helmchart.Metadata, error) {
|
|||
m.APIVersion = helmchart.APIVersionV1
|
||||
}
|
||||
|
||||
b, err = os.ReadFile(filepath.Join(dir, "requirements.yaml"))
|
||||
fp := filepath.Join(dir, "requirements.yaml")
|
||||
stat, err := os.Stat(fp)
|
||||
if (err != nil && !errors.Is(err, os.ErrNotExist)) || stat != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return nil, fmt.Errorf("'%s' is a directory", stat.Name())
|
||||
}
|
||||
if stat.Size() > MaxChartFileSize {
|
||||
return nil, fmt.Errorf("size of '%s' exceeds '%d' limit", stat.Name(), MaxChartFileSize)
|
||||
}
|
||||
}
|
||||
|
||||
b, err = os.ReadFile(fp)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -115,6 +135,17 @@ func LoadChartMetadataFromDir(dir string) (*helmchart.Metadata, error) {
|
|||
// LoadChartMetadataFromArchive loads the chart.Metadata from the "Chart.yaml" file in the archive at the given path.
|
||||
// It takes "requirements.yaml" files into account, and is therefore compatible with the chart.APIVersionV1 format.
|
||||
func LoadChartMetadataFromArchive(archive string) (*helmchart.Metadata, error) {
|
||||
stat, err := os.Stat(archive)
|
||||
if err != nil || stat.IsDir() {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("'%s' is a directory", stat.Name())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if stat.Size() > MaxChartSize {
|
||||
return nil, fmt.Errorf("size of chart '%s' exceeds '%d' limit", stat.Name(), MaxChartSize)
|
||||
}
|
||||
|
||||
f, err := os.Open(archive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -22,340 +22,147 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/fluxcd/source-controller/internal/fs"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/fluxcd/pkg/runtime/transform"
|
||||
)
|
||||
|
||||
// ChartBuilder aims to efficiently build a Helm chart from a directory or packaged chart.
|
||||
// It avoids or delays loading the chart into memory in full, working with chart.Metadata
|
||||
// as much as it can, and returns early (by copying over the already packaged source chart)
|
||||
// if no modifications were made during the build process.
|
||||
type ChartBuilder struct {
|
||||
// baseDir is the chroot for the chart builder when path isDir.
|
||||
// It must be (a higher) relative to path. File references (during e.g.
|
||||
// value file merge operations) are not allowed to traverse out of it.
|
||||
baseDir string
|
||||
|
||||
// path is the file or directory path to a chart source.
|
||||
path string
|
||||
|
||||
// chart holds a (partly) loaded chart.Chart, it contains at least the
|
||||
// chart.Metadata, which may expand to the full chart.Chart if required
|
||||
// for Build operations.
|
||||
chart *helmchart.Chart
|
||||
|
||||
// valueFiles holds a list of path references of valueFiles that should be
|
||||
// merged and packaged as a single "values.yaml" during Build.
|
||||
valueFiles []string
|
||||
|
||||
// repositories holds an index of repository URLs and their ChartRepository.
|
||||
// They are used to configure a DependencyManager for missing chart dependencies
|
||||
// if isDir is true.
|
||||
repositories map[string]*ChartRepository
|
||||
|
||||
// getChartRepositoryCallback is used to configure a DependencyManager for
|
||||
// missing chart dependencies if isDir is true.
|
||||
getChartRepositoryCallback GetChartRepositoryCallback
|
||||
|
||||
mu sync.Mutex
|
||||
// ChartReference holds information to locate a chart.
|
||||
type ChartReference interface {
|
||||
// Validate returns an error if the ChartReference is not valid according
|
||||
// to the spec of the interface implementation.
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// NewChartBuilder constructs a new ChartBuilder for the given chart path.
|
||||
// It returns an error if no chart.Metadata can be loaded from the path.
|
||||
func NewChartBuilder(path string) (*ChartBuilder, error) {
|
||||
metadata, err := LoadChartMetadata(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create new chart builder: %w", err)
|
||||
}
|
||||
return &ChartBuilder{
|
||||
path: path,
|
||||
chart: &helmchart.Chart{
|
||||
Metadata: metadata,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WithBaseDir configures the base dir on the ChartBuilder.
|
||||
func (b *ChartBuilder) WithBaseDir(p string) *ChartBuilder {
|
||||
b.mu.Lock()
|
||||
b.baseDir = p
|
||||
b.mu.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
// WithValueFiles appends the given paths to the ChartBuilder's valueFiles.
|
||||
func (b *ChartBuilder) WithValueFiles(path ...string) *ChartBuilder {
|
||||
b.mu.Lock()
|
||||
b.valueFiles = append(b.valueFiles, path...)
|
||||
b.mu.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
// WithChartRepository indexes the given ChartRepository by the NormalizeChartRepositoryURL,
|
||||
// used to configure the DependencyManager if the chart is not packaged.
|
||||
func (b *ChartBuilder) WithChartRepository(url string, index *ChartRepository) *ChartBuilder {
|
||||
b.mu.Lock()
|
||||
b.repositories[NormalizeChartRepositoryURL(url)] = index
|
||||
b.mu.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
// WithChartRepositoryCallback configures the GetChartRepositoryCallback used by the
|
||||
// DependencyManager if the chart is not packaged.
|
||||
func (b *ChartBuilder) WithChartRepositoryCallback(c GetChartRepositoryCallback) *ChartBuilder {
|
||||
b.mu.Lock()
|
||||
b.getChartRepositoryCallback = c
|
||||
b.mu.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
// ChartBuildResult contains the ChartBuilder result, including build specific
|
||||
// information about the chart.
|
||||
type ChartBuildResult struct {
|
||||
// SourceIsDir indicates if the chart was build from a directory.
|
||||
SourceIsDir bool
|
||||
// Path contains the absolute path to the packaged chart.
|
||||
// LocalChartReference contains sufficient information to locate a chart on the
|
||||
// local filesystem.
|
||||
type LocalChartReference struct {
|
||||
// BaseDir used as chroot during build operations.
|
||||
// File references are not allowed to traverse outside it.
|
||||
BaseDir string
|
||||
// Path of the chart on the local filesystem.
|
||||
Path string
|
||||
// ValuesOverwrite holds a structured map with the merged values used
|
||||
// to overwrite chart default "values.yaml".
|
||||
ValuesOverwrite map[string]interface{}
|
||||
// CollectedDependencies contains the number of missing local and remote
|
||||
// dependencies that were collected by the DependencyManager before building
|
||||
// the chart.
|
||||
CollectedDependencies int
|
||||
}
|
||||
|
||||
// Validate returns an error if the LocalChartReference does not have
|
||||
// a Path set.
|
||||
func (r LocalChartReference) Validate() error {
|
||||
if r.Path == "" {
|
||||
return fmt.Errorf("no path set for local chart reference")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteChartReference contains sufficient information to look up a chart in
|
||||
// a ChartRepository.
|
||||
type RemoteChartReference struct {
|
||||
// Name of the chart.
|
||||
Name string
|
||||
// Version of the chart.
|
||||
// Can be a Semver range, or empty for latest.
|
||||
Version string
|
||||
}
|
||||
|
||||
// Validate returns an error if the RemoteChartReference does not have
|
||||
// a Name set.
|
||||
func (r RemoteChartReference) Validate() error {
|
||||
if r.Name == "" {
|
||||
return fmt.Errorf("no name set for remote chart reference")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChartBuilder is capable of building a (specific) ChartReference.
|
||||
type ChartBuilder interface {
|
||||
// Build builds and packages a Helm chart with the given ChartReference
|
||||
// and BuildOptions and writes it to p. It returns the ChartBuild result,
|
||||
// or an error. It may return an error for unsupported ChartReference
|
||||
// implementations.
|
||||
Build(ctx context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error)
|
||||
}
|
||||
|
||||
// BuildOptions provides a list of options for ChartBuilder.Build.
|
||||
type BuildOptions struct {
|
||||
// VersionMetadata can be set to SemVer build metadata as defined in
|
||||
// the spec, and is included during packaging.
|
||||
// Ref: https://semver.org/#spec-item-10
|
||||
VersionMetadata string
|
||||
// ValueFiles can be set to a list of relative paths, used to compose
|
||||
// and overwrite an alternative default "values.yaml" for the chart.
|
||||
ValueFiles []string
|
||||
// CachedChart can be set to the absolute path of a chart stored on
|
||||
// the local filesystem, and is used for simple validation by metadata
|
||||
// comparisons.
|
||||
CachedChart string
|
||||
// Force can be set to force the build of the chart, for example
|
||||
// because the list of ValueFiles has changed.
|
||||
Force bool
|
||||
}
|
||||
|
||||
// GetValueFiles returns BuildOptions.ValueFiles, except if it equals
|
||||
// "values.yaml", which returns nil.
|
||||
func (o BuildOptions) GetValueFiles() []string {
|
||||
if len(o.ValueFiles) == 1 && filepath.Clean(o.ValueFiles[0]) == filepath.Clean(chartutil.ValuesfileName) {
|
||||
return nil
|
||||
}
|
||||
return o.ValueFiles
|
||||
}
|
||||
|
||||
// ChartBuild contains the ChartBuilder.Build result, including specific
|
||||
// information about the built chart like ResolvedDependencies.
|
||||
type ChartBuild struct {
|
||||
// Path is the absolute path to the packaged chart.
|
||||
Path string
|
||||
// Name of the packaged chart.
|
||||
Name string
|
||||
// Version of the packaged chart.
|
||||
Version string
|
||||
// ValueFiles is the list of files used to compose the chart's
|
||||
// default "values.yaml".
|
||||
ValueFiles []string
|
||||
// ResolvedDependencies is the number of local and remote dependencies
|
||||
// collected by the DependencyManager before building the chart.
|
||||
ResolvedDependencies int
|
||||
// Packaged indicates if the ChartBuilder has packaged the chart.
|
||||
// This can for example be false if SourceIsDir is false and ValuesOverwrite
|
||||
// is nil, which makes the ChartBuilder copy the chart source to Path without
|
||||
// making any modifications.
|
||||
// This can for example be false if ValueFiles is empty and the chart
|
||||
// source was already packaged.
|
||||
Packaged bool
|
||||
}
|
||||
|
||||
// String returns the Path of the ChartBuildResult.
|
||||
func (b *ChartBuildResult) String() string {
|
||||
// Summary returns a human-readable summary of the ChartBuild.
|
||||
func (b *ChartBuild) Summary() string {
|
||||
if b == nil {
|
||||
return "no chart build"
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
|
||||
action := "Fetched"
|
||||
if b.Packaged {
|
||||
action = "Packaged"
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s '%s' chart with version '%s'.", action, b.Name, b.Version))
|
||||
|
||||
if b.Packaged && b.ResolvedDependencies > 0 {
|
||||
s.WriteString(fmt.Sprintf(" Resolved %d dependencies before packaging.", b.ResolvedDependencies))
|
||||
}
|
||||
|
||||
if len(b.ValueFiles) > 0 {
|
||||
s.WriteString(fmt.Sprintf(" Merged %v value files into default chart values.", b.ValueFiles))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// String returns the Path of the ChartBuild.
|
||||
func (b *ChartBuild) String() string {
|
||||
if b != nil {
|
||||
return b.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Build attempts to build a new chart using ChartBuilder configuration,
|
||||
// writing it to the provided path.
|
||||
// It returns a ChartBuildResult containing all information about the resulting chart,
|
||||
// or an error.
|
||||
func (b *ChartBuilder) Build(ctx context.Context, p string) (_ *ChartBuildResult, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.chart == nil {
|
||||
err = fmt.Errorf("chart build failed: no initial chart (metadata) loaded")
|
||||
return
|
||||
}
|
||||
if b.path == "" {
|
||||
err = fmt.Errorf("chart build failed: no path set")
|
||||
return
|
||||
}
|
||||
|
||||
result := &ChartBuildResult{}
|
||||
result.SourceIsDir = pathIsDir(b.path)
|
||||
result.Path = p
|
||||
|
||||
// Merge chart values
|
||||
if err = b.mergeValues(result); err != nil {
|
||||
err = fmt.Errorf("chart build failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure chart has all dependencies
|
||||
if err = b.buildDependencies(ctx, result); err != nil {
|
||||
err = fmt.Errorf("chart build failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Package (or copy) chart
|
||||
if err = b.packageChart(result); err != nil {
|
||||
err = fmt.Errorf("chart package failed: %w", err)
|
||||
return
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// load lazy-loads chart.Chart into chart from the set path, it replaces any previously set
|
||||
// chart.Metadata shim.
|
||||
func (b *ChartBuilder) load() (err error) {
|
||||
if b.chart == nil || len(b.chart.Files) <= 0 {
|
||||
if b.path == "" {
|
||||
return fmt.Errorf("failed to load chart: path not set")
|
||||
}
|
||||
chart, err := loader.Load(b.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load chart: %w", err)
|
||||
}
|
||||
b.chart = chart
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// buildDependencies builds the missing dependencies for a chart from a directory.
|
||||
// Using the chart using a NewDependencyManager and the configured repositories
|
||||
// and getChartRepositoryCallback
|
||||
// It returns the number of dependencies it collected, or an error.
|
||||
func (b *ChartBuilder) buildDependencies(ctx context.Context, result *ChartBuildResult) (err error) {
|
||||
if !result.SourceIsDir {
|
||||
return
|
||||
}
|
||||
|
||||
if err = b.load(); err != nil {
|
||||
err = fmt.Errorf("failed to ensure chart has no missing dependencies: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
dm := NewDependencyManager(b.chart, b.baseDir, strings.TrimLeft(b.path, b.baseDir)).
|
||||
WithRepositories(b.repositories).
|
||||
WithChartRepositoryCallback(b.getChartRepositoryCallback)
|
||||
|
||||
result.CollectedDependencies, err = dm.Build(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// mergeValues strategically merges the valueFiles, it merges using mergeFileValues
|
||||
// or mergeChartValues depending on if the chart is sourced from a package or directory.
|
||||
// Ir only calls load to propagate the chart if required by the strategy.
|
||||
// It returns the merged values, or an error.
|
||||
func (b *ChartBuilder) mergeValues(result *ChartBuildResult) (err error) {
|
||||
if len(b.valueFiles) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if result.SourceIsDir {
|
||||
result.ValuesOverwrite, err = mergeFileValues(b.baseDir, b.valueFiles)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to merge value files: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Values equal to default
|
||||
if len(b.valueFiles) == 1 && b.valueFiles[0] == chartutil.ValuesfileName {
|
||||
return
|
||||
}
|
||||
|
||||
if err = b.load(); err != nil {
|
||||
err = fmt.Errorf("failed to merge chart values: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.ValuesOverwrite, err = mergeChartValues(b.chart, b.valueFiles); err != nil {
|
||||
err = fmt.Errorf("failed to merge chart values: %w", err)
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// packageChart determines if it should copyFileToPath or packageToPath
|
||||
// based on the provided result. It sets Packaged on ChartBuildResult to
|
||||
// true if packageToPath is successful.
|
||||
func (b *ChartBuilder) packageChart(result *ChartBuildResult) error {
|
||||
// If we are not building from a directory, and we do not have any
|
||||
// replacement values, we can copy over the already packaged source
|
||||
// chart without making any modifications
|
||||
if !result.SourceIsDir && len(result.ValuesOverwrite) == 0 {
|
||||
if err := copyFileToPath(b.path, result.Path); err != nil {
|
||||
return fmt.Errorf("chart build failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Package chart to a new temporary directory
|
||||
if err := packageToPath(b.chart, result.Path); err != nil {
|
||||
return fmt.Errorf("chart build failed: %w", err)
|
||||
}
|
||||
result.Packaged = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeChartValues merges the given chart.Chart Files paths into a single "values.yaml" map.
|
||||
// It returns the merge result, or an error.
|
||||
func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interface{}, error) {
|
||||
mergedValues := make(map[string]interface{})
|
||||
for _, p := range paths {
|
||||
cfn := filepath.Clean(p)
|
||||
if cfn == chartutil.ValuesfileName {
|
||||
mergedValues = transform.MergeMaps(mergedValues, chart.Values)
|
||||
continue
|
||||
}
|
||||
var b []byte
|
||||
for _, f := range chart.Files {
|
||||
if f.Name == cfn {
|
||||
b = f.Data
|
||||
break
|
||||
}
|
||||
}
|
||||
if b == nil {
|
||||
return nil, fmt.Errorf("no values file found at path '%s'", p)
|
||||
}
|
||||
values := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(b, &values); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling values from '%s' failed: %w", p, err)
|
||||
}
|
||||
mergedValues = transform.MergeMaps(mergedValues, values)
|
||||
}
|
||||
return mergedValues, nil
|
||||
}
|
||||
|
||||
// mergeFileValues merges the given value file paths into a single "values.yaml" map.
|
||||
// The provided (relative) paths may not traverse outside baseDir. It returns the merge
|
||||
// result, or an error.
|
||||
func mergeFileValues(baseDir string, paths []string) (map[string]interface{}, error) {
|
||||
mergedValues := make(map[string]interface{})
|
||||
for _, p := range paths {
|
||||
secureP, err := securejoin.SecureJoin(baseDir, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f, err := os.Stat(secureP); os.IsNotExist(err) || !f.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("no values file found at path '%s' (reference '%s')",
|
||||
strings.TrimPrefix(secureP, baseDir), p)
|
||||
}
|
||||
b, err := os.ReadFile(secureP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read values from file '%s': %w", p, err)
|
||||
}
|
||||
values := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(b, &values)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling values from '%s' failed: %w", p, err)
|
||||
}
|
||||
mergedValues = transform.MergeMaps(mergedValues, values)
|
||||
}
|
||||
return mergedValues, nil
|
||||
}
|
||||
|
||||
// copyFileToPath attempts to copy in to out. It returns an error if out already exists.
|
||||
func copyFileToPath(in, out string) error {
|
||||
o, err := os.Create(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create copy target: %w", err)
|
||||
}
|
||||
defer o.Close()
|
||||
i, err := os.Open(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file to copy from: %w", err)
|
||||
}
|
||||
defer i.Close()
|
||||
if _, err := o.ReadFrom(i); err != nil {
|
||||
return fmt.Errorf("failed to read from source during copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// packageToPath attempts to package the given chart.Chart to the out filepath.
|
||||
func packageToPath(chart *helmchart.Chart, out string) error {
|
||||
o, err := os.MkdirTemp("", "chart-build-*")
|
||||
|
|
@ -368,17 +175,8 @@ func packageToPath(chart *helmchart.Chart, out string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to package chart: %w", err)
|
||||
}
|
||||
return fs.RenameWithFallback(p, out)
|
||||
}
|
||||
|
||||
// pathIsDir returns a boolean indicating if the given path points to a directory.
|
||||
// In case os.Stat on the given path returns an error it returns false as well.
|
||||
func pathIsDir(p string) bool {
|
||||
if p == "" {
|
||||
return false
|
||||
if err = fs.RenameWithFallback(p, out); err != nil {
|
||||
return fmt.Errorf("failed to write chart to file: %w", err)
|
||||
}
|
||||
if i, err := os.Stat(p); err != nil || !i.IsDir() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/fluxcd/pkg/runtime/transform"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type localChartBuilder struct {
|
||||
dm *DependencyManager
|
||||
}
|
||||
|
||||
// NewLocalChartBuilder returns a ChartBuilder capable of building a Helm
|
||||
// chart with a LocalChartReference. For chart references pointing to a
|
||||
// directory, the DependencyManager is used to resolve missing local and
|
||||
// remote dependencies.
|
||||
func NewLocalChartBuilder(dm *DependencyManager) ChartBuilder {
|
||||
return &localChartBuilder{
|
||||
dm: dm,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *localChartBuilder) Build(ctx context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error) {
|
||||
localRef, ok := ref.(LocalChartReference)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected local chart reference")
|
||||
}
|
||||
|
||||
if err := ref.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load the chart metadata from the LocalChartReference to ensure it points
|
||||
// to a chart
|
||||
curMeta, err := LoadChartMetadata(localRef.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &ChartBuild{}
|
||||
result.Name = curMeta.Name
|
||||
|
||||
// Set build specific metadata if instructed
|
||||
result.Version = curMeta.Version
|
||||
if opts.VersionMetadata != "" {
|
||||
ver, err := semver.NewVersion(curMeta.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse chart version from metadata as SemVer: %w", err)
|
||||
}
|
||||
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to set metadata on chart version: %w", err)
|
||||
}
|
||||
result.Version = ver.String()
|
||||
}
|
||||
|
||||
// If all the following is true, we do not need to package the chart:
|
||||
// Chart version from metadata matches chart version for ref
|
||||
// BuildOptions.Force is False
|
||||
if opts.CachedChart != "" && !opts.Force {
|
||||
if curMeta, err = LoadChartMetadataFromArchive(opts.CachedChart); err == nil && result.Version == curMeta.Version {
|
||||
result.Path = opts.CachedChart
|
||||
result.ValueFiles = opts.ValueFiles
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If the chart at the path is already packaged and no custom value files
|
||||
// options are set, we can copy the chart without making modifications
|
||||
isChartDir := pathIsDir(localRef.Path)
|
||||
if !isChartDir && len(opts.GetValueFiles()) == 0 {
|
||||
if err := copyFileToPath(localRef.Path, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Path = p
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Merge chart values, if instructed
|
||||
var mergedValues map[string]interface{}
|
||||
if len(opts.GetValueFiles()) > 0 {
|
||||
if mergedValues, err = mergeFileValues(localRef.BaseDir, opts.ValueFiles); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge value files: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we are certain we need to load the chart;
|
||||
// either to package it because it originates from a directory,
|
||||
// or because we have merged values and need to repackage
|
||||
chart, err := loader.Load(localRef.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set earlier resolved version (with metadata)
|
||||
chart.Metadata.Version = result.Version
|
||||
|
||||
// Overwrite default values with merged values, if any
|
||||
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ValueFiles = opts.GetValueFiles()
|
||||
}
|
||||
|
||||
// Ensure dependencies are fetched if building from a directory
|
||||
if isChartDir {
|
||||
if b.dm == nil {
|
||||
return nil, fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
|
||||
}
|
||||
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Package the chart
|
||||
if err = packageToPath(chart, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Path = p
|
||||
result.Packaged = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeFileValues merges the given value file paths into a single "values.yaml" map.
|
||||
// The provided (relative) paths may not traverse outside baseDir. It returns the merge
|
||||
// result, or an error.
|
||||
func mergeFileValues(baseDir string, paths []string) (map[string]interface{}, error) {
|
||||
mergedValues := make(map[string]interface{})
|
||||
for _, p := range paths {
|
||||
secureP, err := securejoin.SecureJoin(baseDir, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f, err := os.Stat(secureP); os.IsNotExist(err) || !f.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("no values file found at path '%s' (reference '%s')",
|
||||
strings.TrimPrefix(secureP, baseDir), p)
|
||||
}
|
||||
b, err := os.ReadFile(secureP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read values from file '%s': %w", p, err)
|
||||
}
|
||||
values := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(b, &values)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling values from '%s' failed: %w", p, err)
|
||||
}
|
||||
mergedValues = transform.MergeMaps(mergedValues, values)
|
||||
}
|
||||
return mergedValues, nil
|
||||
}
|
||||
|
||||
// copyFileToPath attempts to copy in to out. It returns an error if out already exists.
|
||||
func copyFileToPath(in, out string) error {
|
||||
o, err := os.Create(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create copy target: %w", err)
|
||||
}
|
||||
defer o.Close()
|
||||
i, err := os.Open(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file to copy from: %w", err)
|
||||
}
|
||||
defer i.Close()
|
||||
if _, err := o.ReadFrom(i); err != nil {
|
||||
return fmt.Errorf("failed to read from source during copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
)
|
||||
|
||||
func Test_mergeFileValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files []*helmchart.File
|
||||
paths []string
|
||||
want map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "merges values from files",
|
||||
files: []*helmchart.File{
|
||||
{Name: "a.yaml", Data: []byte("a: b")},
|
||||
{Name: "b.yaml", Data: []byte("b: c")},
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
paths: []string{"a.yaml", "b.yaml", "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "illegal traverse",
|
||||
paths: []string{"../../../traversing/illegally/a/p/a/b"},
|
||||
wantErr: "no values file found at path '/traversing/illegally/a/p/a/b'",
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
files: []*helmchart.File{
|
||||
{Name: "invalid", Data: []byte("abcd")},
|
||||
},
|
||||
paths: []string{"invalid"},
|
||||
wantErr: "unmarshaling values from 'invalid' failed",
|
||||
},
|
||||
{
|
||||
name: "error on invalid path",
|
||||
paths: []string{"a.yaml"},
|
||||
wantErr: "no values file found at path '/a.yaml'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
baseDir, err := os.MkdirTemp("", "merge-file-values-*")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(baseDir)
|
||||
|
||||
for _, f := range tt.files {
|
||||
g.Expect(os.WriteFile(filepath.Join(baseDir, f.Name), f.Data, 0644)).To(Succeed())
|
||||
}
|
||||
|
||||
got, err := mergeFileValues(baseDir, tt.paths)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_copyFileToPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "copies input file",
|
||||
in: "testdata/local-index.yaml",
|
||||
},
|
||||
{
|
||||
name: "invalid input file",
|
||||
in: "testdata/invalid.tgz",
|
||||
wantErr: "failed to open file to copy from",
|
||||
},
|
||||
{
|
||||
name: "invalid input directory",
|
||||
in: "testdata/charts",
|
||||
wantErr: "failed to read from source during copy",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
out := tmpFile("copy-0.1.0", ".tgz")
|
||||
defer os.RemoveAll(out)
|
||||
err := copyFileToPath(tt.in, out)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(out).To(BeARegularFile())
|
||||
f1, err := os.ReadFile(tt.in)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
f2, err := os.ReadFile(out)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(f2).To(Equal(f1))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type remoteChartBuilder struct {
|
||||
remote *ChartRepository
|
||||
}
|
||||
|
||||
// NewRemoteChartBuilder returns a ChartBuilder capable of building a Helm
|
||||
// chart with a RemoteChartReference from the given ChartRepository.
|
||||
func NewRemoteChartBuilder(repository *ChartRepository) ChartBuilder {
|
||||
return &remoteChartBuilder{
|
||||
remote: repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *remoteChartBuilder) Build(_ context.Context, ref ChartReference, p string, opts BuildOptions) (*ChartBuild, error) {
|
||||
remoteRef, ok := ref.(RemoteChartReference)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected remote chart reference")
|
||||
}
|
||||
|
||||
if err := ref.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := b.remote.LoadFromCache(); err != nil {
|
||||
return nil, fmt.Errorf("could not load repository index for remote chart reference: %w", err)
|
||||
}
|
||||
defer b.remote.Unload()
|
||||
|
||||
// Get the current version for the RemoteChartReference
|
||||
cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chart version for remote reference: %w", err)
|
||||
}
|
||||
|
||||
result := &ChartBuild{}
|
||||
result.Name = cv.Name
|
||||
result.Version = cv.Version
|
||||
// Set build specific metadata if instructed
|
||||
if opts.VersionMetadata != "" {
|
||||
ver, err := semver.NewVersion(result.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Version = ver.String()
|
||||
}
|
||||
|
||||
// If all the following is true, we do not need to download and/or build the chart:
|
||||
// Chart version from metadata matches chart version for ref
|
||||
// BuildOptions.Force is False
|
||||
if opts.CachedChart != "" && !opts.Force {
|
||||
if curMeta, err := LoadChartMetadataFromArchive(opts.CachedChart); err == nil && result.Version == curMeta.Version {
|
||||
result.Path = opts.CachedChart
|
||||
result.ValueFiles = opts.GetValueFiles()
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Download the package for the resolved version
|
||||
res, err := b.remote.DownloadChart(cv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download chart for remote reference: %w", err)
|
||||
}
|
||||
|
||||
// Use literal chart copy from remote if no custom value files options are set
|
||||
if len(opts.GetValueFiles()) == 0 {
|
||||
if err = validatePackageAndWriteToPath(res, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Path = p
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Load the chart and merge chart values
|
||||
var chart *helmchart.Chart
|
||||
if chart, err = loader.LoadArchive(res); err != nil {
|
||||
return nil, fmt.Errorf("failed to load downloaded chart: %w", err)
|
||||
}
|
||||
|
||||
mergedValues, err := mergeChartValues(chart, opts.ValueFiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to merge chart values: %w", err)
|
||||
}
|
||||
// Overwrite default values with merged values, if any
|
||||
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ValueFiles = opts.GetValueFiles()
|
||||
}
|
||||
|
||||
// Package the chart with the custom values
|
||||
if err = packageToPath(chart, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Path = p
|
||||
result.Packaged = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeChartValues merges the given chart.Chart Files paths into a single "values.yaml" map.
|
||||
// It returns the merge result, or an error.
|
||||
func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interface{}, error) {
|
||||
mergedValues := make(map[string]interface{})
|
||||
for _, p := range paths {
|
||||
cfn := filepath.Clean(p)
|
||||
if cfn == chartutil.ValuesfileName {
|
||||
mergedValues = transform.MergeMaps(mergedValues, chart.Values)
|
||||
continue
|
||||
}
|
||||
var b []byte
|
||||
for _, f := range chart.Files {
|
||||
if f.Name == cfn {
|
||||
b = f.Data
|
||||
break
|
||||
}
|
||||
}
|
||||
if b == nil {
|
||||
return nil, fmt.Errorf("no values file found at path '%s'", p)
|
||||
}
|
||||
values := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(b, &values); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling values from '%s' failed: %w", p, err)
|
||||
}
|
||||
mergedValues = transform.MergeMaps(mergedValues, values)
|
||||
}
|
||||
return mergedValues, nil
|
||||
}
|
||||
|
||||
// validatePackageAndWriteToPath atomically writes the packaged chart from reader
|
||||
// to out while validating it by loading the chart metadata from the archive.
|
||||
func validatePackageAndWriteToPath(reader io.Reader, out string) error {
|
||||
tmpFile, err := os.CreateTemp("", filepath.Base(out))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temporary file for chart: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
if _, err = tmpFile.ReadFrom(reader); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
return fmt.Errorf("failed to write chart to file: %w", err)
|
||||
}
|
||||
if err = tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = LoadChartMetadataFromArchive(tmpFile.Name()); err != nil {
|
||||
return fmt.Errorf("failed to load chart metadata from written chart: %w", err)
|
||||
}
|
||||
if err = fs.RenameWithFallback(tmpFile.Name(), out); err != nil {
|
||||
return fmt.Errorf("failed to write chart to file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathIsDir returns a boolean indicating if the given path points to a directory.
|
||||
// In case os.Stat on the given path returns an error it returns false as well.
|
||||
func pathIsDir(p string) bool {
|
||||
if p == "" {
|
||||
return false
|
||||
}
|
||||
if i, err := os.Stat(p); err != nil || !i.IsDir() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
)
|
||||
|
||||
func Test_mergeChartValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chart *helmchart.Chart
|
||||
paths []string
|
||||
want map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "merges values",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "a.yaml", Data: []byte("a: b")},
|
||||
{Name: "b.yaml", Data: []byte("b: c")},
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
},
|
||||
paths: []string{"a.yaml", "b.yaml", "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses chart values",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"a": "b",
|
||||
},
|
||||
},
|
||||
paths: []string{chartutil.ValuesfileName, "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "invalid", Data: []byte("abcd")},
|
||||
},
|
||||
},
|
||||
paths: []string{"invalid"},
|
||||
wantErr: "unmarshaling values from 'invalid' failed",
|
||||
},
|
||||
{
|
||||
name: "error on invalid path",
|
||||
chart: &helmchart.Chart{},
|
||||
paths: []string{"a.yaml"},
|
||||
wantErr: "no values file found at path 'a.yaml'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := mergeChartValues(tt.chart, tt.paths)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pathIsDir(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p string
|
||||
want bool
|
||||
}{
|
||||
{name: "directory", p: "testdata/", want: true},
|
||||
{name: "file", p: "testdata/local-index.yaml", want: false},
|
||||
{name: "not found error", p: "testdata/does-not-exist.yaml", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(pathIsDir(tt.p)).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -17,545 +17,27 @@ limitations under the License.
|
|||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
func TestChartBuildResult_String(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
var result *ChartBuildResult
|
||||
var result *ChartBuild
|
||||
g.Expect(result.String()).To(Equal(""))
|
||||
result = &ChartBuildResult{}
|
||||
result = &ChartBuild{}
|
||||
g.Expect(result.String()).To(Equal(""))
|
||||
result = &ChartBuildResult{Path: "/foo/"}
|
||||
result = &ChartBuild{Path: "/foo/"}
|
||||
g.Expect(result.String()).To(Equal("/foo/"))
|
||||
}
|
||||
|
||||
func TestChartBuilder_Build(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseDir string
|
||||
path string
|
||||
valueFiles []string
|
||||
repositories map[string]*ChartRepository
|
||||
getChartRepositoryCallback GetChartRepositoryCallback
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "builds chart from directory",
|
||||
path: "testdata/charts/helmchart",
|
||||
},
|
||||
{
|
||||
name: "builds chart from package",
|
||||
path: "testdata/charts/helmchart-0.1.0.tgz",
|
||||
},
|
||||
{
|
||||
// TODO(hidde): add more diverse tests
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
b, err := NewChartBuilder(tt.path)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(b).ToNot(BeNil())
|
||||
|
||||
b.WithBaseDir(tt.baseDir)
|
||||
b.WithValueFiles(tt.valueFiles...)
|
||||
b.WithChartRepositoryCallback(b.getChartRepositoryCallback)
|
||||
for k, v := range tt.repositories {
|
||||
b.WithChartRepository(k, v)
|
||||
}
|
||||
|
||||
out := tmpFile("build-0.1.0", ".tgz")
|
||||
defer os.RemoveAll(out)
|
||||
got, err := b.Build(context.TODO(), out)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).ToNot(BeNil())
|
||||
|
||||
g.Expect(got.Path).ToNot(BeEmpty())
|
||||
g.Expect(got.Path).To(Equal(out))
|
||||
g.Expect(got.Path).To(BeARegularFile())
|
||||
_, err = loader.Load(got.Path)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartBuilder_load(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
chart *helmchart.Chart
|
||||
wantFunc func(g *WithT, c *helmchart.Chart)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "loads chart",
|
||||
chart: nil,
|
||||
path: "testdata/charts/helmchart-0.1.0.tgz",
|
||||
wantFunc: func(g *WithT, c *helmchart.Chart) {
|
||||
g.Expect(c.Metadata.Name).To(Equal("helmchart"))
|
||||
g.Expect(c.Files).ToNot(BeZero())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "overwrites chart without any files (metadata shim)",
|
||||
chart: &helmchart.Chart{
|
||||
Metadata: &helmchart.Metadata{Name: "dummy"},
|
||||
},
|
||||
path: "testdata/charts/helmchart-0.1.0.tgz",
|
||||
wantFunc: func(g *WithT, c *helmchart.Chart) {
|
||||
g.Expect(c.Metadata.Name).To(Equal("helmchart"))
|
||||
g.Expect(c.Files).ToNot(BeZero())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not overwrite loaded chart",
|
||||
chart: &helmchart.Chart{
|
||||
Metadata: &helmchart.Metadata{Name: "dummy"},
|
||||
Files: []*helmchart.File{
|
||||
{Name: "mock.yaml", Data: []byte("loaded chart")},
|
||||
},
|
||||
},
|
||||
path: "testdata/charts/helmchart-0.1.0.tgz",
|
||||
wantFunc: func(g *WithT, c *helmchart.Chart) {
|
||||
g.Expect(c.Metadata.Name).To(Equal("dummy"))
|
||||
g.Expect(c.Files).To(HaveLen(1))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no path",
|
||||
wantErr: "failed to load chart: path not set",
|
||||
},
|
||||
{
|
||||
name: "invalid chart",
|
||||
path: "testdata/charts/empty.tgz",
|
||||
wantErr: "failed to load chart: no files in chart archive",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
b := &ChartBuilder{
|
||||
path: tt.path,
|
||||
chart: tt.chart,
|
||||
}
|
||||
err := b.load()
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
if tt.wantFunc != nil {
|
||||
tt.wantFunc(g, b.chart)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartBuilder_buildDependencies(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
chartB, err := os.ReadFile("testdata/charts/helmchart-0.1.0.tgz")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(chartB).ToNot(BeEmpty())
|
||||
|
||||
mockRepo := func() *ChartRepository {
|
||||
return &ChartRepository{
|
||||
Client: &mockGetter{
|
||||
response: chartB,
|
||||
},
|
||||
Index: &repo.IndexFile{
|
||||
Entries: map[string]repo.ChartVersions{
|
||||
"grafana": {
|
||||
&repo.ChartVersion{
|
||||
Metadata: &helmchart.Metadata{
|
||||
Name: "grafana",
|
||||
Version: "6.17.4",
|
||||
},
|
||||
URLs: []string{"https://example.com/chart.tgz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RWMutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
var mockCallback GetChartRepositoryCallback = func(url string) (*ChartRepository, error) {
|
||||
if url == "https://grafana.github.io/helm-charts/" {
|
||||
return mockRepo(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("no repository for URL")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseDir string
|
||||
path string
|
||||
chart *helmchart.Chart
|
||||
fromDir bool
|
||||
repositories map[string]*ChartRepository
|
||||
getChartRepositoryCallback GetChartRepositoryCallback
|
||||
wantCollectedDependencies int
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "builds dependencies using callback",
|
||||
fromDir: true,
|
||||
baseDir: "testdata/charts",
|
||||
path: "testdata/charts/helmchartwithdeps",
|
||||
getChartRepositoryCallback: mockCallback,
|
||||
wantCollectedDependencies: 2,
|
||||
},
|
||||
{
|
||||
name: "builds dependencies using repositories",
|
||||
fromDir: true,
|
||||
baseDir: "testdata/charts",
|
||||
path: "testdata/charts/helmchartwithdeps",
|
||||
repositories: map[string]*ChartRepository{
|
||||
"https://grafana.github.io/helm-charts/": mockRepo(),
|
||||
},
|
||||
wantCollectedDependencies: 2,
|
||||
},
|
||||
{
|
||||
name: "skips dependency build for packaged chart",
|
||||
path: "testdata/charts/helmchart-0.1.0.tgz",
|
||||
},
|
||||
{
|
||||
name: "attempts to load chart",
|
||||
fromDir: true,
|
||||
path: "testdata",
|
||||
wantErr: "failed to ensure chart has no missing dependencies",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
b := &ChartBuilder{
|
||||
baseDir: tt.baseDir,
|
||||
path: tt.path,
|
||||
chart: tt.chart,
|
||||
repositories: tt.repositories,
|
||||
getChartRepositoryCallback: tt.getChartRepositoryCallback,
|
||||
}
|
||||
|
||||
result := &ChartBuildResult{SourceIsDir: tt.fromDir}
|
||||
err := b.buildDependencies(context.TODO(), result)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(result.CollectedDependencies).To(BeZero())
|
||||
g.Expect(b.chart).To(Equal(tt.chart))
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(result).ToNot(BeNil())
|
||||
g.Expect(result.CollectedDependencies).To(Equal(tt.wantCollectedDependencies))
|
||||
if tt.wantCollectedDependencies > 0 {
|
||||
g.Expect(b.chart).ToNot(Equal(tt.chart))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartBuilder_mergeValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseDir string
|
||||
path string
|
||||
isDir bool
|
||||
chart *helmchart.Chart
|
||||
valueFiles []string
|
||||
want map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "merges chart values",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "a.yaml", Data: []byte("a: b")},
|
||||
{Name: "b.yaml", Data: []byte("a: c")},
|
||||
},
|
||||
},
|
||||
valueFiles: []string{"a.yaml", "b.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "c",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chart values merge error",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "b.yaml", Data: []byte("a: c")},
|
||||
},
|
||||
},
|
||||
valueFiles: []string{"a.yaml"},
|
||||
wantErr: "failed to merge chart values",
|
||||
},
|
||||
{
|
||||
name: "merges file values",
|
||||
isDir: true,
|
||||
baseDir: "testdata/charts",
|
||||
path: "helmchart",
|
||||
valueFiles: []string{"helmchart/values-prod.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"replicaCount": float64(2),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "file values merge error",
|
||||
isDir: true,
|
||||
baseDir: "testdata/charts",
|
||||
path: "helmchart",
|
||||
valueFiles: []string{"invalid.yaml"},
|
||||
wantErr: "failed to merge value files",
|
||||
},
|
||||
{
|
||||
name: "error on chart load failure",
|
||||
baseDir: "testdata/charts",
|
||||
path: "invalid",
|
||||
wantErr: "failed to load chart",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
b := &ChartBuilder{
|
||||
baseDir: tt.baseDir,
|
||||
path: tt.path,
|
||||
chart: tt.chart,
|
||||
valueFiles: tt.valueFiles,
|
||||
}
|
||||
|
||||
result := &ChartBuildResult{SourceIsDir: tt.isDir}
|
||||
err := b.mergeValues(result)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(result.ValuesOverwrite).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(result.ValuesOverwrite).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergeChartValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chart *helmchart.Chart
|
||||
paths []string
|
||||
want map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "merges values",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "a.yaml", Data: []byte("a: b")},
|
||||
{Name: "b.yaml", Data: []byte("b: c")},
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
},
|
||||
paths: []string{"a.yaml", "b.yaml", "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses chart values",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"a": "b",
|
||||
},
|
||||
},
|
||||
paths: []string{chartutil.ValuesfileName, "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
chart: &helmchart.Chart{
|
||||
Files: []*helmchart.File{
|
||||
{Name: "invalid", Data: []byte("abcd")},
|
||||
},
|
||||
},
|
||||
paths: []string{"invalid"},
|
||||
wantErr: "unmarshaling values from 'invalid' failed",
|
||||
},
|
||||
{
|
||||
name: "error on invalid path",
|
||||
chart: &helmchart.Chart{},
|
||||
paths: []string{"a.yaml"},
|
||||
wantErr: "no values file found at path 'a.yaml'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := mergeChartValues(tt.chart, tt.paths)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergeFileValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files []*helmchart.File
|
||||
paths []string
|
||||
want map[string]interface{}
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "merges values from files",
|
||||
files: []*helmchart.File{
|
||||
{Name: "a.yaml", Data: []byte("a: b")},
|
||||
{Name: "b.yaml", Data: []byte("b: c")},
|
||||
{Name: "c.yaml", Data: []byte("b: d")},
|
||||
},
|
||||
paths: []string{"a.yaml", "b.yaml", "c.yaml"},
|
||||
want: map[string]interface{}{
|
||||
"a": "b",
|
||||
"b": "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "illegal traverse",
|
||||
paths: []string{"../../../traversing/illegally/a/p/a/b"},
|
||||
wantErr: "no values file found at path '/traversing/illegally/a/p/a/b'",
|
||||
},
|
||||
{
|
||||
name: "unmarshal error",
|
||||
files: []*helmchart.File{
|
||||
{Name: "invalid", Data: []byte("abcd")},
|
||||
},
|
||||
paths: []string{"invalid"},
|
||||
wantErr: "unmarshaling values from 'invalid' failed",
|
||||
},
|
||||
{
|
||||
name: "error on invalid path",
|
||||
paths: []string{"a.yaml"},
|
||||
wantErr: "no values file found at path '/a.yaml'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
baseDir, err := os.MkdirTemp("", "merge-file-values-*")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(baseDir)
|
||||
|
||||
for _, f := range tt.files {
|
||||
g.Expect(os.WriteFile(filepath.Join(baseDir, f.Name), f.Data, 0644)).To(Succeed())
|
||||
}
|
||||
|
||||
got, err := mergeFileValues(baseDir, tt.paths)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_copyFileToPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "copies input file",
|
||||
in: "testdata/local-index.yaml",
|
||||
},
|
||||
{
|
||||
name: "invalid input file",
|
||||
in: "testdata/invalid.tgz",
|
||||
wantErr: "failed to open file to copy from",
|
||||
},
|
||||
{
|
||||
name: "invalid input directory",
|
||||
in: "testdata/charts",
|
||||
wantErr: "failed to read from source during copy",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
out := tmpFile("copy-0.1.0", ".tgz")
|
||||
defer os.RemoveAll(out)
|
||||
err := copyFileToPath(tt.in, out)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(out).To(BeARegularFile())
|
||||
f1, err := os.ReadFile(tt.in)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
f2, err := os.ReadFile(out)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(f2).To(Equal(f1))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_packageToPath(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
|
|
@ -572,25 +54,6 @@ func Test_packageToPath(t *testing.T) {
|
|||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func Test_pathIsDir(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p string
|
||||
want bool
|
||||
}{
|
||||
{name: "directory", p: "testdata/", want: true},
|
||||
{name: "file", p: "testdata/local-index.yaml", want: false},
|
||||
{name: "not found error", p: "testdata/does-not-exist.yaml", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(pathIsDir(tt.p)).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tmpFile(prefix, suffix string) string {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
originalValuesFixture = []byte("override: original")
|
||||
chartFilesFixture = []*helmchart.File{
|
||||
originalValuesFixture = []byte(`override: original
|
||||
`)
|
||||
chartFilesFixture = []*helmchart.File{
|
||||
{
|
||||
Name: "values.yaml",
|
||||
Data: originalValuesFixture,
|
||||
|
|
@ -69,19 +70,14 @@ func TestOverwriteChartDefaultValues(t *testing.T) {
|
|||
desc: "valid override",
|
||||
chart: chartFixture,
|
||||
ok: true,
|
||||
data: []byte("override: test"),
|
||||
data: []byte(`override: test
|
||||
`),
|
||||
},
|
||||
{
|
||||
desc: "empty override",
|
||||
chart: chartFixture,
|
||||
ok: true,
|
||||
data: []byte(""),
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
chart: chartFixture,
|
||||
data: []byte("!fail:"),
|
||||
expectErr: true,
|
||||
data: []byte(``),
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
|
|
@ -89,7 +85,9 @@ func TestOverwriteChartDefaultValues(t *testing.T) {
|
|||
g := NewWithT(t)
|
||||
|
||||
fixture := tt.chart
|
||||
ok, err := OverwriteChartDefaultValues(&fixture, tt.data)
|
||||
vals, err := chartutil.ReadValues(tt.data)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
ok, err := OverwriteChartDefaultValues(&fixture, vals)
|
||||
g.Expect(ok).To(Equal(tt.ok))
|
||||
|
||||
if tt.expectErr {
|
||||
|
|
|
|||
|
|
@ -37,72 +37,77 @@ import (
|
|||
// or an error describing why it could not be returned.
|
||||
type GetChartRepositoryCallback func(url string) (*ChartRepository, error)
|
||||
|
||||
// DependencyManager manages dependencies for a Helm chart, downloading
|
||||
// only those that are missing from the chart it holds.
|
||||
// DependencyManager manages dependencies for a Helm chart.
|
||||
type DependencyManager struct {
|
||||
// chart contains the chart.Chart from the path.
|
||||
chart *helmchart.Chart
|
||||
|
||||
// baseDir is the chroot path for dependency manager operations,
|
||||
// Dependencies that hold a local (relative) path reference are not
|
||||
// allowed to traverse outside this directory.
|
||||
baseDir string
|
||||
|
||||
// path is the path of the chart relative to the baseDir,
|
||||
// the combination of the baseDir and path is used to
|
||||
// determine the absolute path of a local dependency.
|
||||
path string
|
||||
|
||||
// repositories contains a map of ChartRepository indexed by their
|
||||
// normalized URL. It is used as a lookup table for missing
|
||||
// dependencies.
|
||||
repositories map[string]*ChartRepository
|
||||
|
||||
// getChartRepositoryCallback can be set to an on-demand get
|
||||
// callback which returned result is cached to repositories.
|
||||
getChartRepositoryCallback GetChartRepositoryCallback
|
||||
// getRepositoryCallback can be set to an on-demand GetChartRepositoryCallback
|
||||
// which returned result is cached to repositories.
|
||||
getRepositoryCallback GetChartRepositoryCallback
|
||||
|
||||
// workers is the number of concurrent chart-add operations during
|
||||
// concurrent is the number of concurrent chart-add operations during
|
||||
// Build. Defaults to 1 (non-concurrent).
|
||||
workers int64
|
||||
concurrent int64
|
||||
|
||||
// mu contains the lock for chart writes.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewDependencyManager(chart *helmchart.Chart, baseDir, path string) *DependencyManager {
|
||||
return &DependencyManager{
|
||||
chart: chart,
|
||||
baseDir: baseDir,
|
||||
path: path,
|
||||
type DependencyManagerOption interface {
|
||||
applyToDependencyManager(dm *DependencyManager)
|
||||
}
|
||||
|
||||
type WithRepositories map[string]*ChartRepository
|
||||
|
||||
func (o WithRepositories) applyToDependencyManager(dm *DependencyManager) {
|
||||
dm.repositories = o
|
||||
}
|
||||
|
||||
type WithRepositoryCallback GetChartRepositoryCallback
|
||||
|
||||
func (o WithRepositoryCallback) applyToDependencyManager(dm *DependencyManager) {
|
||||
dm.getRepositoryCallback = GetChartRepositoryCallback(o)
|
||||
}
|
||||
|
||||
type WithConcurrent int64
|
||||
|
||||
func (o WithConcurrent) applyToDependencyManager(dm *DependencyManager) {
|
||||
dm.concurrent = int64(o)
|
||||
}
|
||||
|
||||
// NewDependencyManager returns a new DependencyManager configured with the given
|
||||
// DependencyManagerOption list.
|
||||
func NewDependencyManager(opts ...DependencyManagerOption) *DependencyManager {
|
||||
dm := &DependencyManager{}
|
||||
for _, v := range opts {
|
||||
v.applyToDependencyManager(dm)
|
||||
}
|
||||
}
|
||||
|
||||
func (dm *DependencyManager) WithRepositories(r map[string]*ChartRepository) *DependencyManager {
|
||||
dm.repositories = r
|
||||
return dm
|
||||
}
|
||||
|
||||
func (dm *DependencyManager) WithChartRepositoryCallback(c GetChartRepositoryCallback) *DependencyManager {
|
||||
dm.getChartRepositoryCallback = c
|
||||
return dm
|
||||
func (dm *DependencyManager) Clear() []error {
|
||||
var errs []error
|
||||
for _, v := range dm.repositories {
|
||||
v.Unload()
|
||||
errs = append(errs, v.RemoveCache())
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (dm *DependencyManager) WithWorkers(w int64) *DependencyManager {
|
||||
dm.workers = w
|
||||
return dm
|
||||
}
|
||||
|
||||
// Build compiles and builds the dependencies of the chart with the
|
||||
// configured number of workers.
|
||||
func (dm *DependencyManager) Build(ctx context.Context) (int, error) {
|
||||
// Build compiles a set of missing dependencies from chart.Chart, and attempts to
|
||||
// resolve and build them using the information from ChartReference.
|
||||
// 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) {
|
||||
// Collect dependency metadata
|
||||
var (
|
||||
deps = dm.chart.Dependencies()
|
||||
reqs = dm.chart.Metadata.Dependencies
|
||||
deps = chart.Dependencies()
|
||||
reqs = chart.Metadata.Dependencies
|
||||
)
|
||||
// Lock file takes precedence
|
||||
if lock := dm.chart.Lock; lock != nil {
|
||||
if lock := chart.Lock; lock != nil {
|
||||
reqs = lock.Dependencies
|
||||
}
|
||||
|
||||
|
|
@ -113,31 +118,32 @@ func (dm *DependencyManager) Build(ctx context.Context) (int, error) {
|
|||
}
|
||||
|
||||
// Run the build for the missing dependencies
|
||||
if err := dm.build(ctx, missing); err != nil {
|
||||
if err := dm.build(ctx, ref, chart, missing); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(missing), nil
|
||||
}
|
||||
|
||||
// build (concurrently) adds the given list of deps to the chart with the configured
|
||||
// number of workers. It returns the first error, cancelling all other workers.
|
||||
func (dm *DependencyManager) build(ctx context.Context, deps map[string]*helmchart.Dependency) error {
|
||||
workers := dm.workers
|
||||
if workers <= 0 {
|
||||
workers = 1
|
||||
}
|
||||
// chartWithLock holds a chart.Chart with a sync.Mutex to lock for writes.
|
||||
type chartWithLock struct {
|
||||
*helmchart.Chart
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Garbage collect temporary cached ChartRepository indexes
|
||||
defer func() {
|
||||
for _, v := range dm.repositories {
|
||||
v.Unload()
|
||||
_ = v.RemoveCache()
|
||||
}
|
||||
}()
|
||||
// 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
|
||||
// LocalChartReference is given, or any dependency could not be added, an error
|
||||
// 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 {
|
||||
current := dm.concurrent
|
||||
if current <= 0 {
|
||||
current = 1
|
||||
}
|
||||
|
||||
group, groupCtx := errgroup.WithContext(ctx)
|
||||
group.Go(func() error {
|
||||
sem := semaphore.NewWeighted(workers)
|
||||
sem := semaphore.NewWeighted(current)
|
||||
chart := &chartWithLock{Chart: chart}
|
||||
for name, dep := range deps {
|
||||
name, dep := name, dep
|
||||
if err := sem.Acquire(groupCtx, 1); err != nil {
|
||||
|
|
@ -146,12 +152,17 @@ func (dm *DependencyManager) build(ctx context.Context, deps map[string]*helmcha
|
|||
group.Go(func() (err error) {
|
||||
defer sem.Release(1)
|
||||
if isLocalDep(dep) {
|
||||
if err = dm.addLocalDependency(dep); err != nil {
|
||||
localRef, ok := ref.(LocalChartReference)
|
||||
if !ok {
|
||||
err = fmt.Errorf("failed to add local dependency '%s': no local chart reference", name)
|
||||
return
|
||||
}
|
||||
if err = dm.addLocalDependency(localRef, chart, dep); err != nil {
|
||||
err = fmt.Errorf("failed to add local dependency '%s': %w", name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = dm.addRemoteDependency(dep); err != nil {
|
||||
if err = dm.addRemoteDependency(chart, dep); err != nil {
|
||||
err = fmt.Errorf("failed to add remote dependency '%s': %w", name, err)
|
||||
}
|
||||
return
|
||||
|
|
@ -162,17 +173,17 @@ func (dm *DependencyManager) build(ctx context.Context, deps map[string]*helmcha
|
|||
return group.Wait()
|
||||
}
|
||||
|
||||
// addLocalDependency attempts to resolve and add the given local chart.Dependency to the chart.
|
||||
func (dm *DependencyManager) addLocalDependency(dep *helmchart.Dependency) error {
|
||||
sLocalChartPath, err := dm.secureLocalChartPath(dep)
|
||||
// addLocalDependency attempts to resolve and add the given local chart.Dependency
|
||||
// to the chart.
|
||||
func (dm *DependencyManager) addLocalDependency(ref LocalChartReference, chart *chartWithLock, dep *helmchart.Dependency) error {
|
||||
sLocalChartPath, err := dm.secureLocalChartPath(ref, dep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sLocalChartPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("no chart found at '%s' (reference '%s')",
|
||||
strings.TrimPrefix(sLocalChartPath, dm.baseDir), dep.Repository)
|
||||
return fmt.Errorf("no chart found at '%s' (reference '%s')", sLocalChartPath, dep.Repository)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -186,7 +197,7 @@ func (dm *DependencyManager) addLocalDependency(dep *helmchart.Dependency) error
|
|||
ch, err := loader.Load(sLocalChartPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load chart from '%s' (reference '%s'): %w",
|
||||
strings.TrimPrefix(sLocalChartPath, dm.baseDir), dep.Repository, err)
|
||||
strings.TrimPrefix(sLocalChartPath, ref.BaseDir), dep.Repository, err)
|
||||
}
|
||||
|
||||
ver, err := semver.NewVersion(ch.Metadata.Version)
|
||||
|
|
@ -199,14 +210,16 @@ func (dm *DependencyManager) addLocalDependency(dep *helmchart.Dependency) error
|
|||
return err
|
||||
}
|
||||
|
||||
dm.mu.Lock()
|
||||
dm.chart.AddDependency(ch)
|
||||
dm.mu.Unlock()
|
||||
chart.mu.Lock()
|
||||
chart.AddDependency(ch)
|
||||
chart.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// addRemoteDependency attempts to resolve and add the given remote chart.Dependency to the chart.
|
||||
func (dm *DependencyManager) addRemoteDependency(dep *helmchart.Dependency) error {
|
||||
// addRemoteDependency attempts to resolve and add the given remote chart.Dependency
|
||||
// to the chart. It locks the chartWithLock before the downloaded dependency is
|
||||
// added to the chart.
|
||||
func (dm *DependencyManager) addRemoteDependency(chart *chartWithLock, dep *helmchart.Dependency) error {
|
||||
repo, err := dm.resolveRepository(dep.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -216,7 +229,6 @@ func (dm *DependencyManager) addRemoteDependency(dep *helmchart.Dependency) erro
|
|||
return fmt.Errorf("failed to load index for '%s': %w", dep.Name, err)
|
||||
}
|
||||
|
||||
|
||||
ver, err := repo.Get(dep.Name, dep.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -230,28 +242,28 @@ func (dm *DependencyManager) addRemoteDependency(dep *helmchart.Dependency) erro
|
|||
return fmt.Errorf("failed to load downloaded archive of version '%s': %w", ver.Version, err)
|
||||
}
|
||||
|
||||
dm.mu.Lock()
|
||||
dm.chart.AddDependency(ch)
|
||||
dm.mu.Unlock()
|
||||
chart.mu.Lock()
|
||||
chart.AddDependency(ch)
|
||||
chart.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRepository first attempts to resolve the url from the repositories, falling back
|
||||
// to getChartRepositoryCallback if set. It returns the resolved ChartRepository, or an error.
|
||||
// to getRepositoryCallback if set. It returns the resolved ChartRepository, or an error.
|
||||
func (dm *DependencyManager) resolveRepository(url string) (_ *ChartRepository, err error) {
|
||||
dm.mu.Lock()
|
||||
defer dm.mu.Unlock()
|
||||
|
||||
nUrl := NormalizeChartRepositoryURL(url)
|
||||
if _, ok := dm.repositories[nUrl]; !ok {
|
||||
if dm.getChartRepositoryCallback == nil {
|
||||
if dm.getRepositoryCallback == nil {
|
||||
err = fmt.Errorf("no chart repository for URL '%s'", nUrl)
|
||||
return
|
||||
}
|
||||
if dm.repositories == nil {
|
||||
dm.repositories = map[string]*ChartRepository{}
|
||||
}
|
||||
if dm.repositories[nUrl], err = dm.getChartRepositoryCallback(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)
|
||||
return
|
||||
}
|
||||
|
|
@ -260,8 +272,9 @@ func (dm *DependencyManager) resolveRepository(url string) (_ *ChartRepository,
|
|||
}
|
||||
|
||||
// secureLocalChartPath returns the secure absolute path of a local dependency.
|
||||
// It does not allow the dependency's path to be outside the scope of baseDir.
|
||||
func (dm *DependencyManager) secureLocalChartPath(dep *helmchart.Dependency) (string, error) {
|
||||
// It does not allow the dependency's path to be outside the scope of
|
||||
// LocalChartReference.BaseDir.
|
||||
func (dm *DependencyManager) secureLocalChartPath(ref LocalChartReference, dep *helmchart.Dependency) (string, error) {
|
||||
localUrl, err := url.Parse(dep.Repository)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse alleged local chart reference: %w", err)
|
||||
|
|
@ -269,7 +282,11 @@ func (dm *DependencyManager) secureLocalChartPath(dep *helmchart.Dependency) (st
|
|||
if localUrl.Scheme != "" && localUrl.Scheme != "file" {
|
||||
return "", fmt.Errorf("'%s' is not a local chart reference", dep.Repository)
|
||||
}
|
||||
return securejoin.SecureJoin(dm.baseDir, filepath.Join(dm.path, localUrl.Host, localUrl.Path))
|
||||
relPath, err := filepath.Rel(ref.BaseDir, ref.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return securejoin.SecureJoin(ref.BaseDir, filepath.Join(relPath, localUrl.Host, localUrl.Path))
|
||||
}
|
||||
|
||||
// collectMissing returns a map with reqs that are missing from current,
|
||||
|
|
|
|||
|
|
@ -88,10 +88,10 @@ func TestDependencyManager_Build(t *testing.T) {
|
|||
chart, err := loader.Load(filepath.Join(tt.baseDir, tt.path))
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
got, err := NewDependencyManager(chart, tt.baseDir, tt.path).
|
||||
WithRepositories(tt.repositories).
|
||||
WithChartRepositoryCallback(tt.getChartRepositoryCallback).
|
||||
Build(context.TODO())
|
||||
got, err := NewDependencyManager(
|
||||
WithRepositories(tt.repositories),
|
||||
WithRepositoryCallback(tt.getChartRepositoryCallback),
|
||||
).Build(context.TODO(), LocalChartReference{BaseDir: tt.baseDir, Path: tt.path}, chart)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
|
|
@ -134,10 +134,8 @@ func TestDependencyManager_build(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
dm := &DependencyManager{
|
||||
baseDir: "testdata/charts",
|
||||
}
|
||||
err := dm.build(context.TODO(), tt.deps)
|
||||
dm := NewDependencyManager()
|
||||
err := dm.build(context.TODO(), LocalChartReference{}, &helmchart.Chart{}, tt.deps)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
return
|
||||
|
|
@ -182,7 +180,7 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
|
|||
Version: chartVersion,
|
||||
Repository: "file://../../../absolutely/invalid",
|
||||
},
|
||||
wantErr: "no chart found at 'absolutely/invalid'",
|
||||
wantErr: "no chart found at 'testdata/charts/absolutely/invalid'",
|
||||
},
|
||||
{
|
||||
name: "invalid chart archive",
|
||||
|
|
@ -191,7 +189,7 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
|
|||
Version: chartVersion,
|
||||
Repository: "file://../empty.tgz",
|
||||
},
|
||||
wantErr: "failed to load chart from 'empty.tgz'",
|
||||
wantErr: "failed to load chart from '/empty.tgz'",
|
||||
},
|
||||
{
|
||||
name: "invalid constraint",
|
||||
|
|
@ -207,13 +205,10 @@ func TestDependencyManager_addLocalDependency(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
dm := &DependencyManager{
|
||||
chart: &helmchart.Chart{},
|
||||
baseDir: "testdata/charts/",
|
||||
path: "helmchartwithdeps",
|
||||
}
|
||||
|
||||
err := dm.addLocalDependency(tt.dep)
|
||||
dm := NewDependencyManager()
|
||||
chart := &helmchart.Chart{}
|
||||
err := dm.addLocalDependency(LocalChartReference{BaseDir: "testdata/charts", Path: "helmchartwithdeps"},
|
||||
&chartWithLock{Chart: chart}, tt.dep)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
|
|
@ -389,10 +384,10 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
|
|||
g := NewWithT(t)
|
||||
|
||||
dm := &DependencyManager{
|
||||
chart: &helmchart.Chart{},
|
||||
repositories: tt.repositories,
|
||||
}
|
||||
err := dm.addRemoteDependency(tt.dep)
|
||||
chart := &helmchart.Chart{}
|
||||
err := dm.addRemoteDependency(&chartWithLock{Chart: chart}, tt.dep)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
|
|
@ -400,7 +395,7 @@ func TestDependencyManager_addRemoteDependency(t *testing.T) {
|
|||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
if tt.wantFunc != nil {
|
||||
tt.wantFunc(g, dm.chart)
|
||||
tt.wantFunc(g, chart)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -455,8 +450,8 @@ func TestDependencyManager_resolveRepository(t *testing.T) {
|
|||
g := NewWithT(t)
|
||||
|
||||
dm := &DependencyManager{
|
||||
repositories: tt.repositories,
|
||||
getChartRepositoryCallback: tt.getChartRepositoryCallback,
|
||||
repositories: tt.repositories,
|
||||
getRepositoryCallback: tt.getChartRepositoryCallback,
|
||||
}
|
||||
|
||||
got, err := dm.resolveRepository(tt.url)
|
||||
|
|
@ -522,11 +517,8 @@ func TestDependencyManager_secureLocalChartPath(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
dm := &DependencyManager{
|
||||
baseDir: tt.baseDir,
|
||||
path: tt.path,
|
||||
}
|
||||
got, err := dm.secureLocalChartPath(tt.dep)
|
||||
dm := NewDependencyManager()
|
||||
got, err := dm.secureLocalChartPath(LocalChartReference{BaseDir: tt.baseDir, Path: tt.path}, tt.dep)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
|
|
|
|||
|
|
@ -19,31 +19,30 @@ package helm
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// ClientOptionsFromSecret constructs a getter.Option slice for the given secret.
|
||||
// It returns the slice, and a callback to remove temporary files.
|
||||
func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, func(), error) {
|
||||
// It returns the slice, or an error.
|
||||
func ClientOptionsFromSecret(dir string, secret corev1.Secret) ([]getter.Option, error) {
|
||||
var opts []getter.Option
|
||||
basicAuth, err := BasicAuthFromSecret(secret)
|
||||
if err != nil {
|
||||
return opts, nil, err
|
||||
return opts, err
|
||||
}
|
||||
if basicAuth != nil {
|
||||
opts = append(opts, basicAuth)
|
||||
}
|
||||
tlsClientConfig, cleanup, err := TLSClientConfigFromSecret(secret)
|
||||
tlsClientConfig, err := TLSClientConfigFromSecret(dir, secret)
|
||||
if err != nil {
|
||||
return opts, nil, err
|
||||
return opts, err
|
||||
}
|
||||
if tlsClientConfig != nil {
|
||||
opts = append(opts, tlsClientConfig)
|
||||
}
|
||||
return opts, cleanup, nil
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// BasicAuthFromSecret attempts to construct a basic auth getter.Option for the
|
||||
|
|
@ -63,50 +62,65 @@ func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
|
|||
}
|
||||
|
||||
// TLSClientConfigFromSecret attempts to construct a TLS client config
|
||||
// getter.Option for the given v1.Secret. It returns the getter.Option and a
|
||||
// callback to remove the temporary TLS files.
|
||||
// getter.Option for the given v1.Secret, placing the required TLS config
|
||||
// related files in the given directory. It returns the getter.Option, or
|
||||
// an error.
|
||||
//
|
||||
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
||||
// certBytes OR keyBytes is defined it returns an error.
|
||||
func TLSClientConfigFromSecret(secret corev1.Secret) (getter.Option, func(), error) {
|
||||
func TLSClientConfigFromSecret(dir string, secret corev1.Secret) (getter.Option, error) {
|
||||
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
||||
switch {
|
||||
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
||||
return nil, func() {}, nil
|
||||
return nil, nil
|
||||
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
||||
return nil, nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
||||
return nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
||||
secret.Name)
|
||||
}
|
||||
|
||||
// create tmp dir for TLS files
|
||||
tmp, err := os.MkdirTemp("", "helm-tls-"+secret.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cleanup := func() { os.RemoveAll(tmp) }
|
||||
|
||||
var certFile, keyFile, caFile string
|
||||
|
||||
var certPath, keyPath, caPath string
|
||||
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
||||
certFile = filepath.Join(tmp, "cert.crt")
|
||||
if err := os.WriteFile(certFile, certBytes, 0644); err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
certFile, err := os.CreateTemp(dir, "cert-*.crt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyFile = filepath.Join(tmp, "key.crt")
|
||||
if err := os.WriteFile(keyFile, keyBytes, 0644); err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
if _, err = certFile.Write(certBytes); err != nil {
|
||||
_ = certFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = certFile.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPath = certFile.Name()
|
||||
|
||||
keyFile, err := os.CreateTemp(dir, "key-*.crt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = keyFile.Write(keyBytes); err != nil {
|
||||
_ = keyFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = keyFile.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPath = keyFile.Name()
|
||||
}
|
||||
|
||||
if len(caBytes) > 0 {
|
||||
caFile = filepath.Join(tmp, "ca.pem")
|
||||
if err := os.WriteFile(caFile, caBytes, 0644); err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
caFile, err := os.CreateTemp(dir, "ca-*.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = caFile.Write(caBytes); err != nil {
|
||||
_ = caFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = caFile.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caPath = caFile.Name()
|
||||
}
|
||||
|
||||
return getter.WithTLSClientConfig(certFile, keyFile, caFile), cleanup, nil
|
||||
return getter.WithTLSClientConfig(certPath, keyPath, caPath), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package helm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -56,10 +57,14 @@ func TestClientOptionsFromSecret(t *testing.T) {
|
|||
secret.Data[k] = v
|
||||
}
|
||||
}
|
||||
got, cleanup, err := ClientOptionsFromSecret(secret)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "client-opts-secret-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
got, err := ClientOptionsFromSecret(tmpDir, secret)
|
||||
if err != nil {
|
||||
t.Errorf("ClientOptionsFromSecret() error = %v", err)
|
||||
return
|
||||
|
|
@ -123,10 +128,14 @@ func TestTLSClientConfigFromSecret(t *testing.T) {
|
|||
if tt.modify != nil {
|
||||
tt.modify(secret)
|
||||
}
|
||||
got, cleanup, err := TLSClientConfigFromSecret(*secret)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "client-opts-secret-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
got, err := TLSClientConfigFromSecret(tmpDir, *secret)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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
|
||||
|
||||
// This list defines a set of global variables used to ensure Helm files loaded
|
||||
// into memory during runtime do not exceed defined upper bound limits.
|
||||
var (
|
||||
// MaxIndexSize is the max allowed file size in bytes of a ChartRepository.
|
||||
MaxIndexSize int64 = 50 << 20
|
||||
// MaxChartSize is the max allowed file size in bytes of a Helm Chart.
|
||||
MaxChartSize int64 = 2 << 20
|
||||
// MaxChartFileSize is the max allowed file size in bytes of any arbitrary
|
||||
// file originating from a chart.
|
||||
MaxChartFileSize int64 = 2 << 10
|
||||
)
|
||||
|
|
@ -234,6 +234,16 @@ func (r *ChartRepository) LoadIndexFromBytes(b []byte) error {
|
|||
|
||||
// LoadFromFile reads the file at the given path and loads it into Index.
|
||||
func (r *ChartRepository) LoadFromFile(path string) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil || stat.IsDir() {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("'%s' is a directory", path)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if stat.Size() > MaxIndexSize {
|
||||
return fmt.Errorf("size of index '%s' exceeds '%d' limit", stat.Name(), MaxIndexSize)
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -342,7 +352,7 @@ func (r *ChartRepository) HasCacheFile() bool {
|
|||
// Unload can be used to signal the Go garbage collector the Index can
|
||||
// be freed from memory if the ChartRepository object is expected to
|
||||
// continue to exist in the stack for some time.
|
||||
func (r *ChartRepository) Unload() {
|
||||
func (r *ChartRepository) Unload() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ func TestChartRepository_LoadFromCache(t *testing.T) {
|
|||
{
|
||||
name: "invalid cache path",
|
||||
cachePath: "invalid",
|
||||
wantErr: "open invalid: no such file",
|
||||
wantErr: "stat invalid: no such file",
|
||||
},
|
||||
{
|
||||
name: "no cache path",
|
||||
|
|
|
|||
Loading…
Reference in New Issue