source-controller/internal/helm/chart/builder_local.go

195 lines
6.2 KiB
Go

/*
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 chart
import (
"context"
"fmt"
"os"
"strings"
"github.com/Masterminds/semver/v3"
securejoin "github.com/cyphar/filepath-securejoin"
"helm.sh/helm/v3/pkg/chart/loader"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/runtime/transform"
)
type localChartBuilder struct {
dm *DependencyManager
}
// NewLocalBuilder returns a Builder capable of building a Helm
// chart with a LocalReference. For chart references pointing to a
// directory, the DependencyManager is used to resolve missing local and
// remote dependencies.
func NewLocalBuilder(dm *DependencyManager) Builder {
return &localChartBuilder{
dm: dm,
}
}
func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) {
localRef, ok := ref.(LocalReference)
if !ok {
return nil, fmt.Errorf("expected local chart reference")
}
if err := ref.Validate(); err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
// Load the chart metadata from the LocalReference to ensure it points
// to a chart
curMeta, err := LoadChartMetadata(localRef.Path)
if err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
result := &Build{}
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 {
err = fmt.Errorf("failed to parse version from chart metadata as SemVer: %w", err)
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
}
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
err = fmt.Errorf("failed to set SemVer metadata on chart version: %w", err)
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: 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.ValuesFiles = opts.ValuesFiles
return result, nil
}
}
// If the chart at the path is already packaged and no custom values files
// options are set, we can copy the chart without making modifications
isChartDir := pathIsDir(localRef.Path)
if !isChartDir && len(opts.GetValuesFiles()) == 0 {
if err = copyFileToPath(localRef.Path, p); err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
result.Path = p
return result, nil
}
// Merge chart values, if instructed
var mergedValues map[string]interface{}
if len(opts.GetValuesFiles()) > 0 {
if mergedValues, err = mergeFileValues(localRef.WorkDir, opts.ValuesFiles); err != nil {
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: 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, &BuildError{Reason: ErrChartPackage, Err: 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, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
}
result.ValuesFiles = opts.GetValuesFiles()
}
// Ensure dependencies are fetched if building from a directory
if isChartDir {
if b.dm == nil {
err = fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err}
}
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil {
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err}
}
}
// Package the chart
if err = packageToPath(chart, p); err != nil {
return nil, &BuildError{Reason: ErrChartPackage, Err: 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
}