app: added helm tgz handle

This commit is contained in:
2026-03-12 16:59:29 +02:00
parent 0d67944966
commit 95ed9ddb97
3182 changed files with 957097 additions and 133 deletions
+182
View File
@@ -0,0 +1,182 @@
/*
Copyright The Helm 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 v2
import (
"path/filepath"
"regexp"
"strings"
"time"
"helm.sh/helm/v4/pkg/chart/common"
)
// APIVersionV1 is the API version number for version 1.
const APIVersionV1 = "v1"
// APIVersionV2 is the API version number for version 2.
const APIVersionV2 = "v2"
// aliasNameFormat defines the characters that are legal in an alias name.
var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
// Chart is a helm package that contains metadata, a default config, zero or more
// optionally parameterizable templates, and zero or more charts (dependencies).
type Chart struct {
// Raw contains the raw contents of the files originally contained in the chart archive.
//
// This should not be used except in special cases like `helm show values`,
// where we want to display the raw values, comments and all.
Raw []*common.File `json:"-"`
// Metadata is the contents of the Chartfile.
Metadata *Metadata `json:"metadata"`
// Lock is the contents of Chart.lock.
Lock *Lock `json:"lock"`
// Templates for this chart.
Templates []*common.File `json:"templates"`
// Values are default config for this chart.
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*common.File `json:"files"`
// ModTime the chart metadata was last modified
ModTime time.Time `json:"modtime,omitzero"`
parent *Chart
dependencies []*Chart
}
type CRD struct {
// Name is the File.Name for the crd file
Name string
// Filename is the File obj Name including (sub-)chart.ChartFullPath
Filename string
// File is the File obj for the crd
File *common.File
}
// SetDependencies replaces the chart dependencies.
func (ch *Chart) SetDependencies(charts ...*Chart) {
ch.dependencies = nil
ch.AddDependency(charts...)
}
// Name returns the name of the chart.
func (ch *Chart) Name() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.Name
}
// AddDependency determines if the chart is a subchart.
func (ch *Chart) AddDependency(charts ...*Chart) {
for i, x := range charts {
charts[i].parent = ch
ch.dependencies = append(ch.dependencies, x)
}
}
// Root finds the root chart.
func (ch *Chart) Root() *Chart {
if ch.IsRoot() {
return ch
}
return ch.Parent().Root()
}
// Dependencies are the charts that this chart depends on.
func (ch *Chart) Dependencies() []*Chart { return ch.dependencies }
// IsRoot determines if the chart is the root chart.
func (ch *Chart) IsRoot() bool { return ch.parent == nil }
// Parent returns a subchart's parent chart.
func (ch *Chart) Parent() *Chart { return ch.parent }
// ChartPath returns the full path to this chart in dot notation.
func (ch *Chart) ChartPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartPath() + "." + ch.Name()
}
return ch.Name()
}
// ChartFullPath returns the full path to this chart.
// Note that the path may not correspond to the path where the file can be found on the file system if the path
// points to an aliased subchart.
func (ch *Chart) ChartFullPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()
}
return ch.Name()
}
// Validate validates the metadata.
func (ch *Chart) Validate() error {
return ch.Metadata.Validate()
}
// AppVersion returns the appversion of the chart.
func (ch *Chart) AppVersion() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.AppVersion
}
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// Deprecated: use CRDObjects()
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
files = append(files, f)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
files = append(files, dep.CRDs()...)
}
return files
}
// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts
func (ch *Chart) CRDObjects() []CRD {
crds := []CRD{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f}
crds = append(crds, mycrd)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
crds = append(crds, dep.CRDObjects()...)
}
return crds
}
func hasManifestExtension(fname string) bool {
ext := filepath.Ext(fname)
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")
}
+82
View File
@@ -0,0 +1,82 @@
/*
Copyright The Helm 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 v2
import "time"
// Dependency describes a chart upon which another chart depends.
//
// Dependencies can be used to express developer intent, or to capture the state
// of a chart.
type Dependency struct {
// Name is the name of the dependency.
//
// This must mach the name in the dependency's Chart.yaml.
Name string `json:"name" yaml:"name"`
// Version is the version (range) of this chart.
//
// A lock file will always produce a single version, while a dependency
// may contain a semantic version range.
Version string `json:"version,omitempty" yaml:"version,omitempty"`
// The URL to the repository.
//
// Appending `index.yaml` to this string should result in a URL that can be
// used to fetch the repository index.
Repository string `json:"repository" yaml:"repository"`
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
// Tags can be used to group charts for enabling/disabling together
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
// Enabled bool determines if chart should be loaded
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
// string or pair of child/parent sublist items.
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
}
// Validate checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func (d *Dependency) Validate() error {
if d == nil {
return ValidationError("dependencies must not contain empty or null nodes")
}
d.Name = sanitizeString(d.Name)
d.Version = sanitizeString(d.Version)
d.Repository = sanitizeString(d.Repository)
d.Condition = sanitizeString(d.Condition)
for i := range d.Tags {
d.Tags[i] = sanitizeString(d.Tags[i])
}
if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
}
return nil
}
// Lock is a lock file for dependencies.
//
// It represents the state that the dependencies should be in.
type Lock struct {
// Generated is the date the lock file was last generated.
Generated time.Time `json:"generated"`
// Digest is a hash of the dependencies in Chart.yaml.
Digest string `json:"digest"`
// Dependencies is the list of dependencies that this lock file has locked.
Dependencies []*Dependency `json:"dependencies"`
}
+23
View File
@@ -0,0 +1,23 @@
/*
Copyright The Helm 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 v2 provides chart handling for apiVersion v1 and v2 charts
This package and its sub-packages provide handling for apiVersion v1 and v2 charts.
The changes from v1 to v2 charts are minor and were able to be handled with minor
switches based on characteristics.
*/
package v2
+30
View File
@@ -0,0 +1,30 @@
/*
Copyright The Helm 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 v2
import "fmt"
// ValidationError represents a data validation error.
type ValidationError string
func (v ValidationError) Error() string {
return "validation: " + string(v)
}
// ValidationErrorf takes a message and formatting options and creates a ValidationError
func ValidationErrorf(msg string, args ...interface{}) ValidationError {
return ValidationError(fmt.Sprintf(msg, args...))
}
+74
View File
@@ -0,0 +1,74 @@
/*
Copyright The Helm 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 loader
import (
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// FileLoader loads a chart from a file
type FileLoader string
// Load loads a chart
func (l FileLoader) Load() (*chart.Chart, error) {
return LoadFile(string(l))
}
// LoadFile loads from an archive file.
func LoadFile(name string) (*chart.Chart, error) {
if fi, err := os.Stat(name); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("cannot load a directory")
}
raw, err := os.Open(name)
if err != nil {
return nil, err
}
defer raw.Close()
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
c, err := LoadArchive(raw)
if err != nil {
if errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err)
}
}
return c, err
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}
return LoadFiles(files)
}
+122
View File
@@ -0,0 +1,122 @@
/*
Copyright The Helm 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 loader
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/ignore"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// DirLoader loads a chart from a directory
type DirLoader string
// Load loads the chart
func (l DirLoader) Load() (*chart.Chart, error) {
return LoadDir(string(l))
}
// LoadDir loads from a directory.
//
// This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
// Just used for errors.
c := &chart.Chart{}
rules := ignore.Empty()
ifile := filepath.Join(topdir, ignore.HelmIgnore)
if _, err := os.Stat(ifile); err == nil {
r, err := ignore.ParseFile(ifile)
if err != nil {
return c, err
}
rules = r
}
rules.AddDefaults()
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir)
if n == "" {
// No need to process top level. Avoid bug with helmignore .* matching
// empty names. See issue 1779.
return nil
}
// Normalize to / since it will also work on Windows
n = filepath.ToSlash(n)
if err != nil {
return err
}
if fi.IsDir() {
// Directory-based ignore rules should involve skipping the entire
// contents of that directory.
if rules.Ignore(n, fi) {
return filepath.SkipDir
}
return nil
}
// If a .helmignore file matches, skip this file.
if rules.Ignore(n, fi) {
return nil
}
// Irregular files include devices, sockets, and other uses of files that
// are not regular files. In Go they have a file mode type bit set.
// See https://golang.org/pkg/os/#FileMode for examples.
if !fi.Mode().IsRegular() {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
if err != nil {
return fmt.Errorf("error reading %s: %w", n, err)
}
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {
return c, err
}
return LoadFiles(files)
}
+249
View File
@@ -0,0 +1,249 @@
/*
Copyright The Helm 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 loader
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"maps"
"os"
"path/filepath"
"strings"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// ChartLoader loads a chart.
type ChartLoader interface {
Load() (*chart.Chart, error)
}
// Loader returns a new ChartLoader appropriate for the given chart name
func Loader(name string) (ChartLoader, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if fi.IsDir() {
return DirLoader(name), nil
}
return FileLoader(name), nil
}
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
//
// This is the preferred way to load a chart. It will discover the chart encoding
// and hand off to the appropriate chart reader.
//
// If a .helmignore file is present, the directory loader will skip loading any files
// matching it. But .helmignore is not evaluated when reading out of an archive.
func Load(name string) (*chart.Chart, error) {
l, err := Loader(name)
if err != nil {
return nil, err
}
return l.Load()
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files {
c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
if f.Name == "Chart.yaml" {
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
}
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
// NOTE(bacongobbler): while the chart specification says that APIVersion must be set,
// Helm 2 accepted charts that did not provide an APIVersion in their chart metadata.
// Because of that, if APIVersion is unset, we should assume we're loading a v1 chart.
if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV1
}
c.ModTime = f.ModTime
}
}
for _, f := range files {
switch {
case f.Name == "Chart.yaml":
// already processed
continue
case f.Name == "Chart.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
return c, fmt.Errorf("cannot load Chart.lock: %w", err)
}
case f.Name == "values.yaml":
values, err := LoadValues(bytes.NewReader(f.Data))
if err != nil {
return c, fmt.Errorf("cannot load values.yaml: %w", err)
}
c.Values = values
case f.Name == "values.schema.json":
c.Schema = f.Data
c.SchemaModTime = f.ModTime
// Deprecated: requirements.yaml is deprecated use Chart.yaml.
// We will handle it for you because we are nice people
case f.Name == "requirements.yaml":
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
}
if c.Metadata.APIVersion != chart.APIVersionV1 {
log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.")
}
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
}
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
// Deprecated: requirements.lock is deprecated use Chart.lock.
case f.Name == "requirements.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
return c, fmt.Errorf("cannot load requirements.lock: %w", err)
}
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
}
if c.Metadata.APIVersion != chart.APIVersionV1 {
log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.")
}
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
case strings.HasPrefix(f.Name, "templates/"):
c.Templates = append(c.Templates, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" {
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
continue
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
}
if c.Metadata == nil {
return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck
}
if err := c.Validate(); err != nil {
return c, err
}
for n, files := range subcharts {
var sc *chart.Chart
var err error
switch {
case strings.IndexAny(n, "_.") == 0:
continue
case filepath.Ext(n) == ".tgz":
file := files[0]
if file.Name != n {
return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
}
// Untar the chart and add to c.Dependencies
sc, err = LoadArchive(bytes.NewBuffer(file.Data))
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {
continue
}
f.Name = parts[1]
buff = append(buff, f)
}
sc, err = LoadFiles(buff)
}
if err != nil {
return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err)
}
c.AddDependency(sc)
}
return c, nil
}
// LoadValues loads values from a reader.
//
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
for {
currentMap := map[string]interface{}{}
raw, err := reader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading yaml document: %w", err)
}
if err := yaml.Unmarshal(raw, &currentMap); err != nil {
return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
}
values = MergeMaps(values, currentMap)
}
return values, nil
}
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
// If the value is a map, the maps will be merged recursively.
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
maps.Copy(out, a)
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = MergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}
+178
View File
@@ -0,0 +1,178 @@
/*
Copyright The Helm 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 v2
import (
"path/filepath"
"strings"
"unicode"
"github.com/Masterminds/semver/v3"
)
// Maintainer describes a Chart maintainer.
type Maintainer struct {
// Name is a user name or organization name
Name string `json:"name,omitempty"`
// Email is an optional email address to contact the named maintainer
Email string `json:"email,omitempty"`
// URL is an optional URL to an address for the named maintainer
URL string `json:"url,omitempty"`
}
// Validate checks valid data and sanitizes string characters.
func (m *Maintainer) Validate() error {
if m == nil {
return ValidationError("maintainers must not contain empty or null nodes")
}
m.Name = sanitizeString(m.Name)
m.Email = sanitizeString(m.Email)
m.URL = sanitizeString(m.URL)
return nil
}
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
type Metadata struct {
// The name of the chart. Required.
Name string `json:"name,omitempty"`
// The URL to a relevant project page, git repo, or contact person
Home string `json:"home,omitempty"`
// Source is the URL to the source code of this chart
Sources []string `json:"sources,omitempty"`
// A version string of the chart. Required.
Version string `json:"version,omitempty"`
// A one-sentence description of the chart
Description string `json:"description,omitempty"`
// A list of string keywords
Keywords []string `json:"keywords,omitempty"`
// A list of name and URL/email address combinations for the maintainer(s)
Maintainers []*Maintainer `json:"maintainers,omitempty"`
// The URL to an icon file.
Icon string `json:"icon,omitempty"`
// The API Version of this chart. Required.
APIVersion string `json:"apiVersion,omitempty"`
// The condition to check to enable chart
Condition string `json:"condition,omitempty"`
// The tags to check to enable chart
Tags string `json:"tags,omitempty"`
// The version of the application enclosed inside of this chart.
AppVersion string `json:"appVersion,omitempty"`
// Whether or not this chart is deprecated
Deprecated bool `json:"deprecated,omitempty"`
// Annotations are additional mappings uninterpreted by Helm,
// made available for inspection by other applications.
Annotations map[string]string `json:"annotations,omitempty"`
// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
KubeVersion string `json:"kubeVersion,omitempty"`
// Dependencies are a list of dependencies for a chart.
Dependencies []*Dependency `json:"dependencies,omitempty"`
// Specifies the chart type: application or library
Type string `json:"type,omitempty"`
}
// Validate checks the metadata for known issues and sanitizes string
// characters.
func (md *Metadata) Validate() error {
if md == nil {
return ValidationError("chart.metadata is required")
}
md.Name = sanitizeString(md.Name)
md.Description = sanitizeString(md.Description)
md.Home = sanitizeString(md.Home)
md.Icon = sanitizeString(md.Icon)
md.Condition = sanitizeString(md.Condition)
md.Tags = sanitizeString(md.Tags)
md.AppVersion = sanitizeString(md.AppVersion)
md.KubeVersion = sanitizeString(md.KubeVersion)
for i := range md.Sources {
md.Sources[i] = sanitizeString(md.Sources[i])
}
for i := range md.Keywords {
md.Keywords[i] = sanitizeString(md.Keywords[i])
}
if md.APIVersion == "" {
return ValidationError("chart.metadata.apiVersion is required")
}
if md.Name == "" {
return ValidationError("chart.metadata.name is required")
}
if md.Name != filepath.Base(md.Name) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
}
if md.Version == "" {
return ValidationError("chart.metadata.version is required")
}
if !isValidSemver(md.Version) {
return ValidationErrorf("chart.metadata.version %q is invalid", md.Version)
}
if !isValidChartType(md.Type) {
return ValidationError("chart.metadata.type must be application or library")
}
for _, m := range md.Maintainers {
if err := m.Validate(); err != nil {
return err
}
}
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
dependencies := map[string]*Dependency{}
for _, dependency := range md.Dependencies {
if err := dependency.Validate(); err != nil {
return err
}
key := dependency.Name
if dependency.Alias != "" {
key = dependency.Alias
}
if dependencies[key] != nil {
return ValidationErrorf("more than one dependency with name or alias %q", key)
}
dependencies[key] = dependency
}
return nil
}
func isValidChartType(in string) bool {
switch in {
case "", "application", "library":
return true
}
return false
}
func isValidSemver(v string) bool {
_, err := semver.NewVersion(v)
return err == nil
}
// sanitizeString normalize spaces and removes non-printable characters.
func sanitizeString(str string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if unicode.IsPrint(r) {
return r
}
return -1
}, str)
}