mirror of https://github.com/helm/helm
Signed-off-by: zyfy29 <wasuremono127@gmail.com>pull/31207/head
commit
b24bfe4168
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package lint // import "helm.sh/helm/v4/internal/chart/v3/lint"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||||
|
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||||
|
"helm.sh/helm/v4/pkg/chart/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type linterOptions struct {
|
||||||
|
KubeVersion *common.KubeVersion
|
||||||
|
SkipSchemaValidation bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinterOption func(lo *linterOptions)
|
||||||
|
|
||||||
|
func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption {
|
||||||
|
return func(lo *linterOptions) {
|
||||||
|
lo.KubeVersion = kubeVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
|
||||||
|
return func(lo *linterOptions) {
|
||||||
|
lo.SkipSchemaValidation = skipSchemaValidation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter {
|
||||||
|
|
||||||
|
chartDir, _ := filepath.Abs(baseDir)
|
||||||
|
|
||||||
|
lo := linterOptions{}
|
||||||
|
for _, option := range options {
|
||||||
|
option(&lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := support.Linter{
|
||||||
|
ChartDir: chartDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.Chartfile(&result)
|
||||||
|
rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation)
|
||||||
|
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
|
||||||
|
rules.Dependencies(&result)
|
||||||
|
rules.Crds(&result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@ -0,0 +1,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,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||||
|
"helm.sh/helm/v4/pkg/chart/common"
|
||||||
|
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValuesWithOverrides tests the values.yaml file.
|
||||||
|
//
|
||||||
|
// If a schema is present in the chart, values are tested against that. Otherwise,
|
||||||
|
// they are only tested for well-formedness.
|
||||||
|
//
|
||||||
|
// If additional values are supplied, they are coalesced into the values in values.yaml.
|
||||||
|
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
|
||||||
|
file := "values.yaml"
|
||||||
|
vf := filepath.Join(linter.ChartDir, file)
|
||||||
|
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
|
||||||
|
|
||||||
|
if !fileExists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateValuesFileExistence(valuesPath string) error {
|
||||||
|
_, err := os.Stat(valuesPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file does not exist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
|
||||||
|
values, err := common.ReadValuesFile(valuesPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
|
||||||
|
// level values against the top-level expectations. Subchart values are not linted.
|
||||||
|
// We could change that. For now, though, we retain that strategy, and thus can
|
||||||
|
// coalesce tables (like reuse-values does) instead of doing the full chart
|
||||||
|
// CoalesceValues
|
||||||
|
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
|
||||||
|
coalescedValues = util.CoalesceTables(coalescedValues, values)
|
||||||
|
|
||||||
|
ext := filepath.Ext(valuesPath)
|
||||||
|
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
|
||||||
|
schema, err := os.ReadFile(schemaPath)
|
||||||
|
if len(schema) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipSchemaValidation {
|
||||||
|
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -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,308 +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"
|
|
||||||
"log"
|
|
||||||
"maps"
|
|
||||||
|
|
||||||
"github.com/mitchellh/copystructure"
|
|
||||||
|
|
||||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func concatPrefix(a, b string) string {
|
|
||||||
if a == "" {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s.%s", a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CoalesceValues coalesces all of the values in a chart (and its subcharts).
|
|
||||||
//
|
|
||||||
// Values are coalesced together using the following rules:
|
|
||||||
//
|
|
||||||
// - Values in a higher level chart always override values in a lower-level
|
|
||||||
// dependency chart
|
|
||||||
// - Scalar values and arrays are replaced, maps are merged
|
|
||||||
// - A chart has access to all of the variables for it, as well as all of
|
|
||||||
// the values destined for its dependencies.
|
|
||||||
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
|
|
||||||
valsCopy, err := copyValues(vals)
|
|
||||||
if err != nil {
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
return coalesce(log.Printf, chrt, valsCopy, "", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeValues is used to merge the values in a chart and its subcharts. This
|
|
||||||
// is different from Coalescing as nil/null values are preserved.
|
|
||||||
//
|
|
||||||
// Values are coalesced together using the following rules:
|
|
||||||
//
|
|
||||||
// - Values in a higher level chart always override values in a lower-level
|
|
||||||
// dependency chart
|
|
||||||
// - Scalar values and arrays are replaced, maps are merged
|
|
||||||
// - A chart has access to all of the variables for it, as well as all of
|
|
||||||
// the values destined for its dependencies.
|
|
||||||
//
|
|
||||||
// Retaining Nils is useful when processes early in a Helm action or business
|
|
||||||
// logic need to retain them for when Coalescing will happen again later in the
|
|
||||||
// business logic.
|
|
||||||
func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
|
|
||||||
valsCopy, err := copyValues(vals)
|
|
||||||
if err != nil {
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
return coalesce(log.Printf, chrt, valsCopy, "", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyValues(vals map[string]interface{}) (Values, error) {
|
|
||||||
v, err := copystructure.Copy(vals)
|
|
||||||
if err != nil {
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
|
|
||||||
valsCopy := v.(map[string]interface{})
|
|
||||||
// if we have an empty map, make sure it is initialized
|
|
||||||
if valsCopy == nil {
|
|
||||||
valsCopy = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return valsCopy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type printFn func(format string, v ...interface{})
|
|
||||||
|
|
||||||
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
|
|
||||||
//
|
|
||||||
// This is a helper function for CoalesceValues and MergeValues.
|
|
||||||
//
|
|
||||||
// Note, the merge argument specifies whether this is being used by MergeValues
|
|
||||||
// or CoalesceValues. Coalescing removes null values and their keys in some
|
|
||||||
// situations while merging keeps the null values.
|
|
||||||
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
|
|
||||||
coalesceValues(printf, ch, dest, prefix, merge)
|
|
||||||
return coalesceDeps(printf, ch, dest, prefix, merge)
|
|
||||||
}
|
|
||||||
|
|
||||||
// coalesceDeps coalesces the dependencies of the given chart.
|
|
||||||
func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
|
|
||||||
for _, subchart := range chrt.Dependencies() {
|
|
||||||
if c, ok := dest[subchart.Name()]; !ok {
|
|
||||||
// If dest doesn't already have the key, create it.
|
|
||||||
dest[subchart.Name()] = make(map[string]interface{})
|
|
||||||
} else if !istable(c) {
|
|
||||||
return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c)
|
|
||||||
}
|
|
||||||
if dv, ok := dest[subchart.Name()]; ok {
|
|
||||||
dvmap := dv.(map[string]interface{})
|
|
||||||
subPrefix := concatPrefix(prefix, chrt.Metadata.Name)
|
|
||||||
// Get globals out of dest and merge them into dvmap.
|
|
||||||
coalesceGlobals(printf, dvmap, dest, subPrefix, merge)
|
|
||||||
// Now coalesce the rest of the values.
|
|
||||||
var err error
|
|
||||||
dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge)
|
|
||||||
if err != nil {
|
|
||||||
return dest, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// coalesceGlobals copies the globals out of src and merges them into dest.
|
|
||||||
//
|
|
||||||
// For convenience, returns dest.
|
|
||||||
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) {
|
|
||||||
var dg, sg map[string]interface{}
|
|
||||||
|
|
||||||
if destglob, ok := dest[GlobalKey]; !ok {
|
|
||||||
dg = make(map[string]interface{})
|
|
||||||
} else if dg, ok = destglob.(map[string]interface{}); !ok {
|
|
||||||
printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if srcglob, ok := src[GlobalKey]; !ok {
|
|
||||||
sg = make(map[string]interface{})
|
|
||||||
} else if sg, ok = srcglob.(map[string]interface{}); !ok {
|
|
||||||
printf("warning: skipping globals because source %s is not a table.", GlobalKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
|
|
||||||
// reverses that decision. It may somehow be possible to introduce a loop
|
|
||||||
// here, but I haven't found a way. So for the time being, let's allow
|
|
||||||
// tables in globals.
|
|
||||||
for key, val := range sg {
|
|
||||||
if istable(val) {
|
|
||||||
vv := copyMap(val.(map[string]interface{}))
|
|
||||||
if destv, ok := dg[key]; !ok {
|
|
||||||
// Here there is no merge. We're just adding.
|
|
||||||
dg[key] = vv
|
|
||||||
} else {
|
|
||||||
if destvmap, ok := destv.(map[string]interface{}); !ok {
|
|
||||||
printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
|
|
||||||
} else {
|
|
||||||
// Basically, we reverse order of coalesce here to merge
|
|
||||||
// top-down.
|
|
||||||
subPrefix := concatPrefix(prefix, key)
|
|
||||||
// In this location coalesceTablesFullKey should always have
|
|
||||||
// merge set to true. The output of coalesceGlobals is run
|
|
||||||
// through coalesce where any nils will be removed.
|
|
||||||
coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true)
|
|
||||||
dg[key] = vv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if dv, ok := dg[key]; ok && istable(dv) {
|
|
||||||
// It's not clear if this condition can actually ever trigger.
|
|
||||||
printf("key %s is table. Skipping", key)
|
|
||||||
} else {
|
|
||||||
// TODO: Do we need to do any additional checking on the value?
|
|
||||||
dg[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dest[GlobalKey] = dg
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMap(src map[string]interface{}) map[string]interface{} {
|
|
||||||
m := make(map[string]interface{}, len(src))
|
|
||||||
maps.Copy(m, src)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// coalesceValues builds up a values map for a particular chart.
|
|
||||||
//
|
|
||||||
// Values in v will override the values in the chart.
|
|
||||||
func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) {
|
|
||||||
subPrefix := concatPrefix(prefix, c.Metadata.Name)
|
|
||||||
|
|
||||||
// Using c.Values directly when coalescing a table can cause problems where
|
|
||||||
// the original c.Values is altered. Creating a deep copy stops the problem.
|
|
||||||
// This section is fault-tolerant as there is no ability to return an error.
|
|
||||||
valuesCopy, err := copystructure.Copy(c.Values)
|
|
||||||
var vc map[string]interface{}
|
|
||||||
var ok bool
|
|
||||||
if err != nil {
|
|
||||||
// If there is an error something is wrong with copying c.Values it
|
|
||||||
// means there is a problem in the deep copying package or something
|
|
||||||
// wrong with c.Values. In this case we will use c.Values and report
|
|
||||||
// an error.
|
|
||||||
printf("warning: unable to copy values, err: %s", err)
|
|
||||||
vc = c.Values
|
|
||||||
} else {
|
|
||||||
vc, ok = valuesCopy.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
// c.Values has a map[string]interface{} structure. If the copy of
|
|
||||||
// it cannot be treated as map[string]interface{} there is something
|
|
||||||
// strangely wrong. Log it and use c.Values
|
|
||||||
printf("warning: unable to convert values copy to values type")
|
|
||||||
vc = c.Values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, val := range vc {
|
|
||||||
if value, ok := v[key]; ok {
|
|
||||||
if value == nil && !merge {
|
|
||||||
// When the YAML value is null and we are coalescing instead of
|
|
||||||
// merging, we remove the value's key.
|
|
||||||
// This allows Helm's various sources of values (value files or --set) to
|
|
||||||
// remove incompatible keys from any previous chart, file, or set values.
|
|
||||||
delete(v, key)
|
|
||||||
} else if dest, ok := value.(map[string]interface{}); ok {
|
|
||||||
// if v[key] is a table, merge nv's val table into v[key].
|
|
||||||
src, ok := val.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
// If the original value is nil, there is nothing to coalesce, so we don't print
|
|
||||||
// the warning
|
|
||||||
if val != nil {
|
|
||||||
printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the key is a child chart, coalesce tables with Merge set to true
|
|
||||||
merge := childChartMergeTrue(c, key, merge)
|
|
||||||
|
|
||||||
// Because v has higher precedence than nv, dest values override src
|
|
||||||
// values.
|
|
||||||
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the key is not in v, copy it from nv.
|
|
||||||
v[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool {
|
|
||||||
for _, subchart := range chrt.Dependencies() {
|
|
||||||
if subchart.Name() == key {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merge
|
|
||||||
}
|
|
||||||
|
|
||||||
// CoalesceTables merges a source map into a destination map.
|
|
||||||
//
|
|
||||||
// dest is considered authoritative.
|
|
||||||
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
|
|
||||||
return coalesceTablesFullKey(log.Printf, dst, src, "", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeTables(dst, src map[string]interface{}) map[string]interface{} {
|
|
||||||
return coalesceTablesFullKey(log.Printf, dst, src, "", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// coalesceTablesFullKey merges a source map into a destination map.
|
|
||||||
//
|
|
||||||
// dest is considered authoritative.
|
|
||||||
func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} {
|
|
||||||
// When --reuse-values is set but there are no modifications yet, return new values
|
|
||||||
if src == nil {
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
if dst == nil {
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
for key, val := range dst {
|
|
||||||
if val == nil {
|
|
||||||
src[key] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Because dest has higher precedence than src, dest values override src
|
|
||||||
// values.
|
|
||||||
for key, val := range src {
|
|
||||||
fullkey := concatPrefix(prefix, key)
|
|
||||||
if dv, ok := dst[key]; ok && !merge && dv == nil {
|
|
||||||
delete(dst, key)
|
|
||||||
} else if !ok {
|
|
||||||
dst[key] = val
|
|
||||||
} else if istable(val) {
|
|
||||||
if istable(dv) {
|
|
||||||
coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge)
|
|
||||||
} else {
|
|
||||||
printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val)
|
|
||||||
}
|
|
||||||
} else if istable(dv) && val != nil {
|
|
||||||
printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
@ -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,113 +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"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
||||||
|
|
||||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema
|
|
||||||
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
|
|
||||||
var sb strings.Builder
|
|
||||||
if chrt.Schema != nil {
|
|
||||||
slog.Debug("chart name", "chart-name", chrt.Name())
|
|
||||||
err := ValidateAgainstSingleSchema(values, chrt.Schema)
|
|
||||||
if err != nil {
|
|
||||||
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
|
|
||||||
sb.WriteString(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies()))
|
|
||||||
// For each dependency, recursively call this function with the coalesced values
|
|
||||||
for _, subchart := range chrt.Dependencies() {
|
|
||||||
subchartValues := values[subchart.Name()].(map[string]interface{})
|
|
||||||
if err := ValidateAgainstSchema(subchart, subchartValues); err != nil {
|
|
||||||
sb.WriteString(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sb.Len() > 0 {
|
|
||||||
return errors.New(sb.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
|
|
||||||
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
reterr = fmt.Errorf("unable to validate schema: %s", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// This unmarshal function leverages UseNumber() for number precision. The parser
|
|
||||||
// used for values does this as well.
|
|
||||||
schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
slog.Debug("unmarshalled JSON schema", "schema", schemaJSON)
|
|
||||||
|
|
||||||
compiler := jsonschema.NewCompiler()
|
|
||||||
err = compiler.AddResource("file:///values.schema.json", schema)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
validator, err := compiler.Compile("file:///values.schema.json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validator.Validate(values.AsMap())
|
|
||||||
if err != nil {
|
|
||||||
return JSONSchemaValidationError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note, JSONSchemaValidationError is used to wrap the error from the underlying
|
|
||||||
// validation package so that Helm has a clean interface and the validation package
|
|
||||||
// could be replaced without changing the Helm SDK API.
|
|
||||||
|
|
||||||
// JSONSchemaValidationError is the error returned when there is a schema validation
|
|
||||||
// error.
|
|
||||||
type JSONSchemaValidationError struct {
|
|
||||||
embeddedErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error prints the error message
|
|
||||||
func (e JSONSchemaValidationError) Error() string {
|
|
||||||
errStr := e.embeddedErr.Error()
|
|
||||||
|
|
||||||
// This string prefixes all of our error details. Further up the stack of helm error message
|
|
||||||
// building more detail is provided to users. This is removed.
|
|
||||||
errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n")
|
|
||||||
|
|
||||||
// The extra new line is needed for when there are sub-charts.
|
|
||||||
return errStr + "\n"
|
|
||||||
}
|
|
@ -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,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,195 @@
|
|||||||
|
/*
|
||||||
|
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 installer // import "helm.sh/helm/v4/internal/plugin/installer"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TarGzExtractor extracts gzip compressed tar archives
|
||||||
|
type TarGzExtractor struct{}
|
||||||
|
|
||||||
|
// Extractor provides an interface for extracting archives
|
||||||
|
type Extractor interface {
|
||||||
|
Extract(buffer *bytes.Buffer, targetDir string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extractors contains a map of suffixes and matching implementations of extractor to return
|
||||||
|
var Extractors = map[string]Extractor{
|
||||||
|
".tar.gz": &TarGzExtractor{},
|
||||||
|
".tgz": &TarGzExtractor{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a media type to an extractor extension.
|
||||||
|
//
|
||||||
|
// This should be refactored in Helm 4, combined with the extension-based mechanism.
|
||||||
|
func mediaTypeToExtension(mt string) (string, bool) {
|
||||||
|
switch strings.ToLower(mt) {
|
||||||
|
case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":
|
||||||
|
return ".tgz", true
|
||||||
|
case "application/octet-stream":
|
||||||
|
// Generic binary type - we'll need to check the URL suffix
|
||||||
|
return "", false
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExtractor creates a new extractor matching the source file name
|
||||||
|
func NewExtractor(source string) (Extractor, error) {
|
||||||
|
for suffix, extractor := range Extractors {
|
||||||
|
if strings.HasSuffix(source, suffix) {
|
||||||
|
return extractor, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no extractor implemented yet for %s", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanJoin resolves dest as a subpath of root.
|
||||||
|
//
|
||||||
|
// This function runs several security checks on the path, generating an error if
|
||||||
|
// the supplied `dest` looks suspicious or would result in dubious behavior on the
|
||||||
|
// filesystem.
|
||||||
|
//
|
||||||
|
// cleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt
|
||||||
|
// to be malicious. (If you don't care about this, use the securejoin-filepath library.)
|
||||||
|
// It will emit an error if it detects paths that _look_ malicious, operating on the
|
||||||
|
// assumption that we don't actually want to do anything with files that already
|
||||||
|
// appear to be nefarious.
|
||||||
|
//
|
||||||
|
// - The character `:` is considered illegal because it is a separator on UNIX and a
|
||||||
|
// drive designator on Windows.
|
||||||
|
// - The path component `..` is considered suspicions, and therefore illegal
|
||||||
|
// - The character \ (backslash) is treated as a path separator and is converted to /.
|
||||||
|
// - Beginning a path with a path separator is illegal
|
||||||
|
// - Rudimentary symlink protects are offered by SecureJoin.
|
||||||
|
func cleanJoin(root, dest string) (string, error) {
|
||||||
|
|
||||||
|
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator.
|
||||||
|
// In neither case do we want to trust a TAR that contains these.
|
||||||
|
if strings.Contains(dest, ":") {
|
||||||
|
return "", errors.New("path contains ':', which is illegal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Go tar library does not convert separators for us.
|
||||||
|
// We assume here, as we do elsewhere, that `\\` means a Windows path.
|
||||||
|
dest = strings.ReplaceAll(dest, "\\", "/")
|
||||||
|
|
||||||
|
// We want to alert the user that something bad was attempted. Cleaning it
|
||||||
|
// is not a good practice.
|
||||||
|
if slices.Contains(strings.Split(dest, "/"), "..") {
|
||||||
|
return "", errors.New("path contains '..', which is illegal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a path is absolute, the creator of the TAR is doing something shady.
|
||||||
|
if path.IsAbs(dest) {
|
||||||
|
return "", errors.New("path is absolute, which is illegal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureJoin will do some cleaning, as well as some rudimentary checking of symlinks.
|
||||||
|
// The directory needs to be cleaned prior to passing to SecureJoin or the location may end up
|
||||||
|
// being wrong or returning an error. This was introduced in v0.4.0.
|
||||||
|
root = filepath.Clean(root)
|
||||||
|
newpath, err := securejoin.SecureJoin(root, dest)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.ToSlash(newpath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract extracts compressed archives
|
||||||
|
//
|
||||||
|
// Implements Extractor.
|
||||||
|
func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
|
||||||
|
uncompressedStream, err := gzip.NewReader(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(uncompressedStream)
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := cleanJoin(targetDir, header.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
// Ensure parent directory exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
outFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
// We don't want to process these extension header files.
|
||||||
|
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripPluginName is a helper that relies on some sort of convention for plugin name (plugin-name-<version>)
|
||||||
|
func stripPluginName(name string) string {
|
||||||
|
var strippedName string
|
||||||
|
for suffix := range Extractors {
|
||||||
|
if before, ok := strings.CutSuffix(name, suffix); ok {
|
||||||
|
strippedName = before
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`(.*)-[0-9]+\..*`)
|
||||||
|
return re.ReplaceAllString(strippedName, `$1`)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue