mirror of https://github.com/helm/helm
Merge branch 'main' of https://github.com/helm/helm into main-fix-push-scope
Signed-off-by: kimsungmin1 <sm28.kim@samsung.com>pull/31211/head
commit
8cbde7b1d4
@ -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)
|
||||
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
|
||||
rules.Dependencies(&result)
|
||||
rules.Crds(&result)
|
||||
|
||||
return result
|
||||
}
|
@ -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,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,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: invalidcrdsdir
|
||||
type: application
|
||||
icon: http://riverrun.io
|
@ -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,79 @@
|
||||
/*
|
||||
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{}) {
|
||||
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))
|
||||
}
|
||||
|
||||
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{}) 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
|
||||
}
|
||||
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
|
||||
helmversion "helm.sh/helm/v4/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
|
||||
// must be a string.
|
||||
k8sVersionMajor = "1"
|
||||
k8sVersionMinor = "20"
|
||||
|
||||
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
|
||||
DefaultVersionSet = allKnownVersions()
|
||||
|
||||
// DefaultCapabilities is the default set of capabilities.
|
||||
DefaultCapabilities = &Capabilities{
|
||||
KubeVersion: KubeVersion{
|
||||
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
|
||||
Major: k8sVersionMajor,
|
||||
Minor: k8sVersionMinor,
|
||||
},
|
||||
APIVersions: DefaultVersionSet,
|
||||
HelmVersion: helmversion.Get(),
|
||||
}
|
||||
)
|
||||
|
||||
// Capabilities describes the capabilities of the Kubernetes cluster.
|
||||
type Capabilities struct {
|
||||
// KubeVersion is the Kubernetes version.
|
||||
KubeVersion KubeVersion
|
||||
// APIVersions are supported Kubernetes API versions.
|
||||
APIVersions VersionSet
|
||||
// HelmVersion is the build information for this helm version
|
||||
HelmVersion helmversion.BuildInfo
|
||||
}
|
||||
|
||||
func (capabilities *Capabilities) Copy() *Capabilities {
|
||||
return &Capabilities{
|
||||
KubeVersion: capabilities.KubeVersion,
|
||||
APIVersions: capabilities.APIVersions,
|
||||
HelmVersion: capabilities.HelmVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// KubeVersion is the Kubernetes version.
|
||||
type KubeVersion struct {
|
||||
Version string // Kubernetes version
|
||||
Major string // Kubernetes major version
|
||||
Minor string // Kubernetes minor version
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (kv *KubeVersion) String() string { return kv.Version }
|
||||
|
||||
// GitVersion returns the Kubernetes version string.
|
||||
//
|
||||
// Deprecated: use KubeVersion.Version.
|
||||
func (kv *KubeVersion) GitVersion() string { return kv.Version }
|
||||
|
||||
// ParseKubeVersion parses kubernetes version from string
|
||||
func ParseKubeVersion(version string) (*KubeVersion, error) {
|
||||
sv, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &KubeVersion{
|
||||
Version: "v" + sv.String(),
|
||||
Major: strconv.FormatUint(sv.Major(), 10),
|
||||
Minor: strconv.FormatUint(sv.Minor(), 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VersionSet is a set of Kubernetes API versions.
|
||||
type VersionSet []string
|
||||
|
||||
// Has returns true if the version string is in the set.
|
||||
//
|
||||
// vs.Has("apps/v1")
|
||||
func (v VersionSet) Has(apiVersion string) bool {
|
||||
return slices.Contains(v, apiVersion)
|
||||
}
|
||||
|
||||
func allKnownVersions() VersionSet {
|
||||
// We should register the built in extension APIs as well so CRDs are
|
||||
// supported in the default version set. This has caused problems with `helm
|
||||
// template` in the past, so let's be safe
|
||||
apiextensionsv1beta1.AddToScheme(scheme.Scheme)
|
||||
apiextensionsv1.AddToScheme(scheme.Scheme)
|
||||
|
||||
groups := scheme.Scheme.PrioritizedVersionsAllGroups()
|
||||
vs := make(VersionSet, 0, len(groups))
|
||||
for _, gv := range groups {
|
||||
vs = append(vs, gv.String())
|
||||
}
|
||||
return vs
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionSet(t *testing.T) {
|
||||
vs := VersionSet{"v1", "apps/v1"}
|
||||
if d := len(vs); d != 2 {
|
||||
t.Errorf("Expected 2 versions, got %d", d)
|
||||
}
|
||||
|
||||
if !vs.Has("apps/v1") {
|
||||
t.Error("Expected to find apps/v1")
|
||||
}
|
||||
|
||||
if vs.Has("Spanish/inquisition") {
|
||||
t.Error("No one expects the Spanish/inquisition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultVersionSet(t *testing.T) {
|
||||
if !DefaultVersionSet.Has("v1") {
|
||||
t.Error("Expected core v1 version set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilities(t *testing.T) {
|
||||
kv := DefaultCapabilities.KubeVersion
|
||||
if kv.String() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Version != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.GitVersion() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "20" {
|
||||
t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
|
||||
hv := DefaultCapabilities.HelmVersion
|
||||
|
||||
if hv.Version != "v4.0" {
|
||||
t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKubeVersion(t *testing.T) {
|
||||
kv, err := ParseKubeVersion("v1.16.0")
|
||||
if err != nil {
|
||||
t.Errorf("Expected v1.16.0 to parse successfully")
|
||||
}
|
||||
if kv.Version != "v1.16.0" {
|
||||
t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "16" {
|
||||
t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor)
|
||||
}
|
||||
}
|
@ -1,723 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
|
||||
var testCoalesceValuesYaml = []byte(`
|
||||
top: yup
|
||||
bottom: null
|
||||
right: Null
|
||||
left: NULL
|
||||
front: ~
|
||||
back: ""
|
||||
nested:
|
||||
boat: null
|
||||
|
||||
global:
|
||||
name: Ishmael
|
||||
subject: Queequeg
|
||||
nested:
|
||||
boat: true
|
||||
|
||||
pequod:
|
||||
boat: null
|
||||
global:
|
||||
name: Stinky
|
||||
harpooner: Tashtego
|
||||
nested:
|
||||
boat: false
|
||||
sail: true
|
||||
foo2: null
|
||||
ahab:
|
||||
scope: whale
|
||||
boat: null
|
||||
nested:
|
||||
foo: true
|
||||
boat: null
|
||||
object: null
|
||||
`)
|
||||
|
||||
func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart {
|
||||
c.AddDependency(deps...)
|
||||
return c
|
||||
}
|
||||
|
||||
func TestCoalesceValues(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "moby"},
|
||||
Values: map[string]interface{}{
|
||||
"back": "exists",
|
||||
"bottom": "exists",
|
||||
"front": "exists",
|
||||
"left": "exists",
|
||||
"name": "moby",
|
||||
"nested": map[string]interface{}{"boat": true},
|
||||
"override": "bad",
|
||||
"right": "exists",
|
||||
"scope": "moby",
|
||||
"top": "nope",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l0": "moby"},
|
||||
},
|
||||
"pequod": map[string]interface{}{
|
||||
"boat": "maybe",
|
||||
"ahab": map[string]interface{}{
|
||||
"boat": "maybe",
|
||||
"nested": map[string]interface{}{"boat": "maybe"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "pequod"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
"scope": "pequod",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "pequod"},
|
||||
},
|
||||
"boat": false,
|
||||
"ahab": map[string]interface{}{
|
||||
"boat": false,
|
||||
"nested": map[string]interface{}{"boat": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "ahab"},
|
||||
Values: map[string]interface{}{
|
||||
"global": map[string]interface{}{
|
||||
"nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"},
|
||||
"nested2": map[string]interface{}{"l2": "ahab"},
|
||||
},
|
||||
"scope": "ahab",
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"nested": map[string]interface{}{"foo": false, "boat": true},
|
||||
"object": map[string]interface{}{"foo": "bar"},
|
||||
},
|
||||
},
|
||||
),
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "spouter"},
|
||||
Values: map[string]interface{}{
|
||||
"scope": "spouter",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "spouter"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
vals, err := ReadValues(testCoalesceValuesYaml)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// taking a copy of the values before passing it
|
||||
// to CoalesceValues as argument, so that we can
|
||||
// use it for asserting later
|
||||
valsCopy := make(Values, len(vals))
|
||||
maps.Copy(valsCopy, vals)
|
||||
|
||||
v, err := CoalesceValues(c, vals)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", " ")
|
||||
t.Logf("Coalesced Values: %s", string(j))
|
||||
|
||||
tests := []struct {
|
||||
tpl string
|
||||
expect string
|
||||
}{
|
||||
{"{{.top}}", "yup"},
|
||||
{"{{.back}}", ""},
|
||||
{"{{.name}}", "moby"},
|
||||
{"{{.global.name}}", "Ishmael"},
|
||||
{"{{.global.subject}}", "Queequeg"},
|
||||
{"{{.global.harpooner}}", "<no value>"},
|
||||
{"{{.pequod.name}}", "pequod"},
|
||||
{"{{.pequod.ahab.name}}", "ahab"},
|
||||
{"{{.pequod.ahab.scope}}", "whale"},
|
||||
{"{{.pequod.ahab.nested.foo}}", "true"},
|
||||
{"{{.pequod.ahab.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.ahab.global.nested.foo}}", "bar"},
|
||||
{"{{.pequod.ahab.global.nested.foo2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
|
||||
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
|
||||
{"{{.pequod.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.global.nested.foo}}", "<no value>"},
|
||||
{"{{.pequod.global.subject}}", "Queequeg"},
|
||||
{"{{.spouter.global.name}}", "Ishmael"},
|
||||
{"{{.spouter.global.harpooner}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.boat}}", "true"},
|
||||
{"{{.spouter.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.sail}}", "true"},
|
||||
{"{{.spouter.global.nested.sail}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested2.l0}}", "moby"},
|
||||
{"{{.global.nested2.l1}}", "<no value>"},
|
||||
{"{{.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
|
||||
{"{{.spouter.global.nested2.l0}}", "moby"},
|
||||
{"{{.spouter.global.nested2.l1}}", "spouter"},
|
||||
{"{{.spouter.global.nested2.l2}}", "<no value>"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
|
||||
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
|
||||
}
|
||||
}
|
||||
|
||||
nullKeys := []string{"bottom", "right", "left", "front"}
|
||||
for _, nullKey := range nullKeys {
|
||||
if _, ok := v[nullKey]; ok {
|
||||
t.Errorf("Expected key %q to be removed, still present", nullKey)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := v["nested"].(map[string]interface{})["boat"]; ok {
|
||||
t.Error("Expected nested boat key to be removed, still present")
|
||||
}
|
||||
|
||||
subchart := v["pequod"].(map[string]interface{})
|
||||
if _, ok := subchart["boat"]; ok {
|
||||
t.Error("Expected subchart boat key to be removed, still present")
|
||||
}
|
||||
|
||||
subsubchart := subchart["ahab"].(map[string]interface{})
|
||||
if _, ok := subsubchart["boat"]; ok {
|
||||
t.Error("Expected sub-subchart ahab boat key to be removed, still present")
|
||||
}
|
||||
|
||||
if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok {
|
||||
t.Error("Expected sub-subchart nested boat key to be removed, still present")
|
||||
}
|
||||
|
||||
if _, ok := subsubchart["object"]; ok {
|
||||
t.Error("Expected sub-subchart object map to be removed, still present")
|
||||
}
|
||||
|
||||
// CoalesceValues should not mutate the passed arguments
|
||||
is.Equal(valsCopy, vals)
|
||||
}
|
||||
|
||||
func TestMergeValues(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "moby"},
|
||||
Values: map[string]interface{}{
|
||||
"back": "exists",
|
||||
"bottom": "exists",
|
||||
"front": "exists",
|
||||
"left": "exists",
|
||||
"name": "moby",
|
||||
"nested": map[string]interface{}{"boat": true},
|
||||
"override": "bad",
|
||||
"right": "exists",
|
||||
"scope": "moby",
|
||||
"top": "nope",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l0": "moby"},
|
||||
},
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "pequod"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
"scope": "pequod",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "pequod"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "ahab"},
|
||||
Values: map[string]interface{}{
|
||||
"global": map[string]interface{}{
|
||||
"nested": map[string]interface{}{"foo": "bar"},
|
||||
"nested2": map[string]interface{}{"l2": "ahab"},
|
||||
},
|
||||
"scope": "ahab",
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"nested": map[string]interface{}{"foo": false, "bar": true},
|
||||
},
|
||||
},
|
||||
),
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "spouter"},
|
||||
Values: map[string]interface{}{
|
||||
"scope": "spouter",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "spouter"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
vals, err := ReadValues(testCoalesceValuesYaml)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// taking a copy of the values before passing it
|
||||
// to MergeValues as argument, so that we can
|
||||
// use it for asserting later
|
||||
valsCopy := make(Values, len(vals))
|
||||
maps.Copy(valsCopy, vals)
|
||||
|
||||
v, err := MergeValues(c, vals)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", " ")
|
||||
t.Logf("Coalesced Values: %s", string(j))
|
||||
|
||||
tests := []struct {
|
||||
tpl string
|
||||
expect string
|
||||
}{
|
||||
{"{{.top}}", "yup"},
|
||||
{"{{.back}}", ""},
|
||||
{"{{.name}}", "moby"},
|
||||
{"{{.global.name}}", "Ishmael"},
|
||||
{"{{.global.subject}}", "Queequeg"},
|
||||
{"{{.global.harpooner}}", "<no value>"},
|
||||
{"{{.pequod.name}}", "pequod"},
|
||||
{"{{.pequod.ahab.name}}", "ahab"},
|
||||
{"{{.pequod.ahab.scope}}", "whale"},
|
||||
{"{{.pequod.ahab.nested.foo}}", "true"},
|
||||
{"{{.pequod.ahab.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.ahab.global.nested.foo}}", "bar"},
|
||||
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
|
||||
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
|
||||
{"{{.pequod.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.global.nested.foo}}", "<no value>"},
|
||||
{"{{.pequod.global.subject}}", "Queequeg"},
|
||||
{"{{.spouter.global.name}}", "Ishmael"},
|
||||
{"{{.spouter.global.harpooner}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.boat}}", "true"},
|
||||
{"{{.spouter.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.sail}}", "true"},
|
||||
{"{{.spouter.global.nested.sail}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested2.l0}}", "moby"},
|
||||
{"{{.global.nested2.l1}}", "<no value>"},
|
||||
{"{{.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
|
||||
{"{{.spouter.global.nested2.l0}}", "moby"},
|
||||
{"{{.spouter.global.nested2.l1}}", "spouter"},
|
||||
{"{{.spouter.global.nested2.l2}}", "<no value>"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
|
||||
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
|
||||
}
|
||||
}
|
||||
|
||||
// nullKeys is different from coalescing. Here the null/nil values are not
|
||||
// removed.
|
||||
nullKeys := []string{"bottom", "right", "left", "front"}
|
||||
for _, nullKey := range nullKeys {
|
||||
if vv, ok := v[nullKey]; !ok {
|
||||
t.Errorf("Expected key %q to be present but it was removed", nullKey)
|
||||
} else if vv != nil {
|
||||
t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok {
|
||||
t.Error("Expected nested boat key to be present but it was removed")
|
||||
}
|
||||
|
||||
subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{})
|
||||
if _, ok := subchart["boat"]; !ok {
|
||||
t.Error("Expected subchart boat key to be present but it was removed")
|
||||
}
|
||||
|
||||
if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok {
|
||||
t.Error("Expected subchart nested bar key to be present but it was removed")
|
||||
}
|
||||
|
||||
// CoalesceValues should not mutate the passed arguments
|
||||
is.Equal(valsCopy, vals)
|
||||
}
|
||||
|
||||
func TestCoalesceTables(t *testing.T) {
|
||||
dst := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": nil,
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": nil,
|
||||
}
|
||||
src := map[string]interface{}{
|
||||
"occupation": "whaler",
|
||||
"address": map[string]interface{}{
|
||||
"state": "MA",
|
||||
"street": "234 Spouter Inn Ct.",
|
||||
"country": "US",
|
||||
},
|
||||
"details": "empty",
|
||||
"boat": map[string]interface{}{
|
||||
"mast": true,
|
||||
},
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst overrides anything in src, but that
|
||||
// otherwise the values are coalesced.
|
||||
CoalesceTables(dst, src)
|
||||
|
||||
if dst["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst["name"])
|
||||
}
|
||||
if dst["occupation"] != "whaler" {
|
||||
t.Errorf("Unexpected occupation: %s", dst["occupation"])
|
||||
}
|
||||
|
||||
addr, ok := dst["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr["street"])
|
||||
}
|
||||
|
||||
if addr["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr["city"])
|
||||
}
|
||||
|
||||
if addr["state"].(string) != "MA" {
|
||||
t.Errorf("Unexpected state: %v", addr["state"])
|
||||
}
|
||||
|
||||
if _, ok = addr["country"]; ok {
|
||||
t.Error("The country is not left out.")
|
||||
}
|
||||
|
||||
if det, ok := dst["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst["details"])
|
||||
} else if _, ok := det["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst["boat"])
|
||||
}
|
||||
|
||||
if _, ok = dst["hole"]; ok {
|
||||
t.Error("The hole still exists.")
|
||||
}
|
||||
|
||||
dst2 := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": "US",
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst should have all values set,
|
||||
// this happens when the --reuse-values flag is set but the chart has no modifications yet
|
||||
CoalesceTables(dst2, nil)
|
||||
|
||||
if dst2["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst2["name"])
|
||||
}
|
||||
|
||||
addr2, ok := dst2["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr2["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr2["street"])
|
||||
}
|
||||
|
||||
if addr2["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr2["city"])
|
||||
}
|
||||
|
||||
if addr2["country"].(string) != "US" {
|
||||
t.Errorf("Unexpected Country: %v", addr2["country"])
|
||||
}
|
||||
|
||||
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst2["details"])
|
||||
} else if _, ok := det2["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst2["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["hole"].(string) != "black" {
|
||||
t.Errorf("Expected hole string, got %v", dst2["boat"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTables(t *testing.T) {
|
||||
dst := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": nil,
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": nil,
|
||||
}
|
||||
src := map[string]interface{}{
|
||||
"occupation": "whaler",
|
||||
"address": map[string]interface{}{
|
||||
"state": "MA",
|
||||
"street": "234 Spouter Inn Ct.",
|
||||
"country": "US",
|
||||
},
|
||||
"details": "empty",
|
||||
"boat": map[string]interface{}{
|
||||
"mast": true,
|
||||
},
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst overrides anything in src, but that
|
||||
// otherwise the values are coalesced.
|
||||
MergeTables(dst, src)
|
||||
|
||||
if dst["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst["name"])
|
||||
}
|
||||
if dst["occupation"] != "whaler" {
|
||||
t.Errorf("Unexpected occupation: %s", dst["occupation"])
|
||||
}
|
||||
|
||||
addr, ok := dst["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr["street"])
|
||||
}
|
||||
|
||||
if addr["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr["city"])
|
||||
}
|
||||
|
||||
if addr["state"].(string) != "MA" {
|
||||
t.Errorf("Unexpected state: %v", addr["state"])
|
||||
}
|
||||
|
||||
// This is one test that is different from CoalesceTables. Because country
|
||||
// is a nil value and it's not removed it's still present.
|
||||
if _, ok = addr["country"]; !ok {
|
||||
t.Error("The country is left out.")
|
||||
}
|
||||
|
||||
if det, ok := dst["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst["details"])
|
||||
} else if _, ok := det["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst["boat"])
|
||||
}
|
||||
|
||||
// This is one test that is different from CoalesceTables. Because hole
|
||||
// is a nil value and it's not removed it's still present.
|
||||
if _, ok = dst["hole"]; !ok {
|
||||
t.Error("The hole no longer exists.")
|
||||
}
|
||||
|
||||
dst2 := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": "US",
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": "black",
|
||||
"nilval": nil,
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst should have all values set,
|
||||
// this happens when the --reuse-values flag is set but the chart has no modifications yet
|
||||
MergeTables(dst2, nil)
|
||||
|
||||
if dst2["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst2["name"])
|
||||
}
|
||||
|
||||
addr2, ok := dst2["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr2["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr2["street"])
|
||||
}
|
||||
|
||||
if addr2["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr2["city"])
|
||||
}
|
||||
|
||||
if addr2["country"].(string) != "US" {
|
||||
t.Errorf("Unexpected Country: %v", addr2["country"])
|
||||
}
|
||||
|
||||
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst2["details"])
|
||||
} else if _, ok := det2["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst2["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["hole"].(string) != "black" {
|
||||
t.Errorf("Expected hole string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["nilval"] != nil {
|
||||
t.Error("Expected nilvalue to have nil value but it does not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoalesceValuesWarnings(t *testing.T) {
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level1"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "moby",
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level2"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level3"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"spear": map[string]interface{}{
|
||||
"tip": true,
|
||||
"sail": map[string]interface{}{
|
||||
"cotton": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"level2": map[string]interface{}{
|
||||
"level3": map[string]interface{}{
|
||||
"boat": map[string]interface{}{"mast": true},
|
||||
"spear": map[string]interface{}{
|
||||
"tip": map[string]interface{}{
|
||||
"sharp": true,
|
||||
},
|
||||
"sail": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
warnings := make([]string, 0)
|
||||
printf := func(format string, v ...interface{}) {
|
||||
t.Logf(format, v...)
|
||||
warnings = append(warnings, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
_, err := coalesce(printf, c, vals, "", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("vals: %v", vals)
|
||||
assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.")
|
||||
assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)")
|
||||
assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])")
|
||||
|
||||
}
|
||||
|
||||
func TestConcatPrefix(t *testing.T) {
|
||||
assert.Equal(t, "b", concatPrefix("", "b"))
|
||||
assert.Equal(t, "a.b", concatPrefix("a", "b"))
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
func TestValidateAgainstSingleSchema(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#'
|
||||
- at '': got number, want boolean or object`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading JSON file: %s", err)
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `- at '': missing property 'employmentInfo'
|
||||
- at '/age': minimum: got -5, want 0
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
const subchartSchema = `{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"age": {
|
||||
"description": "Age",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"age"
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
const subchartSchema2020 = `{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"contains": { "type": "string" },
|
||||
"unevaluatedItems": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"required": ["data"]
|
||||
}
|
||||
`
|
||||
|
||||
func TestValidateAgainstSchema(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"age": 25,
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchemaNegative(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{},
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSchema(chrt, vals); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `subchart:
|
||||
- at '': missing property 'age'
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchema2020(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema2020)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"data": []any{"hello", 12},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchema2020Negative(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema2020)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"data": []any{12},
|
||||
},
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSchema(chrt, vals); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `subchart:
|
||||
- at '/data': no items match contains schema
|
||||
- at '/data/0': got number, want string
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
// GlobalKey is the name of the Values key that is used for storing global vars.
|
||||
const GlobalKey = "global"
|
||||
|
||||
// Values represents a collection of chart values.
|
||||
type Values map[string]interface{}
|
||||
|
||||
// YAML encodes the Values into a YAML string.
|
||||
func (v Values) YAML() (string, error) {
|
||||
b, err := yaml.Marshal(v)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// Table gets a table (YAML subsection) from a Values object.
|
||||
//
|
||||
// The table is returned as a Values.
|
||||
//
|
||||
// Compound table names may be specified with dots:
|
||||
//
|
||||
// foo.bar
|
||||
//
|
||||
// The above will be evaluated as "The table bar inside the table
|
||||
// foo".
|
||||
//
|
||||
// An ErrNoTable is returned if the table does not exist.
|
||||
func (v Values) Table(name string) (Values, error) {
|
||||
table := v
|
||||
var err error
|
||||
|
||||
for _, n := range parsePath(name) {
|
||||
if table, err = tableLookup(table, n); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
// AsMap is a utility function for converting Values to a map[string]interface{}.
|
||||
//
|
||||
// It protects against nil map panics.
|
||||
func (v Values) AsMap() map[string]interface{} {
|
||||
if len(v) == 0 {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Encode writes serialized Values information to the given io.Writer.
|
||||
func (v Values) Encode(w io.Writer) error {
|
||||
out, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func tableLookup(v Values, simple string) (Values, error) {
|
||||
v2, ok := v[simple]
|
||||
if !ok {
|
||||
return v, ErrNoTable{simple}
|
||||
}
|
||||
if vv, ok := v2.(map[string]interface{}); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// This catches a case where a value is of type Values, but doesn't (for some
|
||||
// reason) match the map[string]interface{}. This has been observed in the
|
||||
// wild, and might be a result of a nil map of type Values.
|
||||
if vv, ok := v2.(Values); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
return Values{}, ErrNoTable{simple}
|
||||
}
|
||||
|
||||
// ReadValues will parse YAML byte data into a Values.
|
||||
func ReadValues(data []byte) (vals Values, err error) {
|
||||
err = yaml.Unmarshal(data, &vals)
|
||||
if len(vals) == 0 {
|
||||
vals = Values{}
|
||||
}
|
||||
return vals, err
|
||||
}
|
||||
|
||||
// ReadValuesFile will parse a YAML file into a map of values.
|
||||
func ReadValuesFile(filename string) (Values, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
return ReadValues(data)
|
||||
}
|
||||
|
||||
// ReleaseOptions represents the additional release options needed
|
||||
// for the composition of the final values struct
|
||||
type ReleaseOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Revision int
|
||||
IsUpgrade bool
|
||||
IsInstall bool
|
||||
}
|
||||
|
||||
// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// This takes both ReleaseOptions and Capabilities to merge into the render values.
|
||||
func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) {
|
||||
return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false)
|
||||
}
|
||||
|
||||
// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// This takes both ReleaseOptions and Capabilities to merge into the render values.
|
||||
func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) {
|
||||
if caps == nil {
|
||||
caps = DefaultCapabilities
|
||||
}
|
||||
top := map[string]interface{}{
|
||||
"Chart": chrt.Metadata,
|
||||
"Capabilities": caps,
|
||||
"Release": map[string]interface{}{
|
||||
"Name": options.Name,
|
||||
"Namespace": options.Namespace,
|
||||
"IsUpgrade": options.IsUpgrade,
|
||||
"IsInstall": options.IsInstall,
|
||||
"Revision": options.Revision,
|
||||
"Service": "Helm",
|
||||
},
|
||||
}
|
||||
|
||||
vals, err := CoalesceValues(chrt, chrtVals)
|
||||
if err != nil {
|
||||
return top, err
|
||||
}
|
||||
|
||||
if !skipSchemaValidation {
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err)
|
||||
}
|
||||
}
|
||||
|
||||
top["Values"] = vals
|
||||
return top, nil
|
||||
}
|
||||
|
||||
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
|
||||
func istable(v interface{}) bool {
|
||||
_, ok := v.(map[string]interface{})
|
||||
return ok
|
||||
}
|
||||
|
||||
// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
|
||||
// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
|
||||
// Given the following YAML data the value at path "chapter.one.title" is "Loomings".
|
||||
//
|
||||
// chapter:
|
||||
// one:
|
||||
// title: "Loomings"
|
||||
func (v Values) PathValue(path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("YAML path cannot be empty")
|
||||
}
|
||||
return v.pathValue(parsePath(path))
|
||||
}
|
||||
|
||||
func (v Values) pathValue(path []string) (interface{}, error) {
|
||||
if len(path) == 1 {
|
||||
// if exists must be root key not table
|
||||
if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
|
||||
return v[path[0]], nil
|
||||
}
|
||||
return nil, ErrNoValue{path[0]}
|
||||
}
|
||||
|
||||
key, path := path[len(path)-1], path[:len(path)-1]
|
||||
// get our table for table path
|
||||
t, err := v.Table(joinPath(path...))
|
||||
if err != nil {
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
// check table for key and ensure value is not a table
|
||||
if k, ok := t[key]; ok && !istable(k) {
|
||||
return k, nil
|
||||
}
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
|
||||
func parsePath(key string) []string { return strings.Split(key, ".") }
|
||||
|
||||
func joinPath(path ...string) string { return strings.Join(path, ".") }
|
@ -1,293 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
func TestReadValues(t *testing.T) {
|
||||
doc := `# Test YAML parse
|
||||
poet: "Coleridge"
|
||||
title: "Rime of the Ancient Mariner"
|
||||
stanza:
|
||||
- "at"
|
||||
- "length"
|
||||
- "did"
|
||||
- cross
|
||||
- an
|
||||
- Albatross
|
||||
|
||||
mariner:
|
||||
with: "crossbow"
|
||||
shot: "ALBATROSS"
|
||||
|
||||
water:
|
||||
water:
|
||||
where: "everywhere"
|
||||
nor: "any drop to drink"
|
||||
`
|
||||
|
||||
data, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing bytes: %s", err)
|
||||
}
|
||||
matchValues(t, data)
|
||||
|
||||
tests := []string{`poet: "Coleridge"`, "# Just a comment", ""}
|
||||
|
||||
for _, tt := range tests {
|
||||
data, err = ReadValues([]byte(tt))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing bytes (%s): %s", tt, err)
|
||||
}
|
||||
if data == nil {
|
||||
t.Errorf(`YAML string "%s" gave a nil map`, tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToRenderValues(t *testing.T) {
|
||||
|
||||
chartValues := map[string]interface{}{
|
||||
"name": "al Rashid",
|
||||
"where": map[string]interface{}{
|
||||
"city": "Basrah",
|
||||
"title": "caliph",
|
||||
},
|
||||
}
|
||||
|
||||
overrideValues := map[string]interface{}{
|
||||
"name": "Haroun",
|
||||
"where": map[string]interface{}{
|
||||
"city": "Baghdad",
|
||||
"date": "809 CE",
|
||||
},
|
||||
}
|
||||
|
||||
c := &chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "test"},
|
||||
Templates: []*chart.File{},
|
||||
Values: chartValues,
|
||||
Files: []*chart.File{
|
||||
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
|
||||
},
|
||||
}
|
||||
c.AddDependency(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "where"},
|
||||
})
|
||||
|
||||
o := ReleaseOptions{
|
||||
Name: "Seven Voyages",
|
||||
Namespace: "default",
|
||||
Revision: 1,
|
||||
IsInstall: true,
|
||||
}
|
||||
|
||||
res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure that the top-level values are all set.
|
||||
if name := res["Chart"].(*chart.Metadata).Name; name != "test" {
|
||||
t.Errorf("Expected chart name 'test', got %q", name)
|
||||
}
|
||||
relmap := res["Release"].(map[string]interface{})
|
||||
if name := relmap["Name"]; name.(string) != "Seven Voyages" {
|
||||
t.Errorf("Expected release name 'Seven Voyages', got %q", name)
|
||||
}
|
||||
if namespace := relmap["Namespace"]; namespace.(string) != "default" {
|
||||
t.Errorf("Expected namespace 'default', got %q", namespace)
|
||||
}
|
||||
if revision := relmap["Revision"]; revision.(int) != 1 {
|
||||
t.Errorf("Expected revision '1', got %d", revision)
|
||||
}
|
||||
if relmap["IsUpgrade"].(bool) {
|
||||
t.Error("Expected upgrade to be false.")
|
||||
}
|
||||
if !relmap["IsInstall"].(bool) {
|
||||
t.Errorf("Expected install to be true.")
|
||||
}
|
||||
if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
|
||||
t.Error("Expected Capabilities to have v1 as an API")
|
||||
}
|
||||
if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" {
|
||||
t.Error("Expected Capabilities to have a Kube version")
|
||||
}
|
||||
|
||||
vals := res["Values"].(Values)
|
||||
if vals["name"] != "Haroun" {
|
||||
t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals)
|
||||
}
|
||||
where := vals["where"].(map[string]interface{})
|
||||
expects := map[string]string{
|
||||
"city": "Baghdad",
|
||||
"date": "809 CE",
|
||||
"title": "caliph",
|
||||
}
|
||||
for field, expect := range expects {
|
||||
if got := where[field]; got != expect {
|
||||
t.Errorf("Expected %q, got %q (%v)", expect, got, where)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadValuesFile(t *testing.T) {
|
||||
data, err := ReadValuesFile("./testdata/coleridge.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
matchValues(t, data)
|
||||
}
|
||||
|
||||
func ExampleValues() {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ch1, err := d.Table("chapter.one")
|
||||
if err != nil {
|
||||
panic("could not find chapter one")
|
||||
}
|
||||
fmt.Print(ch1["title"])
|
||||
// Output:
|
||||
// Loomings
|
||||
}
|
||||
|
||||
func TestTable(t *testing.T) {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse the White Whale: %s", err)
|
||||
}
|
||||
|
||||
if _, err := d.Table("title"); err == nil {
|
||||
t.Fatalf("Title is not a table.")
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter"); err != nil {
|
||||
t.Fatalf("Failed to get the chapter table: %s\n%v", err, d)
|
||||
}
|
||||
|
||||
if v, err := d.Table("chapter.one"); err != nil {
|
||||
t.Errorf("Failed to get chapter.one: %s", err)
|
||||
} else if v["title"] != "Loomings" {
|
||||
t.Errorf("Unexpected title: %s", v["title"])
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter.three"); err != nil {
|
||||
t.Errorf("Chapter three is missing: %s\n%v", err, d)
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil {
|
||||
t.Errorf("I think you mean 'Epilogue'")
|
||||
}
|
||||
}
|
||||
|
||||
func matchValues(t *testing.T, data map[string]interface{}) {
|
||||
t.Helper()
|
||||
if data["poet"] != "Coleridge" {
|
||||
t.Errorf("Unexpected poet: %s", data["poet"])
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{len .stanza}}", data); err != nil {
|
||||
t.Errorf("len stanza: %s", err)
|
||||
} else if o != "6" {
|
||||
t.Errorf("Expected 6, got %s", o)
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{.mariner.shot}}", data); err != nil {
|
||||
t.Errorf(".mariner.shot: %s", err)
|
||||
} else if o != "ALBATROSS" {
|
||||
t.Errorf("Expected that mariner shot ALBATROSS")
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{.water.water.where}}", data); err != nil {
|
||||
t.Errorf(".water.water.where: %s", err)
|
||||
} else if o != "everywhere" {
|
||||
t.Errorf("Expected water water everywhere")
|
||||
}
|
||||
}
|
||||
|
||||
func ttpl(tpl string, v map[string]interface{}) (string, error) {
|
||||
var b bytes.Buffer
|
||||
tt := template.Must(template.New("t").Parse(tpl))
|
||||
err := tt.Execute(&b, v)
|
||||
return b.String(), err
|
||||
}
|
||||
|
||||
func TestPathValue(t *testing.T) {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse the White Whale: %s", err)
|
||||
}
|
||||
|
||||
if v, err := d.PathValue("chapter.one.title"); err != nil {
|
||||
t.Errorf("Got error instead of title: %s\n%v", err, d)
|
||||
} else if v != "Loomings" {
|
||||
t.Errorf("No error but got wrong value for title: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil {
|
||||
t.Errorf("Non-existent key should return error: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil {
|
||||
t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue(""); err == nil {
|
||||
t.Error("Asking for the value from an empty path should yield an error")
|
||||
}
|
||||
if v, err := d.PathValue("title"); err == nil {
|
||||
if v != "Moby Dick" {
|
||||
t.Errorf("Failed to return values for root key title")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// Config represents an plugin type specific configuration
|
||||
// It is expected to type assert (cast) the a Config to its expected underlying type (schema.ConfigCLIV1, schema.ConfigGetterV1, etc).
|
||||
type Config interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
func unmarshaConfig(pluginType string, configData map[string]any) (Config, error) {
|
||||
|
||||
pluginTypeMeta, ok := pluginTypesIndex[pluginType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown plugin type %q", pluginType)
|
||||
}
|
||||
|
||||
// TODO: Avoid (yaml) serialization/deserialization for type conversion here
|
||||
|
||||
data, err := yaml.Marshal(configData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshel config data (plugin type %s): %w", pluginType, err)
|
||||
}
|
||||
|
||||
config := reflect.New(pluginTypeMeta.configType)
|
||||
d := yaml.NewDecoder(bytes.NewReader(data))
|
||||
d.KnownFields(true)
|
||||
if err := d.Decode(config.Interface()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config.Interface().(Config), nil
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
|
||||
func TestUnmarshaConfig(t *testing.T) {
|
||||
// Test unmarshalling a CLI plugin config
|
||||
{
|
||||
config, err := unmarshaConfig("cli/v1", map[string]any{
|
||||
"usage": "usage string",
|
||||
"shortHelp": "short help string",
|
||||
"longHelp": "long help string",
|
||||
"ignoreFlags": true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.IsType(t, &schema.ConfigCLIV1{}, config)
|
||||
assert.Equal(t, schema.ConfigCLIV1{
|
||||
Usage: "usage string",
|
||||
ShortHelp: "short help string",
|
||||
LongHelp: "long help string",
|
||||
IgnoreFlags: true,
|
||||
}, *(config.(*schema.ConfigCLIV1)))
|
||||
}
|
||||
|
||||
// Test unmarshalling invalid config data
|
||||
{
|
||||
config, err := unmarshaConfig("cli/v1", map[string]any{
|
||||
"invalid field": "foo",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "field not found")
|
||||
assert.Nil(t, config)
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
---
|
||||
TODO: move this section to public plugin package
|
||||
|
||||
Package plugin provides the implementation of the Helm plugin system.
|
||||
|
||||
Conceptually, "plugins" enable extending Helm's functionality external to Helm's core codebase. The plugin system allows
|
||||
code to fetch plugins by type, then invoke the plugin with an input as required by that plugin type. The plugin
|
||||
returning an output for the caller to consume.
|
||||
|
||||
An example of a plugin invocation:
|
||||
```
|
||||
d := plugin.Descriptor{
|
||||
Type: "example/v1", //
|
||||
}
|
||||
plgs, err := plugin.FindPlugins([]string{settings.PluginsDirectory}, d)
|
||||
|
||||
for _, plg := range plgs {
|
||||
input := &plugin.Input{
|
||||
Message: schema.InputMessageExampleV1{ // The type of the input message is defined by the plugin's "type" (example/v1 here)
|
||||
...
|
||||
},
|
||||
}
|
||||
output, err := plg.Invoke(context.Background(), input)
|
||||
if err != nil {
|
||||
...
|
||||
}
|
||||
|
||||
// consume the output, using type assertion to convert to the expected output type (as defined by the plugin's "type")
|
||||
outputMessage, ok := output.Message.(schema.OutputMessageExampleV1)
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
Package `plugin` provides the implementation of the Helm plugin system.
|
||||
|
||||
Helm plugins are exposed to uses as the "Plugin" type, the basic interface that primarily support the "Invoke" method.
|
||||
|
||||
# Plugin Runtimes
|
||||
Internally, plugins must be implemented by a "runtime" that is responsible for creating the plugin instance, and dispatching the plugin's invocation to the plugin's implementation.
|
||||
For example:
|
||||
- forming environment variables and command line args for subprocess execution
|
||||
- converting input to JSON and invoking a function in a Wasm runtime
|
||||
|
||||
Internally, the code structure is:
|
||||
Runtime.CreatePlugin()
|
||||
|
|
||||
| (creates)
|
||||
|
|
||||
\---> PluginRuntime
|
||||
|
|
||||
| (implements)
|
||||
v
|
||||
Plugin.Invoke()
|
||||
|
||||
# Plugin Types
|
||||
Each plugin implements a specific functionality, denoted by the plugin's "type" e.g. "getter/v1". The "type" includes a version, in order to allow a given types messaging schema and invocation options to evolve.
|
||||
|
||||
Specifically, the plugin's "type" specifies the contract for the input and output messages that are expected to be passed to the plugin, and returned from the plugin. The plugin's "type" also defines the options that can be passed to the plugin when invoking it.
|
||||
|
||||
# Metadata
|
||||
Each plugin must have a `plugin.yaml`, that defines the plugin's metadata. The metadata includes the plugin's name, version, and other information.
|
||||
|
||||
For legacy plugins, the type is inferred by which fields are set on the plugin: a downloader plugin is inferred when metadata contains a "downloaders" yaml node, otherwise it is assumed to define a Helm CLI subcommand.
|
||||
|
||||
For v1 plugins, the metadata includes explicit apiVersion and type fields. It will also contain type-specific Config, and RuntimeConfig fields.
|
||||
|
||||
# Runtime and type cardinality
|
||||
From a cardinality perspective, this means there a "few" runtimes, and "many" plugins types. It is also expected that the subprocess runtime will not be extended to support extra plugin types, and deprecated in a future version of Helm.
|
||||
|
||||
Future ideas that are intended to be implemented include extending the plugin system to support future Wasm standards. Or allowing Helm SDK user's to inject "plugins" that are actually implemented as native go modules. Or even moving Helm's internal functionality e.g. yaml rendering engine to be used as an "in-built" plugin, along side other plugins that may implement other (non-go template) rendering engines.
|
||||
*/
|
||||
|
||||
package plugin
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 plugin
|
||||
|
||||
// InvokeExecError is returned when a plugin invocation returns a non-zero status/exit code
|
||||
// - subprocess plugin: child process exit code
|
||||
// - extism plugin: wasm function return code
|
||||
type InvokeExecError struct {
|
||||
ExitCode int // Exit code from plugin code execution
|
||||
Err error // Underlying error
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *InvokeExecError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue