mirror of https://github.com/helm/helm
Merge pull request #31225 from mattfarina/move-lint-to-chart
Move lint pkg to be part of each chart versionpull/31128/merge v4.0.0-alpha.1
commit
618b14a772
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package lint // import "helm.sh/helm/v4/internal/chart/v3/lint"
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
type linterOptions struct {
|
||||
KubeVersion *common.KubeVersion
|
||||
SkipSchemaValidation bool
|
||||
}
|
||||
|
||||
type LinterOption func(lo *linterOptions)
|
||||
|
||||
func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption {
|
||||
return func(lo *linterOptions) {
|
||||
lo.KubeVersion = kubeVersion
|
||||
}
|
||||
}
|
||||
|
||||
func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
|
||||
return func(lo *linterOptions) {
|
||||
lo.SkipSchemaValidation = skipSchemaValidation
|
||||
}
|
||||
}
|
||||
|
||||
func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter {
|
||||
|
||||
chartDir, _ := filepath.Abs(baseDir)
|
||||
|
||||
lo := linterOptions{}
|
||||
for _, option := range options {
|
||||
option(&lo)
|
||||
}
|
||||
|
||||
result := support.Linter{
|
||||
ChartDir: chartDir,
|
||||
}
|
||||
|
||||
rules.Chartfile(&result)
|
||||
rules.ValuesWithOverrides(&result, values)
|
||||
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
|
||||
rules.Dependencies(&result)
|
||||
rules.Crds(&result)
|
||||
|
||||
return result
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package lint
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
var values map[string]interface{}
|
||||
|
||||
const namespace = "testNamespace"
|
||||
|
||||
const badChartDir = "rules/testdata/badchartfile"
|
||||
const badValuesFileDir = "rules/testdata/badvaluesfile"
|
||||
const badYamlFileDir = "rules/testdata/albatross"
|
||||
const badCrdFileDir = "rules/testdata/badcrdfile"
|
||||
const goodChartDir = "rules/testdata/goodone"
|
||||
const subChartValuesDir = "rules/testdata/withsubchart"
|
||||
const malformedTemplate = "rules/testdata/malformed-template"
|
||||
const invalidChartFileDir = "rules/testdata/invalidchartfile"
|
||||
|
||||
func TestBadChartV3(t *testing.T) {
|
||||
m := RunAll(badChartDir, values, namespace).Messages
|
||||
if len(m) != 8 {
|
||||
t.Errorf("Number of errors %v", len(m))
|
||||
t.Errorf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
// There should be one INFO, one WARNING, and 2 ERROR messages, check for them
|
||||
var i, w, e, e2, e3, e4, e5, e6 bool
|
||||
for _, msg := range m {
|
||||
if msg.Severity == support.InfoSev {
|
||||
if strings.Contains(msg.Err.Error(), "icon is recommended") {
|
||||
i = true
|
||||
}
|
||||
}
|
||||
if msg.Severity == support.WarningSev {
|
||||
if strings.Contains(msg.Err.Error(), "does not exist") {
|
||||
w = true
|
||||
}
|
||||
}
|
||||
if msg.Severity == support.ErrorSev {
|
||||
if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") {
|
||||
e = true
|
||||
}
|
||||
if strings.Contains(msg.Err.Error(), "name is required") {
|
||||
e2 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") {
|
||||
e3 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") {
|
||||
e4 = true
|
||||
}
|
||||
|
||||
if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") {
|
||||
e5 = true
|
||||
}
|
||||
// This comes from the dependency check, which loads dependency info from the Chart.yaml
|
||||
if strings.Contains(msg.Err.Error(), "unable to load chart") {
|
||||
e6 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w {
|
||||
t.Errorf("Didn't find all the expected errors, got %#v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidYaml(t *testing.T) {
|
||||
m := RunAll(badYamlFileDir, values, namespace).Messages
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
|
||||
t.Errorf("All didn't have the error for deliberateSyntaxError")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidChartYamlV3(t *testing.T) {
|
||||
m := RunAll(invalidChartFileDir, values, namespace).Messages
|
||||
t.Log(m)
|
||||
if len(m) != 3 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") {
|
||||
t.Errorf("All didn't have the error for duplicate YAML keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadValuesV3(t *testing.T) {
|
||||
m := RunAll(badValuesFileDir, values, namespace).Messages
|
||||
if len(m) < 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") {
|
||||
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadCrdFileV3(t *testing.T) {
|
||||
m := RunAll(badCrdFileDir, values, namespace).Messages
|
||||
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
|
||||
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
|
||||
assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'")
|
||||
}
|
||||
|
||||
func TestGoodChart(t *testing.T) {
|
||||
m := RunAll(goodChartDir, values, namespace).Messages
|
||||
if len(m) != 0 {
|
||||
t.Error("All returned linter messages when it shouldn't have")
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7923
|
||||
func TestHelmCreateChart(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
// Fatal is bad because of the defer.
|
||||
return
|
||||
}
|
||||
|
||||
// Note: we test with strict=true here, even though others have
|
||||
// strict = false.
|
||||
m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages
|
||||
if ll := len(m); ll != 1 {
|
||||
t.Errorf("All should have had exactly 1 error. Got %d", ll)
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg.Error())
|
||||
}
|
||||
} else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") {
|
||||
t.Errorf("Unexpected lint error: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws
|
||||
// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags).
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/11495
|
||||
//
|
||||
// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
|
||||
// of the `--set` flag.
|
||||
//
|
||||
// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive
|
||||
// results.
|
||||
// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=<k8s-major-version>
|
||||
// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=<k8s-minor-version>
|
||||
// or directly use '$(LDFLAGS)' in Makefile.
|
||||
//
|
||||
// When run without ldflags, the test passes giving a false-positive result. This is because the variables
|
||||
// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there
|
||||
// might not be the deprecation warning.
|
||||
func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
|
||||
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add values to enable hpa, and ingress which are disabled by default.
|
||||
// This is the equivalent of:
|
||||
// helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true'
|
||||
updatedValues := map[string]interface{}{
|
||||
"autoscaling": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
"ingress": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
}
|
||||
|
||||
linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true))
|
||||
for _, msg := range linterRunDetails.Messages {
|
||||
if strings.HasPrefix(msg.Error(), "[WARNING]") &&
|
||||
strings.Contains(msg.Error(), "deprecated") {
|
||||
// When there is a deprecation warning for an object created
|
||||
// by `helm create` for the current Kubernetes version, fail.
|
||||
t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lint ignores import-values
|
||||
// See https://github.com/helm/helm/issues/9658
|
||||
func TestSubChartValuesChart(t *testing.T) {
|
||||
m := RunAll(subChartValuesDir, values, namespace).Messages
|
||||
if len(m) != 0 {
|
||||
t.Error("All returned linter messages when it shouldn't have")
|
||||
for i, msg := range m {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lint stuck with malformed template object
|
||||
// See https://github.com/helm/helm/issues/11391
|
||||
func TestMalformedTemplate(t *testing.T) {
|
||||
c := time.After(3 * time.Second)
|
||||
ch := make(chan int, 1)
|
||||
var m []support.Message
|
||||
go func() {
|
||||
m = RunAll(malformedTemplate, values, namespace).Messages
|
||||
ch <- 1
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
t.Fatalf("lint malformed template timeout")
|
||||
case <-ch:
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("All didn't fail with expected errors, got %#v", m)
|
||||
}
|
||||
if !strings.Contains(m[0].Err.Error(), "invalid character '{'") {
|
||||
t.Errorf("All didn't have the error for invalid character '{'")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
// Chartfile runs a set of linter rules related to Chart.yaml file
|
||||
func Chartfile(linter *support.Linter) {
|
||||
chartFileName := "Chart.yaml"
|
||||
chartPath := filepath.Join(linter.ChartDir, chartFileName)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
|
||||
|
||||
chartFile, err := chartutil.LoadChartfile(chartPath)
|
||||
validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
|
||||
|
||||
// Guard clause. Following linter rules require a parsable ChartFile
|
||||
if !validChartFile {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = chartutil.StrictLoadChartfile(chartPath)
|
||||
linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err))
|
||||
|
||||
// type check for Chart.yaml . ignoring error as any parse
|
||||
// errors would already be caught in the above load function
|
||||
chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
|
||||
|
||||
// Chart metadata
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
|
||||
linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
|
||||
}
|
||||
|
||||
func validateChartVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "version")
|
||||
}
|
||||
|
||||
func validateChartAppVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "appVersion")
|
||||
}
|
||||
|
||||
func isStringValue(data map[string]interface{}, key string) error {
|
||||
value, ok := data[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
valueType := fmt.Sprintf("%T", value)
|
||||
if valueType != "string" {
|
||||
return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlNotDirectory(chartPath string) error {
|
||||
fi, err := os.Stat(chartPath)
|
||||
|
||||
if err == nil && fi.IsDir() {
|
||||
return errors.New("should be a file, not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlStrictFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartName(cf *chart.Metadata) error {
|
||||
if cf.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
name := filepath.Base(cf.Name)
|
||||
if name != cf.Name {
|
||||
return fmt.Errorf("chart name %q is invalid", cf.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartAPIVersion(cf *chart.Metadata) error {
|
||||
if cf.APIVersion == "" {
|
||||
return errors.New("apiVersion is required. The value must be \"v3\"")
|
||||
}
|
||||
|
||||
if cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartVersion(cf *chart.Metadata) error {
|
||||
if cf.Version == "" {
|
||||
return errors.New("version is required")
|
||||
}
|
||||
|
||||
version, err := semver.NewVersion(cf.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("version '%s' is not a valid SemVer", 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,276 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
badChartNameDir = "testdata/badchartname"
|
||||
badChartDir = "testdata/badchartfile"
|
||||
anotherBadChartDir = "testdata/anotherbadchartfile"
|
||||
)
|
||||
|
||||
var (
|
||||
badChartNamePath = filepath.Join(badChartNameDir, "Chart.yaml")
|
||||
badChartFilePath = filepath.Join(badChartDir, "Chart.yaml")
|
||||
nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml")
|
||||
)
|
||||
|
||||
var badChart, _ = chartutil.LoadChartfile(badChartFilePath)
|
||||
var badChartName, _ = chartutil.LoadChartfile(badChartNamePath)
|
||||
|
||||
// Validation functions Test
|
||||
func TestValidateChartYamlNotDirectory(t *testing.T) {
|
||||
_ = os.Mkdir(nonExistingChartFilePath, os.ModePerm)
|
||||
defer os.Remove(nonExistingChartFilePath)
|
||||
|
||||
err := validateChartYamlNotDirectory(nonExistingChartFilePath)
|
||||
if err == nil {
|
||||
t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartYamlFormat(t *testing.T) {
|
||||
err := validateChartYamlFormat(errors.New("Read error"))
|
||||
if err == nil {
|
||||
t.Errorf("validateChartYamlFormat to return a linter error, got no error")
|
||||
}
|
||||
|
||||
err = validateChartYamlFormat(nil)
|
||||
if err != nil {
|
||||
t.Errorf("validateChartYamlFormat to return no error, got a linter error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartName(t *testing.T) {
|
||||
err := validateChartName(badChart)
|
||||
if err == nil {
|
||||
t.Errorf("validateChartName to return a linter error, got no error")
|
||||
}
|
||||
|
||||
err = validateChartName(badChartName)
|
||||
if err == nil {
|
||||
t.Error("expected validateChartName to return a linter error for an invalid name, got no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartVersion(t *testing.T) {
|
||||
var failTest = []struct {
|
||||
Version string
|
||||
ErrorMsg string
|
||||
}{
|
||||
{"", "version is required"},
|
||||
{"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"},
|
||||
{"waps", "'waps' is not a valid SemVer"},
|
||||
{"-3", "'-3' is not a valid SemVer"},
|
||||
}
|
||||
|
||||
var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
|
||||
|
||||
for _, test := range failTest {
|
||||
badChart.Version = test.Version
|
||||
err := validateChartVersion(badChart)
|
||||
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
|
||||
t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, version := range successTest {
|
||||
badChart.Version = version
|
||||
err := validateChartVersion(badChart)
|
||||
if err != nil {
|
||||
t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartMaintainer(t *testing.T) {
|
||||
var failTest = []struct {
|
||||
Name string
|
||||
Email string
|
||||
ErrorMsg string
|
||||
}{
|
||||
{"", "", "each maintainer requires a name"},
|
||||
{"", "test@test.com", "each maintainer requires a name"},
|
||||
{"John Snow", "wrongFormatEmail.com", "invalid email"},
|
||||
}
|
||||
|
||||
var successTest = []struct {
|
||||
Name string
|
||||
Email string
|
||||
}{
|
||||
{"John Snow", ""},
|
||||
{"John Snow", "john@winterfell.com"},
|
||||
}
|
||||
|
||||
for _, test := range failTest {
|
||||
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
|
||||
err := validateChartMaintainer(badChart)
|
||||
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
|
||||
t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range successTest {
|
||||
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
|
||||
err := validateChartMaintainer(badChart)
|
||||
if err != nil {
|
||||
t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Testing for an empty maintainer
|
||||
badChart.Maintainers = []*chart.Maintainer{nil}
|
||||
err := validateChartMaintainer(badChart)
|
||||
if err == nil {
|
||||
t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected")
|
||||
}
|
||||
if err.Error() != "a maintainer entry is empty" {
|
||||
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartSources(t *testing.T) {
|
||||
var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"}
|
||||
var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"}
|
||||
for _, test := range failTest {
|
||||
badChart.Sources = []string{test}
|
||||
err := validateChartSources(badChart)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid source URL") {
|
||||
t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range successTest {
|
||||
badChart.Sources = []string{test}
|
||||
err := validateChartSources(badChart)
|
||||
if err != nil {
|
||||
t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChartIconPresence(t *testing.T) {
|
||||
t.Run("Icon absent", func(t *testing.T) {
|
||||
testChart := &chart.Metadata{
|
||||
Icon: "",
|
||||
}
|
||||
|
||||
err := validateChartIconPresence(testChart)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("validateChartIconPresence to return a linter error, got no error")
|
||||
} else if !strings.Contains(err.Error(), "icon is recommended") {
|
||||
t.Errorf("expected %q, got %q", "icon is recommended", err.Error())
|
||||
}
|
||||
})
|
||||
t.Run("Icon present", func(t *testing.T) {
|
||||
testChart := &chart.Metadata{
|
||||
Icon: "http://example.org/icon.png",
|
||||
}
|
||||
|
||||
err := validateChartIconPresence(testChart)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %q", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateChartIconURL(t *testing.T) {
|
||||
var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"}
|
||||
var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"}
|
||||
for _, test := range failTest {
|
||||
badChart.Icon = test
|
||||
err := validateChartIconURL(badChart)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid icon URL") {
|
||||
t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range successTest {
|
||||
badChart.Icon = test
|
||||
err := validateChartSources(badChart)
|
||||
if err != nil {
|
||||
t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestV3Chartfile(t *testing.T) {
|
||||
t.Run("Chart.yaml basic validity issues", func(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: badChartDir}
|
||||
Chartfile(&linter)
|
||||
msgs := linter.Messages
|
||||
expectedNumberOfErrorMessages := 6
|
||||
|
||||
if len(msgs) != expectedNumberOfErrorMessages {
|
||||
t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[0].Err.Error(), "name is required") {
|
||||
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") {
|
||||
t.Errorf("Unexpected message 1: %s", msgs[1].Err)
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") {
|
||||
t.Errorf("Unexpected message 2: %s", msgs[2].Err)
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") {
|
||||
t.Errorf("Unexpected message 3: %s", msgs[3].Err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: anotherBadChartDir}
|
||||
Chartfile(&linter)
|
||||
msgs := linter.Messages
|
||||
expectedNumberOfErrorMessages := 3
|
||||
|
||||
if len(msgs) != expectedNumberOfErrorMessages {
|
||||
t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") {
|
||||
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") {
|
||||
t.Errorf("Unexpected message 1: %s", msgs[1].Err)
|
||||
}
|
||||
|
||||
if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") {
|
||||
t.Errorf("Unexpected message 2: %s", msgs[2].Err)
|
||||
}
|
||||
})
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Crds lints the CRDs in the Linter.
|
||||
func Crds(linter *support.Linter) {
|
||||
fpath := "crds/"
|
||||
crdsPath := filepath.Join(linter.ChartDir, fpath)
|
||||
|
||||
// crds directory is optional
|
||||
if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
|
||||
crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath))
|
||||
if !crdsDirValid {
|
||||
return
|
||||
}
|
||||
|
||||
// Load chart and parse CRDs
|
||||
chart, err := loader.Load(linter.ChartDir)
|
||||
|
||||
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !chartLoaded {
|
||||
return
|
||||
}
|
||||
|
||||
/* Iterate over all the CRDs to check:
|
||||
1. It is a YAML file and not a template
|
||||
2. The API version is apiextensions.k8s.io
|
||||
3. The kind is CustomResourceDefinition
|
||||
*/
|
||||
for _, crd := range chart.CRDObjects() {
|
||||
fileName := crd.Name
|
||||
fpath = fileName
|
||||
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096)
|
||||
for {
|
||||
var yamlStruct *k8sYamlStruct
|
||||
|
||||
err := decoder.Decode(&yamlStruct)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
// If YAML parsing fails here, it will always fail in the next block as well, so we should return here.
|
||||
// This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct.
|
||||
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
func validateCrdsDir(crdsPath string) error {
|
||||
fi, err := os.Stat(crdsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdAPIVersion(obj *k8sYamlStruct) error {
|
||||
if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") {
|
||||
return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdKind(obj *k8sYamlStruct) error {
|
||||
if obj.Kind != "CustomResourceDefinition" {
|
||||
return fmt.Errorf("object kind is not 'CustomResourceDefinition'")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
)
|
||||
|
||||
const invalidCrdsDir = "./testdata/invalidcrdsdir"
|
||||
|
||||
func TestInvalidCrdsDir(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: invalidCrdsDir}
|
||||
Crds(&linter)
|
||||
res := linter.Messages
|
||||
|
||||
assert.Len(t, res, 1)
|
||||
assert.ErrorContains(t, res[0].Err, "not a directory")
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Dependencies runs lints against a chart's dependencies
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7910
|
||||
func Dependencies(linter *support.Linter) {
|
||||
c, err := loader.LoadDir(linter.ChartDir)
|
||||
if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
|
||||
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
|
||||
}
|
||||
|
||||
func validateChartFormat(chartError error) error {
|
||||
if chartError != nil {
|
||||
return fmt.Errorf("unable to load chart\n\t%w", chartError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDependencyInChartsDir(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Dependencies() {
|
||||
dependencies[dep.Metadata.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
if _, ok := dependencies[dep.Name]; !ok {
|
||||
missing = append(missing, dep.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependencyInMetadata(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
dependencies[dep.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Dependencies() {
|
||||
if _, ok := dependencies[dep.Metadata.Name]; !ok {
|
||||
missing = append(missing, dep.Metadata.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependenciesUnique(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]*chart.Dependency{}
|
||||
shadowing := []string{}
|
||||
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
key := dep.Name
|
||||
if dep.Alias != "" {
|
||||
key = dep.Alias
|
||||
}
|
||||
if dependencies[key] != nil {
|
||||
shadowing = append(shadowing, key)
|
||||
}
|
||||
dependencies[key] = dep
|
||||
}
|
||||
if len(shadowing) > 0 {
|
||||
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package rules
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
func chartWithBadDependencies() chart.Chart {
|
||||
badChartDeps := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "sub2",
|
||||
},
|
||||
{
|
||||
Name: "sub3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badChartDeps.SetDependencies(
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub1",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub2",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
)
|
||||
return badChartDeps
|
||||
}
|
||||
|
||||
func TestValidateDependencyInChartsDir(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInChartsDir(&c); err == nil {
|
||||
t.Error("chart should have been flagged for missing deps in chart directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependencyInMetadata(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInMetadata(&c); err == nil {
|
||||
t.Errorf("chart should have been flagged for missing deps in chart metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependenciesUnique(t *testing.T) {
|
||||
tests := []struct {
|
||||
chart chart.Chart
|
||||
}{
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "bar",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "baz",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Alias: "baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if err := validateDependenciesUnique(&tt.chart); err == nil {
|
||||
t.Errorf("chart should have been flagged for dependency shadowing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
c := chartWithBadDependencies()
|
||||
err := chartutil.SaveDir(&c, tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)}
|
||||
|
||||
Dependencies(&linter)
|
||||
if l := len(linter.Messages); l != 2 {
|
||||
t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l)
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message: %d, Error: %#v", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateNoDeprecations(t *testing.T) {
|
||||
deprecated := &k8sYamlStruct{
|
||||
APIVersion: "extensions/v1beta1",
|
||||
Kind: "Deployment",
|
||||
}
|
||||
err := validateNoDeprecations(deprecated, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected deprecated extension to be flagged")
|
||||
}
|
||||
depErr := err.(deprecatedAPIError)
|
||||
if depErr.Message == "" {
|
||||
t.Fatalf("Expected error message to be non-blank: %v", err)
|
||||
}
|
||||
|
||||
if err := validateNoDeprecations(&k8sYamlStruct{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
}, nil); err != nil {
|
||||
t.Errorf("Expected a v1 Pod to not be deprecated")
|
||||
}
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/validation"
|
||||
apipath "k8s.io/apimachinery/pkg/api/validation/path"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
"helm.sh/helm/v4/pkg/engine"
|
||||
)
|
||||
|
||||
// Templates lints the templates in the Linter.
|
||||
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
|
||||
TemplatesWithKubeVersion(linter, values, namespace, nil)
|
||||
}
|
||||
|
||||
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
|
||||
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
|
||||
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false)
|
||||
}
|
||||
|
||||
// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not.
|
||||
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
|
||||
fpath := "templates/"
|
||||
templatesPath := filepath.Join(linter.ChartDir, fpath)
|
||||
|
||||
// Templates directory is optional for now
|
||||
templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath))
|
||||
if !templatesDirExists {
|
||||
return
|
||||
}
|
||||
|
||||
validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath))
|
||||
if !validTemplatesDir {
|
||||
return
|
||||
}
|
||||
|
||||
// Load chart and parse templates
|
||||
chart, err := loader.Load(linter.ChartDir)
|
||||
|
||||
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !chartLoaded {
|
||||
return
|
||||
}
|
||||
|
||||
options := common.ReleaseOptions{
|
||||
Name: "test-release",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
caps := common.DefaultCapabilities.Copy()
|
||||
if kubeVersion != nil {
|
||||
caps.KubeVersion = *kubeVersion
|
||||
}
|
||||
|
||||
// lint ignores import-values
|
||||
// See https://github.com/helm/helm/issues/9658
|
||||
if err := chartutil.ProcessDependencies(chart, values); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cvals, err := util.CoalesceValues(chart, values)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
|
||||
if err != nil {
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
return
|
||||
}
|
||||
var e engine.Engine
|
||||
e.LintMode = true
|
||||
renderedContentMap, err := e.Render(chart, valuesToRender)
|
||||
|
||||
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !renderOk {
|
||||
return
|
||||
}
|
||||
|
||||
/* Iterate over all the templates to check:
|
||||
- It is a .yaml file
|
||||
- All the values in the template file is defined
|
||||
- {{}} include | quote
|
||||
- Generated content is a valid Yaml file
|
||||
- Metadata.Namespace is not set
|
||||
*/
|
||||
for _, template := range chart.Templates {
|
||||
fileName := template.Name
|
||||
fpath = fileName
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
|
||||
|
||||
// We only apply the following lint rules to yaml files
|
||||
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463
|
||||
// Check that all the templates have a matching value
|
||||
// linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate))
|
||||
|
||||
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037
|
||||
// linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate)))
|
||||
|
||||
renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)]
|
||||
if strings.TrimSpace(renderedContent) != "" {
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent))
|
||||
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096)
|
||||
|
||||
// Lint all resources if the file contains multiple documents separated by ---
|
||||
for {
|
||||
// Even though k8sYamlStruct only defines a few fields, an error in any other
|
||||
// key will be raised as well
|
||||
var yamlStruct *k8sYamlStruct
|
||||
|
||||
err := decoder.Decode(&yamlStruct)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
// If YAML linting fails here, it will always fail in the next block as well, so we should return here.
|
||||
// fix https://github.com/helm/helm/issues/11391
|
||||
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
|
||||
return
|
||||
}
|
||||
if yamlStruct != nil {
|
||||
// NOTE: set to warnings to allow users to support out-of-date kubernetes
|
||||
// Refs https://github.com/helm/helm/issues/8596
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
|
||||
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateTopIndentLevel checks that the content does not start with an indent level > 0.
|
||||
//
|
||||
// This error can occur when a template accidentally inserts space. It can cause
|
||||
// unpredictable errors depending on whether the text is normalized before being passed
|
||||
// into the YAML parser. So we trap it here.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/8467
|
||||
func validateTopIndentLevel(content string) error {
|
||||
// Read lines until we get to a non-empty one
|
||||
scanner := bufio.NewScanner(bytes.NewBufferString(content))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// If line is empty, skip
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
// If it starts with one or more spaces, this is an error
|
||||
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||
return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line)
|
||||
}
|
||||
// Any other condition passes.
|
||||
return nil
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
func templatesDirExists(templatesPath string) error {
|
||||
_, err := os.Stat(templatesPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return errors.New("directory does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateTemplatesDir(templatesPath string) error {
|
||||
fi, err := os.Stat(templatesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAllowedExtension(fileName string) error {
|
||||
ext := filepath.Ext(fileName)
|
||||
validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"}
|
||||
|
||||
if slices.Contains(validExtensions, ext) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext)
|
||||
}
|
||||
|
||||
func validateYamlContent(err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMetadataName uses the correct validation function for the object
|
||||
// Kind, or if not set, defaults to the standard definition of a subdomain in
|
||||
// DNS (RFC 1123), used by most resources.
|
||||
func validateMetadataName(obj *k8sYamlStruct) error {
|
||||
fn := validateMetadataNameFunc(obj)
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range fn(obj.Metadata.Name, false) {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg))
|
||||
}
|
||||
if len(allErrs) > 0 {
|
||||
return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMetadataNameFunc will return a name validation function for the
|
||||
// object kind, if defined below.
|
||||
//
|
||||
// Rules should match those set in the various api validations:
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39
|
||||
// ...
|
||||
//
|
||||
// Implementing here to avoid importing k/k.
|
||||
//
|
||||
// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object
|
||||
// kinds that don't have special requirements, so is the most likely to work if
|
||||
// new kinds are added.
|
||||
func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc {
|
||||
switch strings.ToLower(obj.Kind) {
|
||||
case "pod", "node", "secret", "endpoints", "resourcequota", // core
|
||||
"controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps
|
||||
"autoscaler", // autoscaler
|
||||
"cronjob", "job", // batch
|
||||
"lease", // coordination
|
||||
"endpointslice", // discovery
|
||||
"networkpolicy", "ingress", // networking
|
||||
"podsecuritypolicy", // policy
|
||||
"priorityclass", // scheduling
|
||||
"podpreset", // settings
|
||||
"storageclass", "volumeattachment", "csinode": // storage
|
||||
return validation.NameIsDNSSubdomain
|
||||
case "service":
|
||||
return validation.NameIsDNS1035Label
|
||||
case "namespace":
|
||||
return validation.ValidateNamespaceName
|
||||
case "serviceaccount":
|
||||
return validation.ValidateServiceAccountName
|
||||
case "certificatesigningrequest":
|
||||
// No validation.
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140
|
||||
return func(_ string, _ bool) []string { return nil }
|
||||
case "role", "clusterrole", "rolebinding", "clusterrolebinding":
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34
|
||||
return func(name string, _ bool) []string {
|
||||
return apipath.IsValidPathSegmentName(name)
|
||||
}
|
||||
default:
|
||||
return validation.NameIsDNSSubdomain
|
||||
}
|
||||
}
|
||||
|
||||
// validateMatchSelector ensures that template specs have a selector declared.
|
||||
// See https://github.com/helm/helm/issues/1990
|
||||
func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error {
|
||||
switch yamlStruct.Kind {
|
||||
case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet":
|
||||
// verify that matchLabels or matchExpressions is present
|
||||
if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") {
|
||||
return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
|
||||
if yamlStruct.Kind == "List" {
|
||||
m := struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Annotations map[string]string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(manifest), &m); err != nil {
|
||||
return validateYamlContent(err)
|
||||
}
|
||||
|
||||
for _, i := range m.Items {
|
||||
if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok {
|
||||
return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// k8sYamlStruct stubs a Kubernetes YAML file.
|
||||
type k8sYamlStruct struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string
|
||||
Metadata k8sYamlMetadata
|
||||
}
|
||||
|
||||
type k8sYamlMetadata struct {
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
@ -0,0 +1,441 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
)
|
||||
|
||||
const templateTestBasedir = "./testdata/albatross"
|
||||
|
||||
func TestValidateAllowedExtension(t *testing.T) {
|
||||
var failTest = []string{"/foo", "/test.toml"}
|
||||
for _, test := range failTest {
|
||||
err := validateAllowedExtension(test)
|
||||
if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") {
|
||||
t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test)
|
||||
}
|
||||
}
|
||||
var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"}
|
||||
for _, test := range successTest {
|
||||
err := validateAllowedExtension(test)
|
||||
if err != nil {
|
||||
t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var values = map[string]interface{}{"nameOverride": "", "httpPort": 80}
|
||||
|
||||
const namespace = "testNamespace"
|
||||
const strict = false
|
||||
|
||||
func TestTemplateParsing(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: templateTestBasedir}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("Expected one error, got %d, %v", len(res), res)
|
||||
}
|
||||
|
||||
if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") {
|
||||
t.Errorf("Unexpected error: %s", res[0])
|
||||
}
|
||||
}
|
||||
|
||||
var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
|
||||
var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
|
||||
|
||||
// Test a template with all the existing features:
|
||||
// namespaces, partial templates
|
||||
func TestTemplateIntegrationHappyPath(t *testing.T) {
|
||||
// Rename file so it gets ignored by the linter
|
||||
os.Rename(wrongTemplatePath, ignoredTemplatePath)
|
||||
defer os.Rename(ignoredTemplatePath, wrongTemplatePath)
|
||||
|
||||
linter := support.Linter{ChartDir: templateTestBasedir}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 0 {
|
||||
t.Fatalf("Expected no error, got %d, %v", len(res), res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiTemplateFail(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: "./testdata/multi-template-fail"}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
res := linter.Messages
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("Expected 1 error, got %d, %v", len(res), res)
|
||||
}
|
||||
|
||||
if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") {
|
||||
t.Errorf("Unexpected error: %s", res[0].Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMetadataName(t *testing.T) {
|
||||
tests := []struct {
|
||||
obj *k8sYamlStruct
|
||||
wantErr bool
|
||||
}{
|
||||
// Most kinds use IsDNS1123Subdomain.
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true},
|
||||
|
||||
// Service uses IsDNS1035Label.
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
|
||||
|
||||
// Namespace uses IsDNS1123Label.
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false},
|
||||
|
||||
// CertificateSigningRequest has no validation.
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false},
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false},
|
||||
|
||||
// RBAC uses path validation.
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
|
||||
{&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
{&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
|
||||
|
||||
// Unknown Kind
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
|
||||
{&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
|
||||
// No kind
|
||||
{&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false},
|
||||
{&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) {
|
||||
if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedAPIFails(t *testing.T) {
|
||||
mychart := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
APIVersion: "v2",
|
||||
Name: "failapi",
|
||||
Version: "0.1.0",
|
||||
Icon: "satisfy-the-linting-gods.gif",
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/baddeployment.yaml",
|
||||
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
|
||||
},
|
||||
{
|
||||
Name: "templates/goodsecret.yaml",
|
||||
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
|
||||
},
|
||||
},
|
||||
}
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
if l := len(linter.Messages); l != 1 {
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
t.Fatalf("Expected 1 lint error, got %d", l)
|
||||
}
|
||||
|
||||
err := linter.Messages[0].Err.(deprecatedAPIError)
|
||||
if err.Deprecated != "apps/v1beta1 Deployment" {
|
||||
t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated)
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: foo
|
||||
data:
|
||||
myval1: {{default "val" .Values.mymap.key1 }}
|
||||
myval2: {{default "val" .Values.mymap.key2 }}
|
||||
`
|
||||
|
||||
// TestStrictTemplateParsingMapError is a regression test.
|
||||
//
|
||||
// The template engine should not produce an error when a map in values.yaml does
|
||||
// not contain all possible keys.
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7483
|
||||
func TestStrictTemplateParsingMapError(t *testing.T) {
|
||||
|
||||
ch := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "regression7483",
|
||||
APIVersion: "v2",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"mymap": map[string]string{
|
||||
"key1": "val1",
|
||||
},
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/configmap.yaml",
|
||||
Data: []byte(manifest),
|
||||
},
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := chartutil.SaveDir(&ch, dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linter := &support.Linter{
|
||||
ChartDir: filepath.Join(dir, ch.Metadata.Name),
|
||||
}
|
||||
Templates(linter, ch.Values, namespace, strict)
|
||||
if len(linter.Messages) != 0 {
|
||||
t.Errorf("expected zero messages, got %d", len(linter.Messages))
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %q", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMatchSelector(t *testing.T) {
|
||||
md := &k8sYamlStruct{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Metadata: k8sYamlMetadata{
|
||||
Name: "mydeployment",
|
||||
},
|
||||
}
|
||||
manifest := `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
manifest = `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchExpressions:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
manifest = `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
`
|
||||
if err := validateMatchSelector(md, manifest); err == nil {
|
||||
t.Error("expected Deployment with no selector to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTopIndentLevel(t *testing.T) {
|
||||
for doc, shouldFail := range map[string]bool{
|
||||
// Should not fail
|
||||
"\n\n\n\t\n \t\n": false,
|
||||
"apiVersion:foo\n bar:baz": false,
|
||||
"\n\n\napiVersion:foo\n\n\n": false,
|
||||
// Should fail
|
||||
" apiVersion:foo": true,
|
||||
"\n\n apiVersion:foo\n\n": true,
|
||||
} {
|
||||
if err := validateTopIndentLevel(doc); (err == nil) == shouldFail {
|
||||
t.Errorf("Expected %t for %q", shouldFail, doc)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
|
||||
// See https://github.com/helm/helm/issues/8621
|
||||
func TestEmptyWithCommentsManifests(t *testing.T) {
|
||||
mychart := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
APIVersion: "v2",
|
||||
Name: "emptymanifests",
|
||||
Version: "0.1.0",
|
||||
Icon: "satisfy-the-linting-gods.gif",
|
||||
},
|
||||
Templates: []*common.File{
|
||||
{
|
||||
Name: "templates/empty-with-comments.yaml",
|
||||
Data: []byte("#@formatter:off\n"),
|
||||
},
|
||||
},
|
||||
}
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
|
||||
Templates(&linter, values, namespace, strict)
|
||||
if l := len(linter.Messages); l > 0 {
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message %d: %s", i, msg)
|
||||
}
|
||||
t.Fatalf("Expected 0 lint errors, got %d", l)
|
||||
}
|
||||
}
|
||||
func TestValidateListAnnotations(t *testing.T) {
|
||||
md := &k8sYamlStruct{
|
||||
APIVersion: "v1",
|
||||
Kind: "List",
|
||||
Metadata: k8sYamlMetadata{
|
||||
Name: "list",
|
||||
},
|
||||
}
|
||||
manifest := `
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
`
|
||||
|
||||
if err := validateListAnnotations(md, manifest); err == nil {
|
||||
t.Fatal("expected list with nested keep annotations to fail")
|
||||
}
|
||||
|
||||
manifest = `
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
metadata:
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
`
|
||||
|
||||
if err := validateListAnnotations(md, manifest); err != nil {
|
||||
t.Fatalf("List objects keep annotations should pass. got: %s", err)
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: albatross
|
||||
description: testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,15 @@
|
||||
name: "some-chart"
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 72445e2
|
||||
home: ""
|
||||
type: application
|
||||
appVersion: 72225e2
|
||||
icon: "https://some-url.com/icon.jpeg"
|
||||
dependencies:
|
||||
- name: mariadb
|
||||
version: 5.x.x
|
||||
repository: https://charts.helm.sh/stable/
|
||||
condition: mariadb.enabled
|
||||
tags:
|
||||
- database
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: "../badchartname"
|
||||
type: application
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: badcrdfile
|
||||
type: application
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: badvaluesfile
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.0.1
|
||||
home: ""
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: goodone
|
||||
description: good testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: invalidcrdsdir
|
||||
type: application
|
||||
icon: http://riverrun.io
|
@ -0,0 +1,25 @@
|
||||
apiVersion: v3
|
||||
name: test
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
icon: https://riverrun.io
|
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: multi-template-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: v3-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,16 @@
|
||||
apiVersion: v3
|
||||
name: withsubchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
||||
icon: http://riverrun.io
|
||||
|
||||
dependencies:
|
||||
- name: subchart
|
||||
version: 0.1.16
|
||||
repository: "file://../subchart"
|
||||
import-values:
|
||||
- child: subchart
|
||||
parent: subchart
|
||||
|
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: subchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
)
|
||||
|
||||
// ValuesWithOverrides tests the values.yaml file.
|
||||
//
|
||||
// If a schema is present in the chart, values are tested against that. Otherwise,
|
||||
// they are only tested for well-formedness.
|
||||
//
|
||||
// If additional values are supplied, they are coalesced into the values in values.yaml.
|
||||
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) {
|
||||
file := "values.yaml"
|
||||
vf := filepath.Join(linter.ChartDir, file)
|
||||
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
|
||||
|
||||
if !fileExists {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides))
|
||||
}
|
||||
|
||||
func validateValuesFileExistence(valuesPath string) error {
|
||||
_, err := os.Stat(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateValuesFile(valuesPath string, overrides map[string]interface{}) error {
|
||||
values, err := common.ReadValuesFile(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
|
||||
// level values against the top-level expectations. Subchart values are not linted.
|
||||
// We could change that. For now, though, we retain that strategy, and thus can
|
||||
// coalesce tables (like reuse-values does) instead of doing the full chart
|
||||
// CoalesceValues
|
||||
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
|
||||
coalescedValues = util.CoalesceTables(coalescedValues, values)
|
||||
|
||||
ext := filepath.Ext(valuesPath)
|
||||
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
|
||||
schema, err := os.ReadFile(schemaPath)
|
||||
if len(schema) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
|
||||
helmversion "helm.sh/helm/v4/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
|
||||
// must be a string.
|
||||
k8sVersionMajor = "1"
|
||||
k8sVersionMinor = "20"
|
||||
|
||||
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
|
||||
DefaultVersionSet = allKnownVersions()
|
||||
|
||||
// DefaultCapabilities is the default set of capabilities.
|
||||
DefaultCapabilities = &Capabilities{
|
||||
KubeVersion: KubeVersion{
|
||||
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
|
||||
Major: k8sVersionMajor,
|
||||
Minor: k8sVersionMinor,
|
||||
},
|
||||
APIVersions: DefaultVersionSet,
|
||||
HelmVersion: helmversion.Get(),
|
||||
}
|
||||
)
|
||||
|
||||
// Capabilities describes the capabilities of the Kubernetes cluster.
|
||||
type Capabilities struct {
|
||||
// KubeVersion is the Kubernetes version.
|
||||
KubeVersion KubeVersion
|
||||
// APIVersions are supported Kubernetes API versions.
|
||||
APIVersions VersionSet
|
||||
// HelmVersion is the build information for this helm version
|
||||
HelmVersion helmversion.BuildInfo
|
||||
}
|
||||
|
||||
func (capabilities *Capabilities) Copy() *Capabilities {
|
||||
return &Capabilities{
|
||||
KubeVersion: capabilities.KubeVersion,
|
||||
APIVersions: capabilities.APIVersions,
|
||||
HelmVersion: capabilities.HelmVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// KubeVersion is the Kubernetes version.
|
||||
type KubeVersion struct {
|
||||
Version string // Kubernetes version
|
||||
Major string // Kubernetes major version
|
||||
Minor string // Kubernetes minor version
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (kv *KubeVersion) String() string { return kv.Version }
|
||||
|
||||
// GitVersion returns the Kubernetes version string.
|
||||
//
|
||||
// Deprecated: use KubeVersion.Version.
|
||||
func (kv *KubeVersion) GitVersion() string { return kv.Version }
|
||||
|
||||
// ParseKubeVersion parses kubernetes version from string
|
||||
func ParseKubeVersion(version string) (*KubeVersion, error) {
|
||||
sv, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &KubeVersion{
|
||||
Version: "v" + sv.String(),
|
||||
Major: strconv.FormatUint(sv.Major(), 10),
|
||||
Minor: strconv.FormatUint(sv.Minor(), 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VersionSet is a set of Kubernetes API versions.
|
||||
type VersionSet []string
|
||||
|
||||
// Has returns true if the version string is in the set.
|
||||
//
|
||||
// vs.Has("apps/v1")
|
||||
func (v VersionSet) Has(apiVersion string) bool {
|
||||
return slices.Contains(v, apiVersion)
|
||||
}
|
||||
|
||||
func allKnownVersions() VersionSet {
|
||||
// We should register the built in extension APIs as well so CRDs are
|
||||
// supported in the default version set. This has caused problems with `helm
|
||||
// template` in the past, so let's be safe
|
||||
apiextensionsv1beta1.AddToScheme(scheme.Scheme)
|
||||
apiextensionsv1.AddToScheme(scheme.Scheme)
|
||||
|
||||
groups := scheme.Scheme.PrioritizedVersionsAllGroups()
|
||||
vs := make(VersionSet, 0, len(groups))
|
||||
for _, gv := range groups {
|
||||
vs = append(vs, gv.String())
|
||||
}
|
||||
return vs
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionSet(t *testing.T) {
|
||||
vs := VersionSet{"v1", "apps/v1"}
|
||||
if d := len(vs); d != 2 {
|
||||
t.Errorf("Expected 2 versions, got %d", d)
|
||||
}
|
||||
|
||||
if !vs.Has("apps/v1") {
|
||||
t.Error("Expected to find apps/v1")
|
||||
}
|
||||
|
||||
if vs.Has("Spanish/inquisition") {
|
||||
t.Error("No one expects the Spanish/inquisition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultVersionSet(t *testing.T) {
|
||||
if !DefaultVersionSet.Has("v1") {
|
||||
t.Error("Expected core v1 version set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilities(t *testing.T) {
|
||||
kv := DefaultCapabilities.KubeVersion
|
||||
if kv.String() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Version != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.GitVersion() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "20" {
|
||||
t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
|
||||
hv := DefaultCapabilities.HelmVersion
|
||||
|
||||
if hv.Version != "v4.0" {
|
||||
t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKubeVersion(t *testing.T) {
|
||||
kv, err := ParseKubeVersion("v1.16.0")
|
||||
if err != nil {
|
||||
t.Errorf("Expected v1.16.0 to parse successfully")
|
||||
}
|
||||
if kv.Version != "v1.16.0" {
|
||||
t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "16" {
|
||||
t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor)
|
||||
}
|
||||
}
|
@ -1,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,43 +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"
|
||||
)
|
||||
|
||||
// ErrNoTable indicates that a chart does not have a matching table.
|
||||
type ErrNoTable struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) }
|
||||
|
||||
// ErrNoValue indicates that Values does not contain a key with a value
|
||||
type ErrNoValue struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) }
|
||||
|
||||
type ErrInvalidChartName struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e ErrInvalidChartName) Error() string {
|
||||
return fmt.Sprintf("%q is not a valid chart name", e.Name)
|
||||
}
|
@ -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,219 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package chart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
v3chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
common "helm.sh/helm/v4/pkg/chart/common"
|
||||
v2chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
)
|
||||
|
||||
var NewAccessor func(chrt Charter) (Accessor, error) = NewDefaultAccessor //nolint:revive
|
||||
|
||||
func NewDefaultAccessor(chrt Charter) (Accessor, error) {
|
||||
switch v := chrt.(type) {
|
||||
case v2chart.Chart:
|
||||
return &v2Accessor{&v}, nil
|
||||
case *v2chart.Chart:
|
||||
return &v2Accessor{v}, nil
|
||||
case v3chart.Chart:
|
||||
return &v3Accessor{&v}, nil
|
||||
case *v3chart.Chart:
|
||||
return &v3Accessor{v}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported chart type")
|
||||
}
|
||||
}
|
||||
|
||||
type v2Accessor struct {
|
||||
chrt *v2chart.Chart
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Name() string {
|
||||
return r.chrt.Metadata.Name
|
||||
}
|
||||
|
||||
func (r *v2Accessor) IsRoot() bool {
|
||||
return r.chrt.IsRoot()
|
||||
}
|
||||
|
||||
func (r *v2Accessor) MetadataAsMap() map[string]interface{} {
|
||||
var ret map[string]interface{}
|
||||
if r.chrt.Metadata == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
ret, err := structToMap(r.chrt.Metadata)
|
||||
if err != nil {
|
||||
slog.Error("error converting metadata to map", "error", err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Files() []*common.File {
|
||||
return r.chrt.Files
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Templates() []*common.File {
|
||||
return r.chrt.Templates
|
||||
}
|
||||
|
||||
func (r *v2Accessor) ChartFullPath() string {
|
||||
return r.chrt.ChartFullPath()
|
||||
}
|
||||
|
||||
func (r *v2Accessor) IsLibraryChart() bool {
|
||||
return strings.EqualFold(r.chrt.Metadata.Type, "library")
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Dependencies() []Charter {
|
||||
var deps = make([]Charter, len(r.chrt.Dependencies()))
|
||||
for i, c := range r.chrt.Dependencies() {
|
||||
deps[i] = c
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Values() map[string]interface{} {
|
||||
return r.chrt.Values
|
||||
}
|
||||
|
||||
func (r *v2Accessor) Schema() []byte {
|
||||
return r.chrt.Schema
|
||||
}
|
||||
|
||||
type v3Accessor struct {
|
||||
chrt *v3chart.Chart
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Name() string {
|
||||
return r.chrt.Metadata.Name
|
||||
}
|
||||
|
||||
func (r *v3Accessor) IsRoot() bool {
|
||||
return r.chrt.IsRoot()
|
||||
}
|
||||
|
||||
func (r *v3Accessor) MetadataAsMap() map[string]interface{} {
|
||||
var ret map[string]interface{}
|
||||
if r.chrt.Metadata == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
ret, err := structToMap(r.chrt.Metadata)
|
||||
if err != nil {
|
||||
slog.Error("error converting metadata to map", "error", err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Files() []*common.File {
|
||||
return r.chrt.Files
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Templates() []*common.File {
|
||||
return r.chrt.Templates
|
||||
}
|
||||
|
||||
func (r *v3Accessor) ChartFullPath() string {
|
||||
return r.chrt.ChartFullPath()
|
||||
}
|
||||
|
||||
func (r *v3Accessor) IsLibraryChart() bool {
|
||||
return strings.EqualFold(r.chrt.Metadata.Type, "library")
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Dependencies() []Charter {
|
||||
var deps = make([]Charter, len(r.chrt.Dependencies()))
|
||||
for i, c := range r.chrt.Dependencies() {
|
||||
deps[i] = c
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Values() map[string]interface{} {
|
||||
return r.chrt.Values
|
||||
}
|
||||
|
||||
func (r *v3Accessor) Schema() []byte {
|
||||
return r.chrt.Schema
|
||||
}
|
||||
|
||||
func structToMap(obj interface{}) (map[string]interface{}, error) {
|
||||
objValue := reflect.ValueOf(obj)
|
||||
|
||||
// If the value is a pointer, dereference it
|
||||
if objValue.Kind() == reflect.Ptr {
|
||||
objValue = objValue.Elem()
|
||||
}
|
||||
|
||||
// Check if the input is a struct
|
||||
if objValue.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("input must be a struct or a pointer to a struct")
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
objType := objValue.Type()
|
||||
|
||||
for i := 0; i < objValue.NumField(); i++ {
|
||||
field := objType.Field(i)
|
||||
value := objValue.Field(i)
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Struct:
|
||||
nestedMap, err := structToMap(value.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[field.Name] = nestedMap
|
||||
case reflect.Ptr:
|
||||
// Recurse for pointers by dereferencing
|
||||
if value.IsNil() {
|
||||
result[field.Name] = nil
|
||||
} else {
|
||||
nestedMap, err := structToMap(value.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[field.Name] = nestedMap
|
||||
}
|
||||
case reflect.Slice:
|
||||
sliceOfMaps := make([]interface{}, value.Len())
|
||||
for j := 0; j < value.Len(); j++ {
|
||||
sliceElement := value.Index(j)
|
||||
if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Ptr {
|
||||
nestedMap, err := structToMap(sliceElement.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sliceOfMaps[j] = nestedMap
|
||||
} else {
|
||||
sliceOfMaps[j] = sliceElement.Interface()
|
||||
}
|
||||
}
|
||||
result[field.Name] = sliceOfMaps
|
||||
default:
|
||||
result[field.Name] = value.Interface()
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue