mirror of https://github.com/helm/helm
This change adds v3 charts. The code for the chart, including a loader, is present. It is based on v2 charts as a starting point. Note, this change does not make the charts available for use with Helm CLI commands or the action package. That will be in follow-up changes. Signed-off-by: Matt Farina <matt.farina@suse.com>pull/31081/head
parent
8face0e596
commit
70257f5cd6
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
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 v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIVersionV3 is the API version number for version 3.
|
||||||
|
const APIVersionV3 = "v3"
|
||||||
|
|
||||||
|
// 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 []*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 []*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"`
|
||||||
|
// Files are miscellaneous files in a chart archive,
|
||||||
|
// e.g. README, LICENSE, etc.
|
||||||
|
Files []*File `json:"files"`
|
||||||
|
|
||||||
|
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 *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() []*File {
|
||||||
|
files := []*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")
|
||||||
|
}
|
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
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 v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCRDs(t *testing.T) {
|
||||||
|
chrt := Chart{
|
||||||
|
Files: []*File{
|
||||||
|
{
|
||||||
|
Name: "crds/foo.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bar.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crds/foo/bar/baz.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crdsfoo/bar/baz.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crds/README.md",
|
||||||
|
Data: []byte("# hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
crds := chrt.CRDs()
|
||||||
|
is.Equal(2, len(crds))
|
||||||
|
is.Equal("crds/foo.yaml", crds[0].Name)
|
||||||
|
is.Equal("crds/foo/bar/baz.yaml", crds[1].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveChartNoRawData(t *testing.T) {
|
||||||
|
chrt := Chart{
|
||||||
|
Raw: []*File{
|
||||||
|
{
|
||||||
|
Name: "fhqwhgads.yaml",
|
||||||
|
Data: []byte("Everybody to the Limit"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
data, err := json.Marshal(chrt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &Chart{}
|
||||||
|
if err := json.Unmarshal(data, res); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
is.Equal([]*File(nil), res.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadata(t *testing.T) {
|
||||||
|
chrt := Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo.yaml",
|
||||||
|
AppVersion: "1.0.0",
|
||||||
|
APIVersion: "v3",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Type: "application",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
|
||||||
|
is.Equal("foo.yaml", chrt.Name())
|
||||||
|
is.Equal("1.0.0", chrt.AppVersion())
|
||||||
|
is.Equal(nil, chrt.Validate())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsRoot(t *testing.T) {
|
||||||
|
chrt1 := Chart{
|
||||||
|
parent: &Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chrt2 := Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
|
||||||
|
is.Equal(false, chrt1.IsRoot())
|
||||||
|
is.Equal(true, chrt2.IsRoot())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartPath(t *testing.T) {
|
||||||
|
chrt1 := Chart{
|
||||||
|
parent: &Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chrt2 := Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
|
||||||
|
is.Equal("foo.", chrt1.ChartPath())
|
||||||
|
is.Equal("foo", chrt2.ChartPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartFullPath(t *testing.T) {
|
||||||
|
chrt1 := Chart{
|
||||||
|
parent: &Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chrt2 := Chart{
|
||||||
|
Metadata: &Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
|
||||||
|
is.Equal("foo/charts/", chrt1.ChartFullPath())
|
||||||
|
is.Equal("foo", chrt2.ChartFullPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDObjects(t *testing.T) {
|
||||||
|
chrt := Chart{
|
||||||
|
Files: []*File{
|
||||||
|
{
|
||||||
|
Name: "crds/foo.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bar.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crds/foo/bar/baz.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crdsfoo/bar/baz.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crds/README.md",
|
||||||
|
Data: []byte("# hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []CRD{
|
||||||
|
{
|
||||||
|
Name: "crds/foo.yaml",
|
||||||
|
Filename: "crds/foo.yaml",
|
||||||
|
File: &File{
|
||||||
|
Name: "crds/foo.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "crds/foo/bar/baz.yaml",
|
||||||
|
Filename: "crds/foo/bar/baz.yaml",
|
||||||
|
File: &File{
|
||||||
|
Name: "crds/foo/bar/baz.yaml",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := assert.New(t)
|
||||||
|
crds := chrt.CRDObjects()
|
||||||
|
is.Equal(expected, crds)
|
||||||
|
}
|
@ -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 v3
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
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 v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateDependency(t *testing.T) {
|
||||||
|
dep := &Dependency{
|
||||||
|
Name: "example",
|
||||||
|
}
|
||||||
|
for value, shouldFail := range map[string]bool{
|
||||||
|
"abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
|
||||||
|
"-okay": false,
|
||||||
|
"_okay": false,
|
||||||
|
"- bad": true,
|
||||||
|
" bad": true,
|
||||||
|
"bad\nvalue": true,
|
||||||
|
"bad ": true,
|
||||||
|
"bad$": true,
|
||||||
|
} {
|
||||||
|
dep.Alias = value
|
||||||
|
res := dep.Validate()
|
||||||
|
if res != nil && !shouldFail {
|
||||||
|
t.Errorf("Failed on case %q", dep.Alias)
|
||||||
|
} else if res == nil && shouldFail {
|
||||||
|
t.Errorf("Expected failure for %q", dep.Alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
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 v3 provides chart handling for apiVersion v3 charts
|
||||||
|
|
||||||
|
This package and its sub-packages provide handling for apiVersion v3 charts.
|
||||||
|
*/
|
||||||
|
package v3
|
@ -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 v3
|
||||||
|
|
||||||
|
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...))
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
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 v3
|
||||||
|
|
||||||
|
// File represents a file as a name/value pair.
|
||||||
|
//
|
||||||
|
// By convention, name is a relative path within the scope of the chart's
|
||||||
|
// base directory.
|
||||||
|
type File struct {
|
||||||
|
// Name is the path-like name of the template.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Data is the template as byte data.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
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 v3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FuzzMetadataValidate(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
fdp := fuzz.NewConsumer(data)
|
||||||
|
// Add random values to the metadata
|
||||||
|
md := &Metadata{}
|
||||||
|
err := fdp.GenerateStruct(md)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
md.Validate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzDependencyValidate(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
f := fuzz.NewConsumer(data)
|
||||||
|
// Add random values to the dependenci
|
||||||
|
d := &Dependency{}
|
||||||
|
err := f.GenerateStruct(d)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
d.Validate()
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
|
||||||
|
// decompressed. This is the decompressed size of all the files.
|
||||||
|
// The default value is 100 MiB.
|
||||||
|
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB
|
||||||
|
|
||||||
|
// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load.
|
||||||
|
// The size of the file is the decompressed version of it when it is stored in an archive.
|
||||||
|
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
|
||||||
|
|
||||||
|
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
|
||||||
|
|
||||||
|
// 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 = ensureArchive(name, raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := LoadArchive(raw)
|
||||||
|
if err != nil {
|
||||||
|
if err == gzip.ErrHeader {
|
||||||
|
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
|
||||||
|
//
|
||||||
|
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
|
||||||
|
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
|
||||||
|
// if we didn't check for this.
|
||||||
|
func ensureArchive(name string, raw *os.File) error {
|
||||||
|
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
|
||||||
|
|
||||||
|
// Check the file format to give us a chance to provide the user with more actionable feedback.
|
||||||
|
buffer := make([]byte, 512)
|
||||||
|
_, err := raw.Read(buffer)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
|
||||||
|
// Fix for: https://github.com/helm/helm/issues/12261
|
||||||
|
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
|
||||||
|
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
|
||||||
|
// variety of content (Makefile, .zshrc) as valid YAML without errors.
|
||||||
|
|
||||||
|
// Wrong content type. Let's check if it's yaml and give an extra hint?
|
||||||
|
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
||||||
|
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGZipApplication checks whether the archive is of the application/x-gzip type.
|
||||||
|
func isGZipApplication(data []byte) bool {
|
||||||
|
sig := []byte("\x1F\x8B\x08")
|
||||||
|
return bytes.HasPrefix(data, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadArchiveFiles reads in files out of an archive into memory. This function
|
||||||
|
// performs important path security checks and should always be used before
|
||||||
|
// expanding a tarball
|
||||||
|
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
|
||||||
|
unzipped, err := gzip.NewReader(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer unzipped.Close()
|
||||||
|
|
||||||
|
files := []*BufferedFile{}
|
||||||
|
tr := tar.NewReader(unzipped)
|
||||||
|
remainingSize := MaxDecompressedChartSize
|
||||||
|
for {
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
hd, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.FileInfo().IsDir() {
|
||||||
|
// Use this instead of hd.Typeflag because we don't have to do any
|
||||||
|
// inference chasing.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch hd.Typeflag {
|
||||||
|
// We don't want to process these extension header files.
|
||||||
|
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive could contain \ if generated on Windows
|
||||||
|
delimiter := "/"
|
||||||
|
if strings.ContainsRune(hd.Name, '\\') {
|
||||||
|
delimiter = "\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(hd.Name, delimiter)
|
||||||
|
n := strings.Join(parts[1:], delimiter)
|
||||||
|
|
||||||
|
// Normalize the path to the / delimiter
|
||||||
|
n = strings.ReplaceAll(n, delimiter, "/")
|
||||||
|
|
||||||
|
if path.IsAbs(n) {
|
||||||
|
return nil, errors.New("chart illegally contains absolute paths")
|
||||||
|
}
|
||||||
|
|
||||||
|
n = path.Clean(n)
|
||||||
|
if n == "." {
|
||||||
|
// In this case, the original path was relative when it should have been absolute.
|
||||||
|
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(n, "..") {
|
||||||
|
return nil, errors.New("chart illegally references parent directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In some particularly arcane acts of path creativity, it is possible to intermix
|
||||||
|
// UNIX and Windows style paths in such a way that you produce a result of the form
|
||||||
|
// c:/foo even after all the built-in absolute path checks. So we explicitly check
|
||||||
|
// for this condition.
|
||||||
|
if drivePathPattern.MatchString(n) {
|
||||||
|
return nil, errors.New("chart contains illegally named files")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] == "Chart.yaml" {
|
||||||
|
return nil, errors.New("chart yaml not in base directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Size > remainingSize {
|
||||||
|
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Size > MaxDecompressedFileSize {
|
||||||
|
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
limitedReader := io.LimitReader(tr, remainingSize)
|
||||||
|
|
||||||
|
bytesWritten, err := io.Copy(b, limitedReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingSize -= bytesWritten
|
||||||
|
// When the bytesWritten are less than the file size it means the limit reader ended
|
||||||
|
// copying early. Here we report that error. This is important if the last file extracted
|
||||||
|
// is the one that goes over the limit. It assumes the Size stored in the tar header
|
||||||
|
// is correct, something many applications do.
|
||||||
|
if bytesWritten < hd.Size || remainingSize <= 0 {
|
||||||
|
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
|
||||||
|
|
||||||
|
files = append(files, &BufferedFile{Name: n, Data: data})
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, errors.New("no files in chart archive")
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadArchive loads from a reader containing a compressed tar archive.
|
||||||
|
func LoadArchive(in io.Reader) (*chart.Chart, error) {
|
||||||
|
files, err := LoadArchiveFiles(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadFiles(files)
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadArchiveFiles(t *testing.T) {
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
generate func(w *tar.Writer)
|
||||||
|
check func(t *testing.T, files []*BufferedFile, err error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty input should return no files",
|
||||||
|
generate: func(_ *tar.Writer) {},
|
||||||
|
check: func(t *testing.T, _ []*BufferedFile, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err.Error() != "no files in chart archive" {
|
||||||
|
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should ignore files with XGlobalHeader type",
|
||||||
|
generate: func(w *tar.Writer) {
|
||||||
|
// simulate the presence of a `pax_global_header` file like you would get when
|
||||||
|
// processing a GitHub release archive.
|
||||||
|
err := w.WriteHeader(&tar.Header{
|
||||||
|
Typeflag: tar.TypeXGlobalHeader,
|
||||||
|
Name: "pax_global_header",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to have at least one file, otherwise we'll get the "no files in chart archive" error
|
||||||
|
err = w.WriteHeader(&tar.Header{
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
Name: "dir/empty",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, files []*BufferedFile, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Fatalf(`expected to get one file but got [%v]`, files)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
gzw := gzip.NewWriter(buf)
|
||||||
|
tw := tar.NewWriter(gzw)
|
||||||
|
|
||||||
|
tc.generate(tw)
|
||||||
|
|
||||||
|
_ = tw.Close()
|
||||||
|
_ = gzw.Close()
|
||||||
|
|
||||||
|
files, err := LoadArchiveFiles(buf)
|
||||||
|
tc.check(t, files, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
"helm.sh/helm/v4/internal/sympath"
|
||||||
|
"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 := []*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() > MaxDecompressedFileSize {
|
||||||
|
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), 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, &BufferedFile{Name: n, Data: data})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err = sympath.Walk(topdir, walk); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadFiles(files)
|
||||||
|
}
|
@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedFile represents an archive file buffered for later processing.
|
||||||
|
type BufferedFile struct {
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFiles loads from in-memory files.
|
||||||
|
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
|
||||||
|
c := new(chart.Chart)
|
||||||
|
subcharts := make(map[string][]*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, &chart.File{Name: f.Name, 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)
|
||||||
|
}
|
||||||
|
// While the documentation says the APIVersion is required, in practice there
|
||||||
|
// are cases where that's not enforced. Since this package set is for v3 charts,
|
||||||
|
// when this function is used v3 is automatically added when not present.
|
||||||
|
if c.Metadata.APIVersion == "" {
|
||||||
|
c.Metadata.APIVersion = chart.APIVersionV3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
case strings.HasPrefix(f.Name, "templates/"):
|
||||||
|
c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data})
|
||||||
|
case strings.HasPrefix(f.Name, "charts/"):
|
||||||
|
if filepath.Ext(f.Name) == ".prov" {
|
||||||
|
c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fname := strings.TrimPrefix(f.Name, "charts/")
|
||||||
|
cname := strings.SplitN(fname, "/", 2)[0]
|
||||||
|
subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
|
||||||
|
default:
|
||||||
|
c.Files = append(c.Files, &chart.File{Name: f.Name, 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([]*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 a 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 err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("error reading yaml document: %w", err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(raw, ¤tMap); 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
|
||||||
|
}
|
@ -0,0 +1,711 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadDir(t *testing.T) {
|
||||||
|
l, err := Loader("testdata/frobnitz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyFrobnitz(t, c)
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
verifyDependenciesLock(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDirWithDevNull(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("test only works on unix systems with /dev/null present")
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := Loader("testdata/frobnitz_with_dev_null")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
if _, err := l.Load(); err == nil {
|
||||||
|
t.Errorf("packages with an irregular file (/dev/null) should not load")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDirWithSymlink(t *testing.T) {
|
||||||
|
sym := filepath.Join("..", "LICENSE")
|
||||||
|
link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE")
|
||||||
|
|
||||||
|
if err := os.Symlink(sym, link); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(link)
|
||||||
|
|
||||||
|
l, err := Loader("testdata/frobnitz_with_symlink")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyFrobnitz(t, c)
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
verifyDependenciesLock(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBomTestData(t *testing.T) {
|
||||||
|
testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
data, err := os.ReadFile("testdata/" + file)
|
||||||
|
if err != nil || !bytes.HasPrefix(data, utf8bom) {
|
||||||
|
t.Errorf("Test file has no BOM or is invalid: testdata/%s", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
|
||||||
|
}
|
||||||
|
unzipped, err := gzip.NewReader(bytes.NewReader(archive))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
|
||||||
|
}
|
||||||
|
defer unzipped.Close()
|
||||||
|
for _, testFile := range testFiles {
|
||||||
|
data := make([]byte, 3)
|
||||||
|
err := unzipped.Reset(bytes.NewReader(archive))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
|
||||||
|
}
|
||||||
|
tr := tar.NewReader(unzipped)
|
||||||
|
for {
|
||||||
|
file, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
|
||||||
|
}
|
||||||
|
if file != nil && strings.EqualFold(file.Name, testFile) {
|
||||||
|
_, err := tr.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, utf8bom) {
|
||||||
|
t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDirWithUTFBOM(t *testing.T) {
|
||||||
|
l, err := Loader("testdata/frobnitz_with_bom")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyFrobnitz(t, c)
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
verifyDependenciesLock(t, c)
|
||||||
|
verifyBomStripped(t, c.Files)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadArchiveWithUTFBOM(t *testing.T) {
|
||||||
|
l, err := Loader("testdata/frobnitz_with_bom.tgz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyFrobnitz(t, c)
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
verifyDependenciesLock(t, c)
|
||||||
|
verifyBomStripped(t, c.Files)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFile(t *testing.T) {
|
||||||
|
l, err := Loader("testdata/frobnitz-1.2.3.tgz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyFrobnitz(t, c)
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFiles(t *testing.T) {
|
||||||
|
goodFiles := []*BufferedFile{
|
||||||
|
{
|
||||||
|
Name: "Chart.yaml",
|
||||||
|
Data: []byte(`apiVersion: v3
|
||||||
|
name: frobnitz
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "values.yaml",
|
||||||
|
Data: []byte("var: some values"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "values.schema.json",
|
||||||
|
Data: []byte("type: Values"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "templates/deployment.yaml",
|
||||||
|
Data: []byte("some deployment"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "templates/service.yaml",
|
||||||
|
Data: []byte("some service"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := LoadFiles(goodFiles)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected good files to be loaded, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Name() != "frobnitz" {
|
||||||
|
t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Values["var"] != "some values" {
|
||||||
|
t.Error("Expected chart values to be populated with default values")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Raw) != 5 {
|
||||||
|
t.Errorf("Expected %d files, got %d", 5, len(c.Raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(c.Schema, []byte("type: Values")) {
|
||||||
|
t.Error("Expected chart schema to be populated with default values")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Templates) != 2 {
|
||||||
|
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = LoadFiles([]*BufferedFile{}); err == nil {
|
||||||
|
t.Fatal("Expected err to be non-nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "Chart.yaml file is missing" {
|
||||||
|
t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the order of file loading. The Chart.yaml file needs to come first for
|
||||||
|
// later comparison checks. See https://github.com/helm/helm/pull/8948
|
||||||
|
func TestLoadFilesOrder(t *testing.T) {
|
||||||
|
goodFiles := []*BufferedFile{
|
||||||
|
{
|
||||||
|
Name: "requirements.yaml",
|
||||||
|
Data: []byte("dependencies:"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "values.yaml",
|
||||||
|
Data: []byte("var: some values"),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "templates/deployment.yaml",
|
||||||
|
Data: []byte("some deployment"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "templates/service.yaml",
|
||||||
|
Data: []byte("some service"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Chart.yaml",
|
||||||
|
Data: []byte(`apiVersion: v3
|
||||||
|
name: frobnitz
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stderr to make sure message about Chart.yaml handle dependencies
|
||||||
|
// is not present
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create pipe: %s", err)
|
||||||
|
}
|
||||||
|
stderr := log.Writer()
|
||||||
|
log.SetOutput(w)
|
||||||
|
defer func() {
|
||||||
|
log.SetOutput(stderr)
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = LoadFiles(goodFiles)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected good files to be loaded, got %v", err)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
var text bytes.Buffer
|
||||||
|
io.Copy(&text, r)
|
||||||
|
if text.String() != "" {
|
||||||
|
t.Errorf("Expected no message to Stderr, got %s", text.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packaging the chart on a Windows machine will produce an
|
||||||
|
// archive that has \\ as delimiters. Test that we support these archives
|
||||||
|
func TestLoadFileBackslash(t *testing.T) {
|
||||||
|
c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyChartFileAndTemplate(t, c, "frobnitz_backslash")
|
||||||
|
verifyChart(t, c)
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadV3WithReqs(t *testing.T) {
|
||||||
|
l, err := Loader("testdata/frobnitz.v3.reqs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
c, err := l.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load testdata: %s", err)
|
||||||
|
}
|
||||||
|
verifyDependencies(t, c)
|
||||||
|
verifyDependenciesLock(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadInvalidArchive(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
writeTar := func(filename, internalPath string, body []byte) {
|
||||||
|
dest, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zipper := gzip.NewWriter(dest)
|
||||||
|
tw := tar.NewWriter(zipper)
|
||||||
|
|
||||||
|
h := &tar.Header{
|
||||||
|
Name: internalPath,
|
||||||
|
Mode: 0755,
|
||||||
|
Size: int64(len(body)),
|
||||||
|
ModTime: time.Now(),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(h); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tw.Close()
|
||||||
|
zipper.Close()
|
||||||
|
dest.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
chartname string
|
||||||
|
internal string
|
||||||
|
expectError string
|
||||||
|
}{
|
||||||
|
{"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"},
|
||||||
|
{"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"},
|
||||||
|
{"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"},
|
||||||
|
{"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"},
|
||||||
|
{"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"},
|
||||||
|
{"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"},
|
||||||
|
{"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"},
|
||||||
|
{"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"},
|
||||||
|
{"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"},
|
||||||
|
{"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"},
|
||||||
|
{"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"},
|
||||||
|
{"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"},
|
||||||
|
|
||||||
|
// Under special circumstances, this can get normalized to things that look like absolute Windows paths
|
||||||
|
{"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"},
|
||||||
|
{"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"},
|
||||||
|
{"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"},
|
||||||
|
} {
|
||||||
|
illegalChart := filepath.Join(tmpdir, tt.chartname)
|
||||||
|
writeTar(illegalChart, tt.internal, []byte("hello: world"))
|
||||||
|
_, err := Load(illegalChart)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when unpacking illegal files")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.expectError) {
|
||||||
|
t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that absolute path gets interpreted as relative
|
||||||
|
illegalChart := filepath.Join(tmpdir, "abs-path.tgz")
|
||||||
|
writeTar(illegalChart, "/Chart.yaml", []byte("hello: world"))
|
||||||
|
_, err := Load(illegalChart)
|
||||||
|
if err.Error() != "validation: chart.metadata.name is required" {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And just to validate that the above was not spurious
|
||||||
|
illegalChart = filepath.Join(tmpdir, "abs-path2.tgz")
|
||||||
|
writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world"))
|
||||||
|
_, err = Load(illegalChart)
|
||||||
|
if err.Error() != "Chart.yaml file is missing" {
|
||||||
|
t.Errorf("Unexpected error message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, test that drive letter gets stripped off on Windows
|
||||||
|
illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz")
|
||||||
|
writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world"))
|
||||||
|
_, err = Load(illegalChart)
|
||||||
|
if err.Error() != "validation: chart.metadata.name is required" {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadValues(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
data []byte
|
||||||
|
expctedValues map[string]interface{}
|
||||||
|
}{
|
||||||
|
"It should load values correctly": {
|
||||||
|
data: []byte(`
|
||||||
|
foo:
|
||||||
|
image: foo:v1
|
||||||
|
bar:
|
||||||
|
version: v2
|
||||||
|
`),
|
||||||
|
expctedValues: map[string]interface{}{
|
||||||
|
"foo": map[string]interface{}{
|
||||||
|
"image": "foo:v1",
|
||||||
|
},
|
||||||
|
"bar": map[string]interface{}{
|
||||||
|
"version": "v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"It should load values correctly with multiple documents in one file": {
|
||||||
|
data: []byte(`
|
||||||
|
foo:
|
||||||
|
image: foo:v1
|
||||||
|
bar:
|
||||||
|
version: v2
|
||||||
|
---
|
||||||
|
foo:
|
||||||
|
image: foo:v2
|
||||||
|
`),
|
||||||
|
expctedValues: map[string]interface{}{
|
||||||
|
"foo": map[string]interface{}{
|
||||||
|
"image": "foo:v2",
|
||||||
|
},
|
||||||
|
"bar": map[string]interface{}{
|
||||||
|
"version": "v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for testName, testCase := range testCases {
|
||||||
|
t.Run(testName, func(tt *testing.T) {
|
||||||
|
values, err := LoadValues(bytes.NewReader(testCase.data))
|
||||||
|
if err != nil {
|
||||||
|
tt.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(values, testCase.expctedValues) {
|
||||||
|
tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeValues(t *testing.T) {
|
||||||
|
nestedMap := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": map[string]string{
|
||||||
|
"cool": "stuff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
anotherNestedMap := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": map[string]string{
|
||||||
|
"cool": "things",
|
||||||
|
"awesome": "stuff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flatMap := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "stuff",
|
||||||
|
}
|
||||||
|
anotherFlatMap := map[string]interface{}{
|
||||||
|
"testing": "fun",
|
||||||
|
}
|
||||||
|
|
||||||
|
testMap := MergeMaps(flatMap, nestedMap)
|
||||||
|
equal := reflect.DeepEqual(testMap, nestedMap)
|
||||||
|
if !equal {
|
||||||
|
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
testMap = MergeMaps(nestedMap, flatMap)
|
||||||
|
equal = reflect.DeepEqual(testMap, flatMap)
|
||||||
|
if !equal {
|
||||||
|
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
testMap = MergeMaps(nestedMap, anotherNestedMap)
|
||||||
|
equal = reflect.DeepEqual(testMap, anotherNestedMap)
|
||||||
|
if !equal {
|
||||||
|
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
|
||||||
|
expectedMap := map[string]interface{}{
|
||||||
|
"testing": "fun",
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": map[string]string{
|
||||||
|
"cool": "things",
|
||||||
|
"awesome": "stuff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
equal = reflect.DeepEqual(testMap, expectedMap)
|
||||||
|
if !equal {
|
||||||
|
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyChart(t *testing.T, c *chart.Chart) {
|
||||||
|
t.Helper()
|
||||||
|
if c.Name() == "" {
|
||||||
|
t.Fatalf("No chart metadata found on %v", c)
|
||||||
|
}
|
||||||
|
t.Logf("Verifying chart %s", c.Name())
|
||||||
|
if len(c.Templates) != 1 {
|
||||||
|
t.Errorf("Expected 1 template, got %d", len(c.Templates))
|
||||||
|
}
|
||||||
|
|
||||||
|
numfiles := 6
|
||||||
|
if len(c.Files) != numfiles {
|
||||||
|
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
|
||||||
|
for _, n := range c.Files {
|
||||||
|
t.Logf("\t%s", n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Dependencies()) != 2 {
|
||||||
|
t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies())
|
||||||
|
for _, d := range c.Dependencies() {
|
||||||
|
t.Logf("\tSubchart: %s\n", d.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect := map[string]map[string]string{
|
||||||
|
"alpine": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
},
|
||||||
|
"mariner": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range c.Dependencies() {
|
||||||
|
if dep.Metadata == nil {
|
||||||
|
t.Fatalf("expected metadata on dependency: %v", dep)
|
||||||
|
}
|
||||||
|
exp, ok := expect[dep.Name()]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Unknown dependency %s", dep.Name())
|
||||||
|
}
|
||||||
|
if exp["version"] != dep.Metadata.Version {
|
||||||
|
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyDependencies(t *testing.T, c *chart.Chart) {
|
||||||
|
t.Helper()
|
||||||
|
if len(c.Metadata.Dependencies) != 2 {
|
||||||
|
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
|
||||||
|
}
|
||||||
|
tests := []*chart.Dependency{
|
||||||
|
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
|
||||||
|
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
d := c.Metadata.Dependencies[i]
|
||||||
|
if d.Name != tt.Name {
|
||||||
|
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
|
||||||
|
}
|
||||||
|
if d.Version != tt.Version {
|
||||||
|
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
|
||||||
|
}
|
||||||
|
if d.Repository != tt.Repository {
|
||||||
|
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
|
||||||
|
t.Helper()
|
||||||
|
if len(c.Metadata.Dependencies) != 2 {
|
||||||
|
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
|
||||||
|
}
|
||||||
|
tests := []*chart.Dependency{
|
||||||
|
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
|
||||||
|
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
d := c.Metadata.Dependencies[i]
|
||||||
|
if d.Name != tt.Name {
|
||||||
|
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
|
||||||
|
}
|
||||||
|
if d.Version != tt.Version {
|
||||||
|
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
|
||||||
|
}
|
||||||
|
if d.Repository != tt.Repository {
|
||||||
|
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyFrobnitz(t *testing.T, c *chart.Chart) {
|
||||||
|
t.Helper()
|
||||||
|
verifyChartFileAndTemplate(t, c, "frobnitz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
|
||||||
|
t.Helper()
|
||||||
|
if c.Metadata == nil {
|
||||||
|
t.Fatal("Metadata is nil")
|
||||||
|
}
|
||||||
|
if c.Name() != name {
|
||||||
|
t.Errorf("Expected %s, got %s", name, c.Name())
|
||||||
|
}
|
||||||
|
if len(c.Templates) != 1 {
|
||||||
|
t.Fatalf("Expected 1 template, got %d", len(c.Templates))
|
||||||
|
}
|
||||||
|
if c.Templates[0].Name != "templates/template.tpl" {
|
||||||
|
t.Errorf("Unexpected template: %s", c.Templates[0].Name)
|
||||||
|
}
|
||||||
|
if len(c.Templates[0].Data) == 0 {
|
||||||
|
t.Error("No template data.")
|
||||||
|
}
|
||||||
|
if len(c.Files) != 6 {
|
||||||
|
t.Fatalf("Expected 6 Files, got %d", len(c.Files))
|
||||||
|
}
|
||||||
|
if len(c.Dependencies()) != 2 {
|
||||||
|
t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies()))
|
||||||
|
}
|
||||||
|
if len(c.Metadata.Dependencies) != 2 {
|
||||||
|
t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
|
||||||
|
}
|
||||||
|
if len(c.Lock.Dependencies) != 2 {
|
||||||
|
t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range c.Dependencies() {
|
||||||
|
switch dep.Name() {
|
||||||
|
case "mariner":
|
||||||
|
case "alpine":
|
||||||
|
if len(dep.Templates) != 1 {
|
||||||
|
t.Fatalf("Expected 1 template, got %d", len(dep.Templates))
|
||||||
|
}
|
||||||
|
if dep.Templates[0].Name != "templates/alpine-pod.yaml" {
|
||||||
|
t.Errorf("Unexpected template: %s", dep.Templates[0].Name)
|
||||||
|
}
|
||||||
|
if len(dep.Templates[0].Data) == 0 {
|
||||||
|
t.Error("No template data.")
|
||||||
|
}
|
||||||
|
if len(dep.Files) != 1 {
|
||||||
|
t.Fatalf("Expected 1 Files, got %d", len(dep.Files))
|
||||||
|
}
|
||||||
|
if len(dep.Dependencies()) != 2 {
|
||||||
|
t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies()))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("Unexpected dependency %s", dep.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyBomStripped(t *testing.T, files []*chart.File) {
|
||||||
|
t.Helper()
|
||||||
|
for _, file := range files {
|
||||||
|
if bytes.HasPrefix(file.Data, utf8bom) {
|
||||||
|
t.Errorf("Byte Order Mark still present in processed file %s", file.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
LICENSE placeholder.
|
@ -0,0 +1,4 @@
|
|||||||
|
name: albatross
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
home: ""
|
@ -0,0 +1,4 @@
|
|||||||
|
albatross: "true"
|
||||||
|
|
||||||
|
global:
|
||||||
|
author: Coleridge
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
ignore/
|
@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: frobnitz
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
annotations:
|
||||||
|
extrakey: extravalue
|
||||||
|
anotherkey: anothervalue
|
||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
@ -0,0 +1 @@
|
|||||||
|
This is an install document. The client may display this.
|
@ -0,0 +1 @@
|
|||||||
|
LICENSE placeholder.
|
@ -0,0 +1,11 @@
|
|||||||
|
# Frobnitz
|
||||||
|
|
||||||
|
This is an example chart.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This is an example. It has no usage.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For developer info, see the top-level repository.
|
@ -0,0 +1 @@
|
|||||||
|
This should be ignored by the loader, but may be included in a chart.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: alpine
|
||||||
|
description: Deploy a basic Alpine Linux pod
|
||||||
|
version: 0.1.0
|
||||||
|
home: https://helm.sh/helm
|
@ -0,0 +1,9 @@
|
|||||||
|
This example was generated using the command `helm create alpine`.
|
||||||
|
|
||||||
|
The `templates/` directory contains a very simple pod resource with a
|
||||||
|
couple of parameters.
|
||||||
|
|
||||||
|
The `values.toml` file contains the default values for the
|
||||||
|
`alpine-pod.yaml` template.
|
||||||
|
|
||||||
|
You can install this example using `helm install ./alpine`.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: mast1
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
home: ""
|
@ -0,0 +1,4 @@
|
|||||||
|
# Default values for mast1.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare name/value pairs to be passed into your templates.
|
||||||
|
# name = "value"
|
Binary file not shown.
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: {{.Release.Name}}-{{.Chart.Name}}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: {{.Release.Service}}
|
||||||
|
app.kubernetes.io/name: {{.Chart.Name}}
|
||||||
|
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
|
||||||
|
spec:
|
||||||
|
restartPolicy: {{default "Never" .restart_policy}}
|
||||||
|
containers:
|
||||||
|
- name: waiter
|
||||||
|
image: "alpine:3.9"
|
||||||
|
command: ["/bin/sleep","9000"]
|
@ -0,0 +1,2 @@
|
|||||||
|
# The pod name
|
||||||
|
name: "my-alpine"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
This is a placeholder for documentation.
|
After Width: | Height: | Size: 374 B |
@ -0,0 +1 @@
|
|||||||
|
Hello {{.Name | default "world"}}
|
@ -0,0 +1,6 @@
|
|||||||
|
# A values file contains configuration.
|
||||||
|
|
||||||
|
name: "Some Name"
|
||||||
|
|
||||||
|
section:
|
||||||
|
name: "Name in a section"
|
@ -0,0 +1 @@
|
|||||||
|
ignore/
|
@ -0,0 +1,8 @@
|
|||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
digest: invalid
|
@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: frobnitz
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
annotations:
|
||||||
|
extrakey: extravalue
|
||||||
|
anotherkey: anothervalue
|
||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
@ -0,0 +1 @@
|
|||||||
|
This is an install document. The client may display this.
|
@ -0,0 +1 @@
|
|||||||
|
LICENSE placeholder.
|
@ -0,0 +1,11 @@
|
|||||||
|
# Frobnitz
|
||||||
|
|
||||||
|
This is an example chart.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This is an example. It has no usage.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For developer info, see the top-level repository.
|
@ -0,0 +1 @@
|
|||||||
|
This should be ignored by the loader, but may be included in a chart.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: alpine
|
||||||
|
description: Deploy a basic Alpine Linux pod
|
||||||
|
version: 0.1.0
|
||||||
|
home: https://helm.sh/helm
|
@ -0,0 +1,9 @@
|
|||||||
|
This example was generated using the command `helm create alpine`.
|
||||||
|
|
||||||
|
The `templates/` directory contains a very simple pod resource with a
|
||||||
|
couple of parameters.
|
||||||
|
|
||||||
|
The `values.toml` file contains the default values for the
|
||||||
|
`alpine-pod.yaml` template.
|
||||||
|
|
||||||
|
You can install this example using `helm install ./alpine`.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: mast1
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
home: ""
|
@ -0,0 +1,4 @@
|
|||||||
|
# Default values for mast1.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare name/value pairs to be passed into your templates.
|
||||||
|
# name = "value"
|
Binary file not shown.
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: {{.Release.Name}}-{{.Chart.Name}}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: {{.Release.Service}}
|
||||||
|
app.kubernetes.io/name: {{.Chart.Name}}
|
||||||
|
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
|
||||||
|
spec:
|
||||||
|
restartPolicy: {{default "Never" .restart_policy}}
|
||||||
|
containers:
|
||||||
|
- name: waiter
|
||||||
|
image: "alpine:3.9"
|
||||||
|
command: ["/bin/sleep","9000"]
|
@ -0,0 +1,2 @@
|
|||||||
|
# The pod name
|
||||||
|
name: "my-alpine"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
This is a placeholder for documentation.
|
After Width: | Height: | Size: 374 B |
@ -0,0 +1 @@
|
|||||||
|
Hello {{.Name | default "world"}}
|
@ -0,0 +1,6 @@
|
|||||||
|
# A values file contains configuration.
|
||||||
|
|
||||||
|
name: "Some Name"
|
||||||
|
|
||||||
|
section:
|
||||||
|
name: "Name in a section"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
ignore/
|
@ -0,0 +1,8 @@
|
|||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
digest: invalid
|
@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: frobnitz_backslash
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
annotations:
|
||||||
|
extrakey: extravalue
|
||||||
|
anotherkey: anothervalue
|
||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
@ -0,0 +1 @@
|
|||||||
|
This is an install document. The client may display this.
|
@ -0,0 +1 @@
|
|||||||
|
LICENSE placeholder.
|
@ -0,0 +1,11 @@
|
|||||||
|
# Frobnitz
|
||||||
|
|
||||||
|
This is an example chart.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This is an example. It has no usage.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For developer info, see the top-level repository.
|
@ -0,0 +1 @@
|
|||||||
|
This should be ignored by the loader, but may be included in a chart.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: alpine
|
||||||
|
description: Deploy a basic Alpine Linux pod
|
||||||
|
version: 0.1.0
|
||||||
|
home: https://helm.sh/helm
|
@ -0,0 +1,9 @@
|
|||||||
|
This example was generated using the command `helm create alpine`.
|
||||||
|
|
||||||
|
The `templates/` directory contains a very simple pod resource with a
|
||||||
|
couple of parameters.
|
||||||
|
|
||||||
|
The `values.toml` file contains the default values for the
|
||||||
|
`alpine-pod.yaml` template.
|
||||||
|
|
||||||
|
You can install this example using `helm install ./alpine`.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: mast1
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
home: ""
|
@ -0,0 +1,4 @@
|
|||||||
|
# Default values for mast1.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare name/value pairs to be passed into your templates.
|
||||||
|
# name = "value"
|
Binary file not shown.
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: {{.Release.Name}}-{{.Chart.Name}}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: {{.Release.Service | quote }}
|
||||||
|
app.kubernetes.io/name: {{.Chart.Name}}
|
||||||
|
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
|
||||||
|
spec:
|
||||||
|
restartPolicy: {{default "Never" .restart_policy}}
|
||||||
|
containers:
|
||||||
|
- name: waiter
|
||||||
|
image: "alpine:3.9"
|
||||||
|
command: ["/bin/sleep","9000"]
|
@ -0,0 +1,2 @@
|
|||||||
|
# The pod name
|
||||||
|
name: "my-alpine"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
This is a placeholder for documentation.
|
After Width: | Height: | Size: 374 B |
@ -0,0 +1 @@
|
|||||||
|
Hello {{.Name | default "world"}}
|
@ -0,0 +1,6 @@
|
|||||||
|
# A values file contains configuration.
|
||||||
|
|
||||||
|
name: "Some Name"
|
||||||
|
|
||||||
|
section:
|
||||||
|
name: "Name in a section"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
ignore/
|
@ -0,0 +1,8 @@
|
|||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
digest: invalid
|
@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: frobnitz
|
||||||
|
description: This is a frobnitz.
|
||||||
|
version: "1.2.3"
|
||||||
|
keywords:
|
||||||
|
- frobnitz
|
||||||
|
- sprocket
|
||||||
|
- dodad
|
||||||
|
maintainers:
|
||||||
|
- name: The Helm Team
|
||||||
|
email: helm@example.com
|
||||||
|
- name: Someone Else
|
||||||
|
email: nobody@example.com
|
||||||
|
sources:
|
||||||
|
- https://example.com/foo/bar
|
||||||
|
home: http://example.com
|
||||||
|
icon: https://example.com/64x64.png
|
||||||
|
annotations:
|
||||||
|
extrakey: extravalue
|
||||||
|
anotherkey: anothervalue
|
||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
@ -0,0 +1 @@
|
|||||||
|
This is an install document. The client may display this.
|
@ -0,0 +1 @@
|
|||||||
|
LICENSE placeholder.
|
@ -0,0 +1,11 @@
|
|||||||
|
# Frobnitz
|
||||||
|
|
||||||
|
This is an example chart.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This is an example. It has no usage.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For developer info, see the top-level repository.
|
@ -0,0 +1 @@
|
|||||||
|
This should be ignored by the loader, but may be included in a chart.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: alpine
|
||||||
|
description: Deploy a basic Alpine Linux pod
|
||||||
|
version: 0.1.0
|
||||||
|
home: https://helm.sh/helm
|
@ -0,0 +1,9 @@
|
|||||||
|
This example was generated using the command `helm create alpine`.
|
||||||
|
|
||||||
|
The `templates/` directory contains a very simple pod resource with a
|
||||||
|
couple of parameters.
|
||||||
|
|
||||||
|
The `values.toml` file contains the default values for the
|
||||||
|
`alpine-pod.yaml` template.
|
||||||
|
|
||||||
|
You can install this example using `helm install ./alpine`.
|
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v3
|
||||||
|
name: mast1
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
version: 0.1.0
|
||||||
|
home: ""
|
@ -0,0 +1,4 @@
|
|||||||
|
# Default values for mast1.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare name/value pairs to be passed into your templates.
|
||||||
|
# name = "value"
|
Binary file not shown.
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: {{.Release.Name}}-{{.Chart.Name}}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: {{.Release.Service}}
|
||||||
|
app.kubernetes.io/name: {{.Chart.Name}}
|
||||||
|
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
|
||||||
|
spec:
|
||||||
|
restartPolicy: {{default "Never" .restart_policy}}
|
||||||
|
containers:
|
||||||
|
- name: waiter
|
||||||
|
image: "alpine:3.9"
|
||||||
|
command: ["/bin/sleep","9000"]
|
@ -0,0 +1,2 @@
|
|||||||
|
# The pod name
|
||||||
|
name: "my-alpine"
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
This is a placeholder for documentation.
|
After Width: | Height: | Size: 374 B |
@ -0,0 +1 @@
|
|||||||
|
Hello {{.Name | default "world"}}
|
@ -0,0 +1,6 @@
|
|||||||
|
# A values file contains configuration.
|
||||||
|
|
||||||
|
name: "Some Name"
|
||||||
|
|
||||||
|
section:
|
||||||
|
name: "Name in a section"
|
@ -0,0 +1 @@
|
|||||||
|
ignore/
|
@ -0,0 +1,8 @@
|
|||||||
|
dependencies:
|
||||||
|
- name: alpine
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
- name: mariner
|
||||||
|
version: "4.3.2"
|
||||||
|
repository: https://example.com/charts
|
||||||
|
digest: invalid
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue