mirror of https://github.com/helm/helm
Signed-off-by: Alik Khilazhev <7482065+alikhil@users.noreply.github.com>pull/30855/head
commit
40797c61fa
@ -0,0 +1,69 @@
|
||||
name: Bug Report
|
||||
description: Report a bug encountered in Helm
|
||||
labels: kind/bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: |
|
||||
Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: How can we reproduce it (as minimally and precisely as possible)?
|
||||
description: |
|
||||
Please list steps someone can follow to trigger the issue.
|
||||
|
||||
For example:
|
||||
1. Run `helm install mychart ./path-to-chart -f values.yaml --debug`
|
||||
2. Observe the following error: ...
|
||||
|
||||
You can include:
|
||||
- a sample `values.yaml` block
|
||||
- a link to a chart
|
||||
- specific `helm` commands used
|
||||
|
||||
This helps others reproduce and debug your issue more effectively.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: helmVersion
|
||||
attributes:
|
||||
label: Helm version
|
||||
value: |
|
||||
<details>
|
||||
```console
|
||||
$ helm version
|
||||
# paste output here
|
||||
```
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: kubeVersion
|
||||
attributes:
|
||||
label: Kubernetes version
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ kubectl version
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
@ -0,0 +1,27 @@
|
||||
name: Documentation
|
||||
description: Report any mistakes or missing information from the documentation or the examples
|
||||
labels: kind/documentation
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
⚠️ **Note**: Most documentation lives in [helm/helm-www](https://github.com/helm/helm-www).
|
||||
If your issue is about Helm website documentation or examples, please [open an issue there](https://github.com/helm/helm-www/issues/new/choose).
|
||||
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What would you like to be added?
|
||||
description: |
|
||||
Link to the issue (please include a link to the specific documentation or example).
|
||||
Link to the issue raised in [Helm Documentation Improvement Proposal](https://github.com/helm/helm-www)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: rationale
|
||||
attributes:
|
||||
label: Why is this needed?
|
||||
validations:
|
||||
required: true
|
||||
|
@ -0,0 +1,21 @@
|
||||
name: Enhancement/feature
|
||||
description: Provide supporting details for a feature in development
|
||||
labels: kind/feature
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What would you like to be added?
|
||||
description: |
|
||||
Feature requests are unlikely to make progress as issues.
|
||||
Initial discussion and ideas can happen on an issue.
|
||||
But significant changes or features must be proposed as a [Helm Improvement Proposal](https://github.com/helm/community/blob/main/hips/hip-0001.md) (HIP)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: rationale
|
||||
attributes:
|
||||
label: Why is this needed?
|
||||
validations:
|
||||
required: true
|
@ -1,9 +0,0 @@
|
||||
<!-- If you need help or think you have found a bug, please help us with your issue by entering the following information (otherwise you can delete this text): -->
|
||||
|
||||
Output of `helm version`:
|
||||
|
||||
Output of `kubectl version`:
|
||||
|
||||
Cloud Provider/Platform (AKS, GKE, Minikube etc.):
|
||||
|
||||
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
// 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 []*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"`
|
||||
// Files are miscellaneous files in a chart archive,
|
||||
// e.g. README, LICENSE, etc.
|
||||
Files []*common.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 *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")
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
func TestCRDs(t *testing.T) {
|
||||
chrt := Chart{
|
||||
Files: []*common.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: []*common.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([]*common.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: []*common.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: &common.File{
|
||||
Name: "crds/foo.yaml",
|
||||
Data: []byte("hello"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "crds/foo/bar/baz.yaml",
|
||||
Filename: "crds/foo/bar/baz.yaml",
|
||||
File: &common.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,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,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,66 @@
|
||||
/*
|
||||
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 lint // import "helm.sh/helm/v4/internal/chart/v3/lint"
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
type linterOptions struct {
|
||||
KubeVersion *common.KubeVersion
|
||||
SkipSchemaValidation bool
|
||||
}
|
||||
|
||||
type LinterOption func(lo *linterOptions)
|
||||
|
||||
func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption {
|
||||
return func(lo *linterOptions) {
|
||||
lo.KubeVersion = kubeVersion
|
||||
}
|
||||
}
|
||||
|
||||
func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
|
||||
return func(lo *linterOptions) {
|
||||
lo.SkipSchemaValidation = skipSchemaValidation
|
||||
}
|
||||
}
|
||||
|
||||
func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter {
|
||||
|
||||
chartDir, _ := filepath.Abs(baseDir)
|
||||
|
||||
lo := linterOptions{}
|
||||
for _, option := range options {
|
||||
option(&lo)
|
||||
}
|
||||
|
||||
result := support.Linter{
|
||||
ChartDir: chartDir,
|
||||
}
|
||||
|
||||
rules.Chartfile(&result)
|
||||
rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation)
|
||||
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
|
||||
rules.Dependencies(&result)
|
||||
rules.Crds(&result)
|
||||
|
||||
return result
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
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 lint
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
var values map[string]interface{}
|
||||
|
||||
const namespace = "testNamespace"
|
||||
|
||||
const badChartDir = "rules/testdata/badchartfile"
|
||||
const badValuesFileDir = "rules/testdata/badvaluesfile"
|
||||
const badYamlFileDir = "rules/testdata/albatross"
|
||||
const badCrdFileDir = "rules/testdata/badcrdfile"
|
||||
const goodChartDir = "rules/testdata/goodone"
|
||||
const subChartValuesDir = "rules/testdata/withsubchart"
|
||||
const malformedTemplate = "rules/testdata/malformed-template"
|
||||
const invalidChartFileDir = "rules/testdata/invalidchartfile"
|
||||
|
||||
func TestBadChartV3(t *testing.T) {
|
||||
m := RunAll(badChartDir, values, namespace).Messages
|
||||
if len(m) != 8 {
|
||||
t.Errorf("Number of errors %v", len(m))
|
||||
t.Errorf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
// There should be one INFO, one WARNING, and 2 ERROR messages, check for them
|
||||
var i, w, e, e2, e3, e4, e5, e6 bool
|
||||
for _, msg := range m {
|
||||
if msg.Severity == support.InfoSev {
|
||||
if strings.Contains(msg.Err.Error(), "icon is recommended") {
|
||||
i = true
|
||||
}
|
||||
}
|
||||
if msg.Severity == support.WarningSev {
|
||||
if strings.Contains(msg.Err.Error(), "does not exist") {
|
||||
w = true
|
||||
}
|
||||
}
|
||||
if msg.Severity == support.ErrorSev {
|
||||
if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") {
|
||||
e = true
|
||||
}
|
||||
if strings.Contains(msg.Err.Error(), "name is required") {
|
||||
e2 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") {
|
||||
e3 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") {
|
||||
e4 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") {
|
||||
e5 = true
|
||||
}
|
||||
// This comes from the dependency check, which loads dependency info from the Chart.yaml
|
||||
if strings.Contains(msg.Err.Error(), "unable to load chart") {
|
||||
e6 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w {
|
||||
t.Errorf("Didn't find all the expected errors, got %#v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidYaml(t *testing.T) {
|
||||
m := RunAll(badYamlFileDir, values, namespace).Messages
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
|
||||
t.Errorf("All didn't have the error for deliberateSyntaxError")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidChartYamlV3(t *testing.T) {
|
||||
m := RunAll(invalidChartFileDir, values, namespace).Messages
|
||||
t.Log(m)
|
||||
if len(m) != 3 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") {
|
||||
t.Errorf("All didn't have the error for duplicate YAML keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadValuesV3(t *testing.T) {
|
||||
m := RunAll(badValuesFileDir, values, namespace).Messages
|
||||
if len(m) < 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") {
|
||||
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadCrdFileV3(t *testing.T) {
|
||||
m := RunAll(badCrdFileDir, values, namespace).Messages
|
||||
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
|
||||
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
|
||||
assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'")
|
||||
}
|
||||
|
||||
func TestGoodChart(t *testing.T) {
|
||||
m := RunAll(goodChartDir, values, namespace).Messages
|
||||
if len(m) != 0 {
|
||||
t.Error("All returned linter messages when it shouldn't have")
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7923
|
||||
func TestHelmCreateChart(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
// Fatal is bad because of the defer.
|
||||
return
|
||||
}
|
||||
|
||||
// Note: we test with strict=true here, even though others have
|
||||
// strict = false.
|
||||
m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages
|
||||
if ll := len(m); ll != 1 {
|
||||
t.Errorf("All should have had exactly 1 error. Got %d", ll)
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg.Error())
|
||||
}
|
||||
} else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") {
|
||||
t.Errorf("Unexpected lint error: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws
|
||||
// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags).
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/11495
|
||||
//
|
||||
// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
|
||||
// of the `--set` flag.
|
||||
//
|
||||
// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive
|
||||
// results.
|
||||
// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=<k8s-major-version>
|
||||
// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=<k8s-minor-version>
|
||||
// or directly use '$(LDFLAGS)' in Makefile.
|
||||
//
|
||||
// When run without ldflags, the test passes giving a false-positive result. This is because the variables
|
||||
// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there
|
||||
// might not be the deprecation warning.
|
||||
func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
|
||||
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add values to enable hpa, and ingress which are disabled by default.
|
||||
// This is the equivalent of:
|
||||
// helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true'
|
||||
updatedValues := map[string]interface{}{
|
||||
"autoscaling": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
"ingress": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
}
|
||||
|
||||
linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true))
|
||||
for _, msg := range linterRunDetails.Messages {
|
||||
if strings.HasPrefix(msg.Error(), "[WARNING]") &&
|
||||
strings.Contains(msg.Error(), "deprecated") {
|
||||
// When there is a deprecation warning for an object created
|
||||
// by `helm create` for the current Kubernetes version, fail.
|
||||
t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lint ignores import-values
|
||||
// See https://github.com/helm/helm/issues/9658
|
||||
func TestSubChartValuesChart(t *testing.T) {
|
||||
m := RunAll(subChartValuesDir, values, namespace).Messages
|
||||
if len(m) != 0 {
|
||||
t.Error("All returned linter messages when it shouldn't have")
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lint stuck with malformed template object
|
||||
// See https://github.com/helm/helm/issues/11391
|
||||
func TestMalformedTemplate(t *testing.T) {
|
||||
c := time.After(3 * time.Second)
|
||||
ch := make(chan int, 1)
|
||||
var m []support.Message
|
||||
go func() {
|
||||
m = RunAll(malformedTemplate, values, namespace).Messages
|
||||
ch <- 1
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
t.Fatalf("lint malformed template timeout")
|
||||
case <-ch:
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "invalid character '{'") {
|
||||
t.Errorf("All didn't have the error for invalid character '{'")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/*
|
||||
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
// Chartfile runs a set of linter rules related to Chart.yaml file
|
||||
func Chartfile(linter *support.Linter) {
|
||||
chartFileName := "Chart.yaml"
|
||||
chartPath := filepath.Join(linter.ChartDir, chartFileName)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
|
||||
|
||||
chartFile, err := chartutil.LoadChartfile(chartPath)
|
||||
validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
|
||||
|
||||
// Guard clause. Following linter rules require a parsable ChartFile
|
||||
if !validChartFile {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = chartutil.StrictLoadChartfile(chartPath)
|
||||
linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err))
|
||||
|
||||
// type check for Chart.yaml . ignoring error as any parse
|
||||
// errors would already be caught in the above load function
|
||||
chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
|
||||
|
||||
// Chart metadata
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
|
||||
linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
|
||||
}
|
||||
|
||||
func validateChartVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "version")
|
||||
}
|
||||
|
||||
func validateChartAppVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "appVersion")
|
||||
}
|
||||
|
||||
func isStringValue(data map[string]interface{}, key string) error {
|
||||
value, ok := data[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
valueType := fmt.Sprintf("%T", value)
|
||||
if valueType != "string" {
|
||||
return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlNotDirectory(chartPath string) error {
|
||||
fi, err := os.Stat(chartPath)
|
||||
|
||||
if err == nil && fi.IsDir() {
|
||||
return errors.New("should be a file, not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlStrictFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartName(cf *chart.Metadata) error {
|
||||
if cf.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
name := filepath.Base(cf.Name)
|
||||
if name != cf.Name {
|
||||
return fmt.Errorf("chart name %q is invalid", cf.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartAPIVersion(cf *chart.Metadata) error {
|
||||
if cf.APIVersion == "" {
|
||||
return errors.New("apiVersion is required. The value must be \"v3\"")
|
||||
}
|
||||
|
||||
if cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartVersion(cf *chart.Metadata) error {
|
||||
if cf.Version == "" {
|
||||
return errors.New("version is required")
|
||||
}
|
||||
|
||||
version, err := semver.StrictNewVersion(cf.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version)
|
||||
}
|
||||
|
||||
c, err := semver.NewConstraint(">0.0.0-0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valid, msg := c.Validate(version)
|
||||
|
||||
if !valid && len(msg) > 0 {
|
||||
return fmt.Errorf("version %v", msg[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartMaintainer(cf *chart.Metadata) error {
|
||||
for _, maintainer := range cf.Maintainers {
|
||||
if maintainer == nil {
|
||||
return errors.New("a maintainer entry is empty")
|
||||
}
|
||||
if maintainer.Name == "" {
|
||||
return errors.New("each maintainer requires a name")
|
||||
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
|
||||
return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
|
||||
} else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) {
|
||||
return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartSources(cf *chart.Metadata) error {
|
||||
for _, source := range cf.Sources {
|
||||
if source == "" || !govalidator.IsRequestURL(source) {
|
||||
return fmt.Errorf("invalid source URL '%s'", source)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartIconPresence(cf *chart.Metadata) error {
|
||||
if cf.Icon == "" {
|
||||
return errors.New("icon is recommended")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartIconURL(cf *chart.Metadata) error {
|
||||
if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) {
|
||||
return fmt.Errorf("invalid icon URL '%s'", cf.Icon)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartDependencies(cf *chart.Metadata) error {
|
||||
if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartType(cf *chart.Metadata) error {
|
||||
if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadChartFileForTypeCheck loads the Chart.yaml
|
||||
// in a generic form of a map[string]interface{}, so that the type
|
||||
// of the values can be checked
|
||||
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
y := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(b, &y)
|
||||
return y, err
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Crds lints the CRDs in the Linter.
|
||||
func Crds(linter *support.Linter) {
|
||||
fpath := "crds/"
|
||||
crdsPath := filepath.Join(linter.ChartDir, fpath)
|
||||
|
||||
// crds directory is optional
|
||||
if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
|
||||
crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath))
|
||||
if !crdsDirValid {
|
||||
return
|
||||
}
|
||||
|
||||
// Load chart and parse CRDs
|
||||
chart, err := loader.Load(linter.ChartDir)
|
||||
|
||||
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !chartLoaded {
|
||||
return
|
||||
}
|
||||
|
||||
/* Iterate over all the CRDs to check:
|
||||
1. It is a YAML file and not a template
|
||||
2. The API version is apiextensions.k8s.io
|
||||
3. The kind is CustomResourceDefinition
|
||||
*/
|
||||
for _, crd := range chart.CRDObjects() {
|
||||
fileName := crd.Name
|
||||
fpath = fileName
|
||||
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096)
|
||||
for {
|
||||
var yamlStruct *k8sYamlStruct
|
||||
|
||||
err := decoder.Decode(&yamlStruct)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
// If YAML parsing fails here, it will always fail in the next block as well, so we should return here.
|
||||
// This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct.
|
||||
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
func validateCrdsDir(crdsPath string) error {
|
||||
fi, err := os.Stat(crdsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdAPIVersion(obj *k8sYamlStruct) error {
|
||||
if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") {
|
||||
return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdKind(obj *k8sYamlStruct) error {
|
||||
if obj.Kind != "CustomResourceDefinition" {
|
||||
return fmt.Errorf("object kind is not 'CustomResourceDefinition'")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
)
|
||||
|
||||
const invalidCrdsDir = "./testdata/invalidcrdsdir"
|
||||
|
||||
func TestInvalidCrdsDir(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: invalidCrdsDir}
|
||||
Crds(&linter)
|
||||
res := linter.Messages
|
||||
|
||||
assert.Len(t, res, 1)
|
||||
assert.ErrorContains(t, res[0].Err, "not a directory")
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Dependencies runs lints against a chart's dependencies
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7910
|
||||
func Dependencies(linter *support.Linter) {
|
||||
c, err := loader.LoadDir(linter.ChartDir)
|
||||
if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
|
||||
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
|
||||
}
|
||||
|
||||
func validateChartFormat(chartError error) error {
|
||||
if chartError != nil {
|
||||
return fmt.Errorf("unable to load chart\n\t%w", chartError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDependencyInChartsDir(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Dependencies() {
|
||||
dependencies[dep.Metadata.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
if _, ok := dependencies[dep.Name]; !ok {
|
||||
missing = append(missing, dep.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependencyInMetadata(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
dependencies[dep.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Dependencies() {
|
||||
if _, ok := dependencies[dep.Metadata.Name]; !ok {
|
||||
missing = append(missing, dep.Metadata.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependenciesUnique(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]*chart.Dependency{}
|
||||
shadowing := []string{}
|
||||
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
key := dep.Name
|
||||
if dep.Alias != "" {
|
||||
key = dep.Alias
|
||||
}
|
||||
if dependencies[key] != nil {
|
||||
shadowing = append(shadowing, key)
|
||||
}
|
||||
dependencies[key] = dep
|
||||
}
|
||||
if len(shadowing) > 0 {
|
||||
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
func chartWithBadDependencies() chart.Chart {
|
||||
badChartDeps := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "sub2",
|
||||
},
|
||||
{
|
||||
Name: "sub3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badChartDeps.SetDependencies(
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub1",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub2",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
)
|
||||
return badChartDeps
|
||||
}
|
||||
|
||||
func TestValidateDependencyInChartsDir(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInChartsDir(&c); err == nil {
|
||||
t.Error("chart should have been flagged for missing deps in chart directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependencyInMetadata(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInMetadata(&c); err == nil {
|
||||
t.Errorf("chart should have been flagged for missing deps in chart metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependenciesUnique(t *testing.T) {
|
||||
tests := []struct {
|
||||
chart chart.Chart
|
||||
}{
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "bar",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "baz",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Alias: "baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if err := validateDependenciesUnique(&tt.chart); err == nil {
|
||||
t.Errorf("chart should have been flagged for dependency shadowing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
c := chartWithBadDependencies()
|
||||
err := chartutil.SaveDir(&c, tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)}
|
||||
|
||||
Dependencies(&linter)
|
||||
if l := len(linter.Messages); l != 2 {
|
||||
t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l)
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message: %d, Error: %#v", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateNoDeprecations(t *testing.T) {
|
||||
deprecated := &k8sYamlStruct{
|
||||
APIVersion: "extensions/v1beta1",
|
||||
Kind: "Deployment",
|
||||
}
|
||||
err := validateNoDeprecations(deprecated, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected deprecated extension to be flagged")
|
||||
}
|
||||
depErr := err.(deprecatedAPIError)
|
||||
if depErr.Message == "" {
|
||||
t.Fatalf("Expected error message to be non-blank: %v", err)
|
||||
}
|
||||
|
||||
if err := validateNoDeprecations(&k8sYamlStruct{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
}, nil); err != nil {
|
||||
t.Errorf("Expected a v1 Pod to not be deprecated")
|
||||
}
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/validation"
|
||||
apipath "k8s.io/apimachinery/pkg/api/validation/path"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
"helm.sh/helm/v4/pkg/engine"
|
||||
)
|
||||
|
||||
// Templates lints the templates in the Linter.
|
||||
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
|
||||
TemplatesWithKubeVersion(linter, values, namespace, nil)
|
||||
}
|
||||
|
||||
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
|
||||
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
|
||||
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false)
|
||||
}
|
||||
|
||||
// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not.
|
||||
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
|
||||
fpath := "templates/"
|
||||
templatesPath := filepath.Join(linter.ChartDir, fpath)
|
||||
|
||||
// Templates directory is optional for now
|
||||
templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath))
|
||||
if !templatesDirExists {
|
||||
return
|
||||
}
|
||||
|
||||
validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath))
|
||||
if !validTemplatesDir {
|
||||
return
|
||||
}
|
||||
|
||||
// Load chart and parse templates
|
||||
chart, err := loader.Load(linter.ChartDir)
|
||||
|
||||
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !chartLoaded {
|
||||
return
|
||||
}
|
||||
|
||||
options := common.ReleaseOptions{
|
||||
Name: "test-release",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
caps := common.DefaultCapabilities.Copy()
|
||||
if kubeVersion != nil {
|
||||
caps.KubeVersion = *kubeVersion
|
||||
}
|
||||
|
||||
// lint ignores import-values
|
||||
// See https://github.com/helm/helm/issues/9658
|
||||
if err := chartutil.ProcessDependencies(chart, values); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cvals, err := util.CoalesceValues(chart, values)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
|
||||
if err != nil {
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
return
|
||||
}
|
||||
var e engine.Engine
|
||||
e.LintMode = true
|
||||
renderedContentMap, err := e.Render(chart, valuesToRender)
|
||||
|
||||
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !renderOk {
|
||||
return
|
||||
}
|
||||
|
||||
/* Iterate over all the templates to check:
|
||||
- It is a .yaml file
|
||||
- All the values in the template file is defined
|
||||
- {{}} include | quote
|
||||
- Generated content is a valid Yaml file
|
||||
- Metadata.Namespace is not set
|
||||
*/
|
||||
for _, template := range chart.Templates {
|
||||
fileName := template.Name
|
||||
fpath = fileName
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
|
||||
|
||||
// We only apply the following lint rules to yaml files
|
||||
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463
|
||||
// Check that all the templates have a matching value
|
||||
// linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate))
|
||||
|
||||
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037
|
||||
// linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate)))
|
||||
|
||||
renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)]
|
||||
if strings.TrimSpace(renderedContent) != "" {
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent))
|
||||
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096)
|
||||
|
||||
// Lint all resources if the file contains multiple documents separated by ---
|
||||
for {
|
||||
// Even though k8sYamlStruct only defines a few fields, an error in any other
|
||||
// key will be raised as well
|
||||
var yamlStruct *k8sYamlStruct
|
||||
|
||||
err := decoder.Decode(&yamlStruct)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
// If YAML linting fails here, it will always fail in the next block as well, so we should return here.
|
||||
// fix https://github.com/helm/helm/issues/11391
|
||||
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
|
||||
return
|
||||
}
|
||||
if yamlStruct != nil {
|
||||
// NOTE: set to warnings to allow users to support out-of-date kubernetes
|
||||
// Refs https://github.com/helm/helm/issues/8596
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateTopIndentLevel checks that the content does not start with an indent level > 0.
|
||||
//
|
||||
// This error can occur when a template accidentally inserts space. It can cause
|
||||
// unpredictable errors depending on whether the text is normalized before being passed
|
||||
// into the YAML parser. So we trap it here.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/8467
|
||||
func validateTopIndentLevel(content string) error {
|
||||
// Read lines until we get to a non-empty one
|
||||
scanner := bufio.NewScanner(bytes.NewBufferString(content))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// If line is empty, skip
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
// If it starts with one or more spaces, this is an error
|
||||
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||
return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line)
|
||||
}
|
||||
// Any other condition passes.
|
||||
return nil
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
func templatesDirExists(templatesPath string) error {
|
||||
_, err := os.Stat(templatesPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return errors.New("directory does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTemplatesDir(templatesPath string) error {
|
||||
fi, err := os.Stat(templatesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAllowedExtension(fileName string) error {
|
||||
ext := filepath.Ext(fileName)
|
||||
validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"}
|
||||
|
||||
if slices.Contains(validExtensions, ext) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext)
|
||||
}
|
||||
|
||||
func validateYamlContent(err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMetadataName uses the correct validation function for the object
|
||||
// Kind, or if not set, defaults to the standard definition of a subdomain in
|
||||
// DNS (RFC 1123), used by most resources.
|
||||
func validateMetadataName(obj *k8sYamlStruct) error {
|
||||
fn := validateMetadataNameFunc(obj)
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range fn(obj.Metadata.Name, false) {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg))
|
||||
}
|
||||
if len(allErrs) > 0 {
|
||||
return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMetadataNameFunc will return a name validation function for the
|
||||
// object kind, if defined below.
|
||||
//
|
||||
// Rules should match those set in the various api validations:
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39
|
||||
// ...
|
||||
//
|
||||
// Implementing here to avoid importing k/k.
|
||||
//
|
||||
// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object
|
||||
// kinds that don't have special requirements, so is the most likely to work if
|
||||
// new kinds are added.
|
||||
func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc {
|
||||
switch strings.ToLower(obj.Kind) {
|
||||
case "pod", "node", "secret", "endpoints", "resourcequota", // core
|
||||
"controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps
|
||||
"autoscaler", // autoscaler
|
||||
"cronjob", "job", // batch
|
||||
"lease", // coordination
|
||||
"endpointslice", // discovery
|
||||
"networkpolicy", "ingress", // networking
|
||||
"podsecuritypolicy", // policy
|
||||
"priorityclass", // scheduling
|
||||
"podpreset", // settings
|
||||
"storageclass", "volumeattachment", "csinode": // storage
|
||||
return validation.NameIsDNSSubdomain
|
||||
case "service":
|
||||
return validation.NameIsDNS1035Label
|
||||
case "namespace":
|
||||
return validation.ValidateNamespaceName
|
||||
case "serviceaccount":
|
||||
return validation.ValidateServiceAccountName
|
||||
case "certificatesigningrequest":
|
||||
// No validation.
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140
|
||||
return func(_ string, _ bool) []string { return nil }
|
||||
case "role", "clusterrole", "rolebinding", "clusterrolebinding":
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34
|
||||
return func(name string, _ bool) []string {
|
||||
return apipath.IsValidPathSegmentName(name)
|
||||
}
|
||||
default:
|
||||
return validation.NameIsDNSSubdomain
|
||||
}
|
||||
}
|
||||
|
||||
// validateMatchSelector ensures that template specs have a selector declared.
|
||||
// See https://github.com/helm/helm/issues/1990
|
||||
func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error {
|
||||
switch yamlStruct.Kind {
|
||||
case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet":
|
||||
// verify that matchLabels or matchExpressions is present
|
||||
if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") {
|
||||
return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
|
||||
if yamlStruct.Kind == "List" {
|
||||
m := struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Annotations map[string]string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(manifest), &m); err != nil {
|
||||
return validateYamlContent(err)
|
||||
}
|
||||
|
||||
for _, i := range m.Items {
|
||||
if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok {
|
||||
return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// k8sYamlStruct stubs a Kubernetes YAML file.
|
||||
type k8sYamlStruct struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string
|
||||
Metadata k8sYamlMetadata
|
||||
}
|
||||
|
||||
type k8sYamlMetadata struct {
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
@ -0,0 +1,441 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
const templateTestBasedir = "./testdata/albatross"
|
||||
|
||||
func TestValidateAllowedExtension(t *testing.T) {
|
||||
var failTest = []string{"/foo", "/test.toml"}
|
||||
for _, test := range failTest {
|
||||
err := validateAllowedExtension(test)
|
||||
if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") {
|
||||
t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test)
|
||||
}
|
||||
}
|
||||
var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"}
|
||||
for _, test := range successTest {
|
||||
err := validateAllowedExtension(test)
|
||||
if err != nil {
|
||||
t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var values = map[string]interface{}{"nameOverride": "", "httpPort": 80}
|
||||
|
||||
const namespace = "testNamespace"
|
||||
const strict = false
|
||||
|
||||
func TestTemplateParsing(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: templateTestBasedir}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("Expected one error, got %d, %v", len(res), res)
|
||||
}
|
||||
|
||||
if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") {
|
||||
t.Errorf("Unexpected error: %s", res[0])
|
||||
}
|
||||
}
|
||||
|
||||
var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
|
||||
var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
|
||||
|
||||
// Test a template with all the existing features:
|
||||
// namespaces, partial templates
|
||||
func TestTemplateIntegrationHappyPath(t *testing.T) {
|
||||
// Rename file so it gets ignored by the linter
|
||||
os.Rename(wrongTemplatePath, ignoredTemplatePath)
|
||||
defer os.Rename(ignoredTemplatePath, wrongTemplatePath)
|
||||
|
||||
linter := support.Linter{ChartDir: templateTestBasedir}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 0 {
|
||||
t.Fatalf("Expected no error, got %d, %v", len(res), res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiTemplateFail(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: "./testdata/multi-template-fail"}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("Expected 1 error, got %d, %v", len(res), res)
|
||||
}
|
||||
|
||||
if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") {
|
||||
t.Errorf("Unexpected error: %s", res[0].Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMetadataName(t *testing.T) {
|
||||
tests := []struct {
|
||||
obj *k8sYamlStruct
|
||||
wantErr bool
|
||||
}{
|
||||
// Most kinds use IsDNS1123Subdomain.
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true},
|
||||
|
||||
// Service uses IsDNS1035Label.
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
|
||||
|
||||
// Namespace uses IsDNS1123Label.
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false},
|
||||
|
||||
// CertificateSigningRequest has no validation.
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false},
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false},
|
||||
|
||||
// RBAC uses path validation.
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
|
||||
// Unknown Kind
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
|
||||
// No kind
|
||||
{&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) {
|
||||
if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedAPIFails(t *testing.T) {
|
||||
mychart := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
APIVersion: "v2",
|
||||
Name: "failapi",
|
||||
Version: "0.1.0",
|
||||
Icon: "satisfy-the-linting-gods.gif",
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/baddeployment.yaml",
|
||||
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
|
||||
},
|
||||
{
|
||||
Name: "templates/goodsecret.yaml",
|
||||
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
|
||||
},
|
||||
},
|
||||
}
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
if l := len(linter.Messages); l != 1 {
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
t.Fatalf("Expected 1 lint error, got %d", l)
|
||||
}
|
||||
|
||||
err := linter.Messages[0].Err.(deprecatedAPIError)
|
||||
if err.Deprecated != "apps/v1beta1 Deployment" {
|
||||
t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated)
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: foo
|
||||
data:
|
||||
myval1: {{default "val" .Values.mymap.key1 }}
|
||||
myval2: {{default "val" .Values.mymap.key2 }}
|
||||
`
|
||||
|
||||
// TestStrictTemplateParsingMapError is a regression test.
|
||||
//
|
||||
// The template engine should not produce an error when a map in values.yaml does
|
||||
// not contain all possible keys.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7483
|
||||
func TestStrictTemplateParsingMapError(t *testing.T) {
|
||||
|
||||
ch := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "regression7483",
|
||||
APIVersion: "v2",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"mymap": map[string]string{
|
||||
"key1": "val1",
|
||||
},
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/configmap.yaml",
|
||||
Data: []byte(manifest),
|
||||
},
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := chartutil.SaveDir(&ch, dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linter := &support.Linter{
|
||||
ChartDir: filepath.Join(dir, ch.Metadata.Name),
|
||||
}
|
||||
Templates(linter, ch.Values, namespace, strict)
|
||||
if len(linter.Messages) != 0 {
|
||||
t.Errorf("expected zero messages, got %d", len(linter.Messages))
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %q", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMatchSelector(t *testing.T) {
|
||||
md := &k8sYamlStruct{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Metadata: k8sYamlMetadata{
|
||||
Name: "mydeployment",
|
||||
},
|
||||
}
|
||||
manifest := `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
manifest = `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchExpressions:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
manifest = `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err == nil {
|
||||
t.Error("expected Deployment with no selector to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTopIndentLevel(t *testing.T) {
|
||||
for doc, shouldFail := range map[string]bool{
|
||||
// Should not fail
|
||||
"\n\n\n\t\n \t\n": false,
|
||||
"apiVersion:foo\n bar:baz": false,
|
||||
"\n\n\napiVersion:foo\n\n\n": false,
|
||||
// Should fail
|
||||
" apiVersion:foo": true,
|
||||
"\n\n apiVersion:foo\n\n": true,
|
||||
} {
|
||||
if err := validateTopIndentLevel(doc); (err == nil) == shouldFail {
|
||||
t.Errorf("Expected %t for %q", shouldFail, doc)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
|
||||
// See https://github.com/helm/helm/issues/8621
|
||||
func TestEmptyWithCommentsManifests(t *testing.T) {
|
||||
mychart := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
APIVersion: "v2",
|
||||
Name: "emptymanifests",
|
||||
Version: "0.1.0",
|
||||
Icon: "satisfy-the-linting-gods.gif",
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/empty-with-comments.yaml",
|
||||
Data: []byte("#@formatter:off\n"),
|
||||
},
|
||||
},
|
||||
}
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
if l := len(linter.Messages); l > 0 {
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
t.Fatalf("Expected 0 lint errors, got %d", l)
|
||||
}
|
||||
}
|
||||
func TestValidateListAnnotations(t *testing.T) {
|
||||
md := &k8sYamlStruct{
|
||||
APIVersion: "v1",
|
||||
Kind: "List",
|
||||
Metadata: k8sYamlMetadata{
|
||||
Name: "list",
|
||||
},
|
||||
}
|
||||
manifest := `
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
`
|
||||
|
||||
if err := validateListAnnotations(md, manifest); err == nil {
|
||||
t.Fatal("expected list with nested keep annotations to fail")
|
||||
}
|
||||
|
||||
manifest = `
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
metadata:
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
`
|
||||
|
||||
if err := validateListAnnotations(md, manifest); err != nil {
|
||||
t.Fatalf("List objects keep annotations should pass. got: %s", err)
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: albatross
|
||||
description: testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,15 @@
|
||||
name: "some-chart"
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 72445e2
|
||||
home: ""
|
||||
type: application
|
||||
appVersion: 72225e2
|
||||
icon: "https://some-url.com/icon.jpeg"
|
||||
dependencies:
|
||||
- name: mariadb
|
||||
version: 5.x.x
|
||||
repository: https://charts.helm.sh/stable/
|
||||
condition: mariadb.enabled
|
||||
tags:
|
||||
- database
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: "../badchartname"
|
||||
type: application
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: badcrdfile
|
||||
type: application
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,2 @@
|
||||
apiVersion: bad.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
@ -0,0 +1,2 @@
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: NotACustomResourceDefinition
|
@ -0,0 +1 @@
|
||||
# Default values for badcrdfile.
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: badvaluesfile
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.0.1
|
||||
home: ""
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: goodone
|
||||
description: good testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,19 @@
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: tests.test.io
|
||||
spec:
|
||||
group: test.io
|
||||
names:
|
||||
kind: Test
|
||||
listKind: TestList
|
||||
plural: tests
|
||||
singular: test
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name : v1alpha2
|
||||
served: true
|
||||
storage: true
|
||||
- name : v1alpha1
|
||||
served: true
|
||||
storage: false
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: invalidcrdsdir
|
||||
type: application
|
||||
icon: http://riverrun.io
|
@ -0,0 +1 @@
|
||||
# Default values for invalidcrdsdir.
|
@ -0,0 +1,25 @@
|
||||
apiVersion: v3
|
||||
name: test
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
icon: https://riverrun.io
|
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: multi-template-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: v3-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,16 @@
|
||||
apiVersion: v3
|
||||
name: withsubchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
||||
icon: http://riverrun.io
|
||||
|
||||
dependencies:
|
||||
- name: subchart
|
||||
version: 0.1.16
|
||||
repository: "file://../subchart"
|
||||
import-values:
|
||||
- child: subchart
|
||||
parent: subchart
|
||||
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: subchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
)
|
||||
|
||||
// ValuesWithOverrides tests the values.yaml file.
|
||||
//
|
||||
// If a schema is present in the chart, values are tested against that. Otherwise,
|
||||
// they are only tested for well-formedness.
|
||||
//
|
||||
// If additional values are supplied, they are coalesced into the values in values.yaml.
|
||||
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
|
||||
file := "values.yaml"
|
||||
vf := filepath.Join(linter.ChartDir, file)
|
||||
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
|
||||
|
||||
if !fileExists {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
|
||||
}
|
||||
|
||||
func validateValuesFileExistence(valuesPath string) error {
|
||||
_, err := os.Stat(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
|
||||
values, err := common.ReadValuesFile(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
|
||||
// level values against the top-level expectations. Subchart values are not linted.
|
||||
// We could change that. For now, though, we retain that strategy, and thus can
|
||||
// coalesce tables (like reuse-values does) instead of doing the full chart
|
||||
// CoalesceValues
|
||||
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
|
||||
coalescedValues = util.CoalesceTables(coalescedValues, values)
|
||||
|
||||
ext := filepath.Ext(valuesPath)
|
||||
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
|
||||
schema, err := os.ReadFile(schemaPath)
|
||||
if len(schema) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !skipSchemaValidation {
|
||||
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -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"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
)
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
@ -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"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/sympath"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
"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, Data: data})
|
||||
return nil
|
||||
}
|
||||
if err = sympath.Walk(topdir, walk); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
return LoadFiles(files)
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
/*
|
||||
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"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
)
|
||||
|
||||
// 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, 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, &common.File{Name: f.Name, 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, 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, Data: f.Data})
|
||||
default:
|
||||
c.Files = append(c.Files, &common.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([]*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 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,713 @@
|
||||
/*
|
||||
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"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||
)
|
||||
|
||||
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 := []*archive.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([]*archive.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 := []*archive.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 TestMergeValuesV3(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 []*common.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.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue