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

190 lines
5.7 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"
"path/filepath"
"regexp"
"strings"
helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"github.com/fluxcd/source-controller/internal/fs"
)
// Reference holds information to locate a chart.
type Reference interface {
// Validate returns an error if the Reference is not valid according
// to the spec of the interface implementation.
Validate() error
}
// LocalReference contains sufficient information to locate a chart on the
// local filesystem.
type LocalReference struct {
// WorkDir used as chroot during build operations.
// File references are not allowed to traverse outside it.
WorkDir string
// Path of the chart on the local filesystem.
Path string
}
// Validate returns an error if the LocalReference does not have
// a Path set.
func (r LocalReference) Validate() error {
if r.Path == "" {
return fmt.Errorf("no path set for local chart reference")
}
return nil
}
// RemoteReference contains sufficient information to look up a chart in
// a ChartRepository.
type RemoteReference 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 RemoteReference does not have
// a Name set.
func (r RemoteReference) Validate() error {
if r.Name == "" {
return fmt.Errorf("no name set for remote chart reference")
}
name := regexp.MustCompile("^([-a-z0-9]*)$")
if !name.MatchString(r.Name) {
return fmt.Errorf("invalid chart name '%s': a valid name must be lower case letters and numbers and MAY be separated with dashes (-)", r.Name)
}
return nil
}
// Builder is capable of building a (specific) chart Reference.
type Builder interface {
// Build builds and packages a Helm chart with the given Reference
// and BuildOptions and writes it to p. It returns the Build result,
// or an error. It may return an error for unsupported Reference
// implementations.
Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error)
}
// BuildOptions provides a list of options for Builder.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
}
// Build contains the Builder.Build result, including specific
// information about the built chart like ResolvedDependencies.
type Build 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 Builder has packaged the chart.
// This can for example be false if ValueFiles is empty and the chart
// source was already packaged.
Packaged bool
}
// Summary returns a human-readable summary of the Build.
func (b *Build) Summary() string {
if b == nil || b.Name == "" || b.Version == "" {
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 && len(b.ValueFiles) > 0 {
s.WriteString(fmt.Sprintf(", with merged value files %v", b.ValueFiles))
}
if b.Packaged && b.ResolvedDependencies > 0 {
s.WriteString(fmt.Sprintf(", resolving %d dependencies before packaging", b.ResolvedDependencies))
}
s.WriteString(".")
return s.String()
}
// String returns the Path of the Build.
func (b *Build) String() string {
if b == nil {
return ""
}
return b.Path
}
// packageToPath attempts to package the given chart to the out filepath.
func packageToPath(chart *helmchart.Chart, out string) error {
o, err := os.MkdirTemp("", "chart-build-*")
if err != nil {
return fmt.Errorf("failed to create temporary directory for chart: %w", err)
}
defer os.RemoveAll(o)
p, err := chartutil.Save(chart, o)
if err != nil {
return fmt.Errorf("failed to package chart: %w", err)
}
if err = fs.RenameWithFallback(p, out); err != nil {
return fmt.Errorf("failed to write chart to file: %w", err)
}
return nil
}