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