mirror of https://github.com/helm/helm
commit
97d78c2e8d
@ -1,2 +1,2 @@
|
||||
GOLANG_VERSION=1.24
|
||||
GOLANGCI_LINT_VERSION=v2.1.0
|
||||
GOLANG_VERSION=1.25
|
||||
GOLANGCI_LINT_VERSION=v2.5.0
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Overview
|
||||
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs.
|
||||
|
||||
## Build & Test
|
||||
```bash
|
||||
make build # Build binary
|
||||
make test # Run all tests (style + unit)
|
||||
make test-unit # Unit tests only
|
||||
make test-coverage # With coverage
|
||||
make test-style # Linting
|
||||
golangci-lint run # Direct linting
|
||||
go test -run TestName # Specific test
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
- `/cmd/helm/` - CLI entry point (Cobra-based)
|
||||
- `/pkg/` - Public API
|
||||
- `action/` - Core operations (install, upgrade, rollback)
|
||||
- `chart/v2/` - Stable chart format
|
||||
- `engine/` - Template rendering (Go templates + Sprig)
|
||||
- `registry/` - OCI support
|
||||
- `storage/` - Release backends (Secrets/ConfigMaps/SQL)
|
||||
- `/internal/` - Private implementation
|
||||
- `chart/v3/` - Next-gen chart format
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
- Use table-driven tests with testify
|
||||
- Golden files in `testdata/` for complex output
|
||||
- Mock Kubernetes clients for action tests
|
||||
- All commits must include DCO sign-off: `git commit -s`
|
||||
|
||||
### Branching
|
||||
- `main` - Helm v4 development
|
||||
- `dev-v3` - Helm v3 stable (backport from main)
|
||||
|
||||
### Dependencies
|
||||
- `k8s.io/client-go` - Kubernetes interaction
|
||||
- `github.com/spf13/cobra` - CLI framework
|
||||
- `github.com/Masterminds/sprig` - Template functions
|
||||
|
||||
### Key Patterns
|
||||
- **Actions**: Operations in `/pkg/action/` use shared Configuration
|
||||
- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/`
|
||||
- **Storage Abstraction**: Pluggable release storage backends
|
||||
@ -0,0 +1,225 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
// Chartfile runs a set of linter rules related to Chart.yaml file
|
||||
func Chartfile(linter *support.Linter) {
|
||||
chartFileName := "Chart.yaml"
|
||||
chartPath := filepath.Join(linter.ChartDir, chartFileName)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
|
||||
|
||||
chartFile, err := chartutil.LoadChartfile(chartPath)
|
||||
validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
|
||||
|
||||
// Guard clause. Following linter rules require a parsable ChartFile
|
||||
if !validChartFile {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = chartutil.StrictLoadChartfile(chartPath)
|
||||
linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err))
|
||||
|
||||
// type check for Chart.yaml . ignoring error as any parse
|
||||
// errors would already be caught in the above load function
|
||||
chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath)
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
|
||||
|
||||
// Chart metadata
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
|
||||
linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile))
|
||||
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
|
||||
}
|
||||
|
||||
func validateChartVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "version")
|
||||
}
|
||||
|
||||
func validateChartAppVersionType(data map[string]interface{}) error {
|
||||
return isStringValue(data, "appVersion")
|
||||
}
|
||||
|
||||
func isStringValue(data map[string]interface{}, key string) error {
|
||||
value, ok := data[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
valueType := fmt.Sprintf("%T", value)
|
||||
if valueType != "string" {
|
||||
return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlNotDirectory(chartPath string) error {
|
||||
fi, err := os.Stat(chartPath)
|
||||
|
||||
if err == nil && fi.IsDir() {
|
||||
return errors.New("should be a file, not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartYamlStrictFormat(chartFileError error) error {
|
||||
if chartFileError != nil {
|
||||
return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartName(cf *chart.Metadata) error {
|
||||
if cf.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
name := filepath.Base(cf.Name)
|
||||
if name != cf.Name {
|
||||
return fmt.Errorf("chart name %q is invalid", cf.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartAPIVersion(cf *chart.Metadata) error {
|
||||
if cf.APIVersion == "" {
|
||||
return errors.New("apiVersion is required. The value must be \"v3\"")
|
||||
}
|
||||
|
||||
if cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartVersion(cf *chart.Metadata) error {
|
||||
if cf.Version == "" {
|
||||
return errors.New("version is required")
|
||||
}
|
||||
|
||||
version, err := semver.StrictNewVersion(cf.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version)
|
||||
}
|
||||
|
||||
c, err := semver.NewConstraint(">0.0.0-0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valid, msg := c.Validate(version)
|
||||
|
||||
if !valid && len(msg) > 0 {
|
||||
return fmt.Errorf("version %v", msg[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartMaintainer(cf *chart.Metadata) error {
|
||||
for _, maintainer := range cf.Maintainers {
|
||||
if maintainer == nil {
|
||||
return errors.New("a maintainer entry is empty")
|
||||
}
|
||||
if maintainer.Name == "" {
|
||||
return errors.New("each maintainer requires a name")
|
||||
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
|
||||
return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
|
||||
} else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) {
|
||||
return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartSources(cf *chart.Metadata) error {
|
||||
for _, source := range cf.Sources {
|
||||
if source == "" || !govalidator.IsRequestURL(source) {
|
||||
return fmt.Errorf("invalid source URL '%s'", source)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartIconPresence(cf *chart.Metadata) error {
|
||||
if cf.Icon == "" {
|
||||
return errors.New("icon is recommended")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartIconURL(cf *chart.Metadata) error {
|
||||
if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) {
|
||||
return fmt.Errorf("invalid icon URL '%s'", cf.Icon)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartDependencies(cf *chart.Metadata) error {
|
||||
if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartType(cf *chart.Metadata) error {
|
||||
if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 {
|
||||
return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadChartFileForTypeCheck loads the Chart.yaml
|
||||
// in a generic form of a map[string]interface{}, so that the type
|
||||
// of the values can be checked
|
||||
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
y := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(b, &y)
|
||||
return y, err
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Crds lints the CRDs in the Linter.
|
||||
func Crds(linter *support.Linter) {
|
||||
fpath := "crds/"
|
||||
crdsPath := filepath.Join(linter.ChartDir, fpath)
|
||||
|
||||
// crds directory is optional
|
||||
if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
|
||||
crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath))
|
||||
if !crdsDirValid {
|
||||
return
|
||||
}
|
||||
|
||||
// Load chart and parse CRDs
|
||||
chart, err := loader.Load(linter.ChartDir)
|
||||
|
||||
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
|
||||
|
||||
if !chartLoaded {
|
||||
return
|
||||
}
|
||||
|
||||
/* Iterate over all the CRDs to check:
|
||||
1. It is a YAML file and not a template
|
||||
2. The API version is apiextensions.k8s.io
|
||||
3. The kind is CustomResourceDefinition
|
||||
*/
|
||||
for _, crd := range chart.CRDObjects() {
|
||||
fileName := crd.Name
|
||||
fpath = fileName
|
||||
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096)
|
||||
for {
|
||||
var yamlStruct *k8sYamlStruct
|
||||
|
||||
err := decoder.Decode(&yamlStruct)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
// If YAML parsing fails here, it will always fail in the next block as well, so we should return here.
|
||||
// This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct.
|
||||
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
|
||||
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
func validateCrdsDir(crdsPath string) error {
|
||||
fi, err := os.Stat(crdsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdAPIVersion(obj *k8sYamlStruct) error {
|
||||
if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") {
|
||||
return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrdKind(obj *k8sYamlStruct) error {
|
||||
if obj.Kind != "CustomResourceDefinition" {
|
||||
return fmt.Errorf("object kind is not 'CustomResourceDefinition'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
)
|
||||
|
||||
const invalidCrdsDir = "./testdata/invalidcrdsdir"
|
||||
|
||||
func TestInvalidCrdsDir(t *testing.T) {
|
||||
linter := support.Linter{ChartDir: invalidCrdsDir}
|
||||
Crds(&linter)
|
||||
res := linter.Messages
|
||||
|
||||
assert.Len(t, res, 1)
|
||||
assert.ErrorContains(t, res[0].Err, "not a directory")
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/internal/chart/v3/loader"
|
||||
)
|
||||
|
||||
// Dependencies runs lints against a chart's dependencies
|
||||
//
|
||||
// See https://github.com/helm/helm/issues/7910
|
||||
func Dependencies(linter *support.Linter) {
|
||||
c, err := loader.LoadDir(linter.ChartDir)
|
||||
if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
|
||||
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
|
||||
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
|
||||
}
|
||||
|
||||
func validateChartFormat(chartError error) error {
|
||||
if chartError != nil {
|
||||
return fmt.Errorf("unable to load chart\n\t%w", chartError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDependencyInChartsDir(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Dependencies() {
|
||||
dependencies[dep.Metadata.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
if _, ok := dependencies[dep.Name]; !ok {
|
||||
missing = append(missing, dep.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependencyInMetadata(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]struct{}{}
|
||||
missing := []string{}
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
dependencies[dep.Name] = struct{}{}
|
||||
}
|
||||
for _, dep := range c.Dependencies() {
|
||||
if _, ok := dependencies[dep.Metadata.Name]; !ok {
|
||||
missing = append(missing, dep.Metadata.Name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateDependenciesUnique(c *chart.Chart) (err error) {
|
||||
dependencies := map[string]*chart.Dependency{}
|
||||
shadowing := []string{}
|
||||
|
||||
for _, dep := range c.Metadata.Dependencies {
|
||||
key := dep.Name
|
||||
if dep.Alias != "" {
|
||||
key = dep.Alias
|
||||
}
|
||||
if dependencies[key] != nil {
|
||||
shadowing = append(shadowing, key)
|
||||
}
|
||||
dependencies[key] = dep
|
||||
}
|
||||
if len(shadowing) > 0 {
|
||||
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package rules
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
)
|
||||
|
||||
func chartWithBadDependencies() chart.Chart {
|
||||
badChartDeps := chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "sub2",
|
||||
},
|
||||
{
|
||||
Name: "sub3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badChartDeps.SetDependencies(
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub1",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sub2",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
},
|
||||
},
|
||||
)
|
||||
return badChartDeps
|
||||
}
|
||||
|
||||
func TestValidateDependencyInChartsDir(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInChartsDir(&c); err == nil {
|
||||
t.Error("chart should have been flagged for missing deps in chart directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependencyInMetadata(t *testing.T) {
|
||||
c := chartWithBadDependencies()
|
||||
|
||||
if err := validateDependencyInMetadata(&c); err == nil {
|
||||
t.Errorf("chart should have been flagged for missing deps in chart metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDependenciesUnique(t *testing.T) {
|
||||
tests := []struct {
|
||||
chart chart.Chart
|
||||
}{
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "bar",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "badchart",
|
||||
Version: "0.1.0",
|
||||
APIVersion: "v2",
|
||||
Dependencies: []*chart.Dependency{
|
||||
{
|
||||
Name: "foo",
|
||||
Alias: "baz",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Alias: "baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if err := validateDependenciesUnique(&tt.chart); err == nil {
|
||||
t.Errorf("chart should have been flagged for dependency shadowing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
c := chartWithBadDependencies()
|
||||
err := chartutil.SaveDir(&c, tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)}
|
||||
|
||||
Dependencies(&linter)
|
||||
if l := len(linter.Messages); l != 2 {
|
||||
t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l)
|
||||
for i, msg := range linter.Messages {
|
||||
t.Logf("Message: %d, Error: %#v", i, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateNoDeprecations(t *testing.T) {
|
||||
deprecated := &k8sYamlStruct{
|
||||
APIVersion: "extensions/v1beta1",
|
||||
Kind: "Deployment",
|
||||
}
|
||||
err := validateNoDeprecations(deprecated, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected deprecated extension to be flagged")
|
||||
}
|
||||
depErr := err.(deprecatedAPIError)
|
||||
if depErr.Message == "" {
|
||||
t.Fatalf("Expected error message to be non-blank: %v", err)
|
||||
}
|
||||
|
||||
if err := validateNoDeprecations(&k8sYamlStruct{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
}, nil); err != nil {
|
||||
t.Errorf("Expected a v1 Pod to not be deprecated")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: albatross
|
||||
description: testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
||||
@ -0,0 +1,15 @@
|
||||
name: "some-chart"
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 72445e2
|
||||
home: ""
|
||||
type: application
|
||||
appVersion: 72225e2
|
||||
icon: "https://some-url.com/icon.jpeg"
|
||||
dependencies:
|
||||
- name: mariadb
|
||||
version: 5.x.x
|
||||
repository: https://charts.helm.sh/stable/
|
||||
condition: mariadb.enabled
|
||||
tags:
|
||||
- database
|
||||
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: "../badchartname"
|
||||
type: application
|
||||
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: badcrdfile
|
||||
type: application
|
||||
icon: http://riverrun.io
|
||||
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: badvaluesfile
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.0.1
|
||||
home: ""
|
||||
icon: http://riverrun.io
|
||||
@ -0,0 +1,5 @@
|
||||
apiVersion: v3
|
||||
name: goodone
|
||||
description: good testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
||||
icon: http://riverrun.io
|
||||
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.1.0
|
||||
name: invalidcrdsdir
|
||||
type: application
|
||||
icon: http://riverrun.io
|
||||
@ -0,0 +1,25 @@
|
||||
apiVersion: v3
|
||||
name: test
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
icon: https://riverrun.io
|
||||
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: multi-template-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
@ -0,0 +1,21 @@
|
||||
apiVersion: v3
|
||||
name: v3-fail
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application and it is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
@ -0,0 +1,16 @@
|
||||
apiVersion: v3
|
||||
name: withsubchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
||||
icon: http://riverrun.io
|
||||
|
||||
dependencies:
|
||||
- name: subchart
|
||||
version: 0.1.16
|
||||
repository: "file://../subchart"
|
||||
import-values:
|
||||
- child: subchart
|
||||
parent: subchart
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
apiVersion: v3
|
||||
name: subchart
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.16.0"
|
||||
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"helm.sh/helm/v4/internal/chart/v3/lint/support"
|
||||
"helm.sh/helm/v4/pkg/chart/common"
|
||||
"helm.sh/helm/v4/pkg/chart/common/util"
|
||||
)
|
||||
|
||||
// ValuesWithOverrides tests the values.yaml file.
|
||||
//
|
||||
// If a schema is present in the chart, values are tested against that. Otherwise,
|
||||
// they are only tested for well-formedness.
|
||||
//
|
||||
// If additional values are supplied, they are coalesced into the values in values.yaml.
|
||||
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
|
||||
file := "values.yaml"
|
||||
vf := filepath.Join(linter.ChartDir, file)
|
||||
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
|
||||
|
||||
if !fileExists {
|
||||
return
|
||||
}
|
||||
|
||||
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
|
||||
}
|
||||
|
||||
func validateValuesFileExistence(valuesPath string) error {
|
||||
_, err := os.Stat(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
|
||||
values, err := common.ReadValuesFile(valuesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
|
||||
// level values against the top-level expectations. Subchart values are not linted.
|
||||
// We could change that. For now, though, we retain that strategy, and thus can
|
||||
// coalesce tables (like reuse-values does) instead of doing the full chart
|
||||
// CoalesceValues
|
||||
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
|
||||
coalescedValues = util.CoalesceTables(coalescedValues, values)
|
||||
|
||||
ext := filepath.Ext(valuesPath)
|
||||
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
|
||||
schema, err := os.ReadFile(schemaPath)
|
||||
if len(schema) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !skipSchemaValidation {
|
||||
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
|
||||
helmversion "helm.sh/helm/v4/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
|
||||
// must be a string.
|
||||
k8sVersionMajor = "1"
|
||||
k8sVersionMinor = "20"
|
||||
|
||||
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
|
||||
DefaultVersionSet = allKnownVersions()
|
||||
|
||||
// DefaultCapabilities is the default set of capabilities.
|
||||
DefaultCapabilities = &Capabilities{
|
||||
KubeVersion: KubeVersion{
|
||||
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
|
||||
Major: k8sVersionMajor,
|
||||
Minor: k8sVersionMinor,
|
||||
},
|
||||
APIVersions: DefaultVersionSet,
|
||||
HelmVersion: helmversion.Get(),
|
||||
}
|
||||
)
|
||||
|
||||
// Capabilities describes the capabilities of the Kubernetes cluster.
|
||||
type Capabilities struct {
|
||||
// KubeVersion is the Kubernetes version.
|
||||
KubeVersion KubeVersion
|
||||
// APIVersions are supported Kubernetes API versions.
|
||||
APIVersions VersionSet
|
||||
// HelmVersion is the build information for this helm version
|
||||
HelmVersion helmversion.BuildInfo
|
||||
}
|
||||
|
||||
func (capabilities *Capabilities) Copy() *Capabilities {
|
||||
return &Capabilities{
|
||||
KubeVersion: capabilities.KubeVersion,
|
||||
APIVersions: capabilities.APIVersions,
|
||||
HelmVersion: capabilities.HelmVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// KubeVersion is the Kubernetes version.
|
||||
type KubeVersion struct {
|
||||
Version string // Kubernetes version
|
||||
Major string // Kubernetes major version
|
||||
Minor string // Kubernetes minor version
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (kv *KubeVersion) String() string { return kv.Version }
|
||||
|
||||
// GitVersion returns the Kubernetes version string.
|
||||
//
|
||||
// Deprecated: use KubeVersion.Version.
|
||||
func (kv *KubeVersion) GitVersion() string { return kv.Version }
|
||||
|
||||
// ParseKubeVersion parses kubernetes version from string
|
||||
func ParseKubeVersion(version string) (*KubeVersion, error) {
|
||||
sv, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &KubeVersion{
|
||||
Version: "v" + sv.String(),
|
||||
Major: strconv.FormatUint(sv.Major(), 10),
|
||||
Minor: strconv.FormatUint(sv.Minor(), 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VersionSet is a set of Kubernetes API versions.
|
||||
type VersionSet []string
|
||||
|
||||
// Has returns true if the version string is in the set.
|
||||
//
|
||||
// vs.Has("apps/v1")
|
||||
func (v VersionSet) Has(apiVersion string) bool {
|
||||
return slices.Contains(v, apiVersion)
|
||||
}
|
||||
|
||||
func allKnownVersions() VersionSet {
|
||||
// We should register the built in extension APIs as well so CRDs are
|
||||
// supported in the default version set. This has caused problems with `helm
|
||||
// template` in the past, so let's be safe
|
||||
apiextensionsv1beta1.AddToScheme(scheme.Scheme)
|
||||
apiextensionsv1.AddToScheme(scheme.Scheme)
|
||||
|
||||
groups := scheme.Scheme.PrioritizedVersionsAllGroups()
|
||||
vs := make(VersionSet, 0, len(groups))
|
||||
for _, gv := range groups {
|
||||
vs = append(vs, gv.String())
|
||||
}
|
||||
return vs
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionSet(t *testing.T) {
|
||||
vs := VersionSet{"v1", "apps/v1"}
|
||||
if d := len(vs); d != 2 {
|
||||
t.Errorf("Expected 2 versions, got %d", d)
|
||||
}
|
||||
|
||||
if !vs.Has("apps/v1") {
|
||||
t.Error("Expected to find apps/v1")
|
||||
}
|
||||
|
||||
if vs.Has("Spanish/inquisition") {
|
||||
t.Error("No one expects the Spanish/inquisition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultVersionSet(t *testing.T) {
|
||||
if !DefaultVersionSet.Has("v1") {
|
||||
t.Error("Expected core v1 version set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilities(t *testing.T) {
|
||||
kv := DefaultCapabilities.KubeVersion
|
||||
if kv.String() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Version != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.GitVersion() != "v1.20.0" {
|
||||
t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version)
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "20" {
|
||||
t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
|
||||
hv := DefaultCapabilities.HelmVersion
|
||||
|
||||
if hv.Version != "v4.0" {
|
||||
t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKubeVersion(t *testing.T) {
|
||||
kv, err := ParseKubeVersion("v1.16.0")
|
||||
if err != nil {
|
||||
t.Errorf("Expected v1.16.0 to parse successfully")
|
||||
}
|
||||
if kv.Version != "v1.16.0" {
|
||||
t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String())
|
||||
}
|
||||
if kv.Major != "1" {
|
||||
t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
|
||||
}
|
||||
if kv.Minor != "16" {
|
||||
t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor)
|
||||
}
|
||||
}
|
||||
@ -1,723 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
|
||||
var testCoalesceValuesYaml = []byte(`
|
||||
top: yup
|
||||
bottom: null
|
||||
right: Null
|
||||
left: NULL
|
||||
front: ~
|
||||
back: ""
|
||||
nested:
|
||||
boat: null
|
||||
|
||||
global:
|
||||
name: Ishmael
|
||||
subject: Queequeg
|
||||
nested:
|
||||
boat: true
|
||||
|
||||
pequod:
|
||||
boat: null
|
||||
global:
|
||||
name: Stinky
|
||||
harpooner: Tashtego
|
||||
nested:
|
||||
boat: false
|
||||
sail: true
|
||||
foo2: null
|
||||
ahab:
|
||||
scope: whale
|
||||
boat: null
|
||||
nested:
|
||||
foo: true
|
||||
boat: null
|
||||
object: null
|
||||
`)
|
||||
|
||||
func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart {
|
||||
c.AddDependency(deps...)
|
||||
return c
|
||||
}
|
||||
|
||||
func TestCoalesceValues(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "moby"},
|
||||
Values: map[string]interface{}{
|
||||
"back": "exists",
|
||||
"bottom": "exists",
|
||||
"front": "exists",
|
||||
"left": "exists",
|
||||
"name": "moby",
|
||||
"nested": map[string]interface{}{"boat": true},
|
||||
"override": "bad",
|
||||
"right": "exists",
|
||||
"scope": "moby",
|
||||
"top": "nope",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l0": "moby"},
|
||||
},
|
||||
"pequod": map[string]interface{}{
|
||||
"boat": "maybe",
|
||||
"ahab": map[string]interface{}{
|
||||
"boat": "maybe",
|
||||
"nested": map[string]interface{}{"boat": "maybe"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "pequod"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
"scope": "pequod",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "pequod"},
|
||||
},
|
||||
"boat": false,
|
||||
"ahab": map[string]interface{}{
|
||||
"boat": false,
|
||||
"nested": map[string]interface{}{"boat": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "ahab"},
|
||||
Values: map[string]interface{}{
|
||||
"global": map[string]interface{}{
|
||||
"nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"},
|
||||
"nested2": map[string]interface{}{"l2": "ahab"},
|
||||
},
|
||||
"scope": "ahab",
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"nested": map[string]interface{}{"foo": false, "boat": true},
|
||||
"object": map[string]interface{}{"foo": "bar"},
|
||||
},
|
||||
},
|
||||
),
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "spouter"},
|
||||
Values: map[string]interface{}{
|
||||
"scope": "spouter",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "spouter"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
vals, err := ReadValues(testCoalesceValuesYaml)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// taking a copy of the values before passing it
|
||||
// to CoalesceValues as argument, so that we can
|
||||
// use it for asserting later
|
||||
valsCopy := make(Values, len(vals))
|
||||
maps.Copy(valsCopy, vals)
|
||||
|
||||
v, err := CoalesceValues(c, vals)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", " ")
|
||||
t.Logf("Coalesced Values: %s", string(j))
|
||||
|
||||
tests := []struct {
|
||||
tpl string
|
||||
expect string
|
||||
}{
|
||||
{"{{.top}}", "yup"},
|
||||
{"{{.back}}", ""},
|
||||
{"{{.name}}", "moby"},
|
||||
{"{{.global.name}}", "Ishmael"},
|
||||
{"{{.global.subject}}", "Queequeg"},
|
||||
{"{{.global.harpooner}}", "<no value>"},
|
||||
{"{{.pequod.name}}", "pequod"},
|
||||
{"{{.pequod.ahab.name}}", "ahab"},
|
||||
{"{{.pequod.ahab.scope}}", "whale"},
|
||||
{"{{.pequod.ahab.nested.foo}}", "true"},
|
||||
{"{{.pequod.ahab.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.ahab.global.nested.foo}}", "bar"},
|
||||
{"{{.pequod.ahab.global.nested.foo2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
|
||||
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
|
||||
{"{{.pequod.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.global.nested.foo}}", "<no value>"},
|
||||
{"{{.pequod.global.subject}}", "Queequeg"},
|
||||
{"{{.spouter.global.name}}", "Ishmael"},
|
||||
{"{{.spouter.global.harpooner}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.boat}}", "true"},
|
||||
{"{{.spouter.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.sail}}", "true"},
|
||||
{"{{.spouter.global.nested.sail}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested2.l0}}", "moby"},
|
||||
{"{{.global.nested2.l1}}", "<no value>"},
|
||||
{"{{.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
|
||||
{"{{.spouter.global.nested2.l0}}", "moby"},
|
||||
{"{{.spouter.global.nested2.l1}}", "spouter"},
|
||||
{"{{.spouter.global.nested2.l2}}", "<no value>"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
|
||||
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
|
||||
}
|
||||
}
|
||||
|
||||
nullKeys := []string{"bottom", "right", "left", "front"}
|
||||
for _, nullKey := range nullKeys {
|
||||
if _, ok := v[nullKey]; ok {
|
||||
t.Errorf("Expected key %q to be removed, still present", nullKey)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := v["nested"].(map[string]interface{})["boat"]; ok {
|
||||
t.Error("Expected nested boat key to be removed, still present")
|
||||
}
|
||||
|
||||
subchart := v["pequod"].(map[string]interface{})
|
||||
if _, ok := subchart["boat"]; ok {
|
||||
t.Error("Expected subchart boat key to be removed, still present")
|
||||
}
|
||||
|
||||
subsubchart := subchart["ahab"].(map[string]interface{})
|
||||
if _, ok := subsubchart["boat"]; ok {
|
||||
t.Error("Expected sub-subchart ahab boat key to be removed, still present")
|
||||
}
|
||||
|
||||
if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok {
|
||||
t.Error("Expected sub-subchart nested boat key to be removed, still present")
|
||||
}
|
||||
|
||||
if _, ok := subsubchart["object"]; ok {
|
||||
t.Error("Expected sub-subchart object map to be removed, still present")
|
||||
}
|
||||
|
||||
// CoalesceValues should not mutate the passed arguments
|
||||
is.Equal(valsCopy, vals)
|
||||
}
|
||||
|
||||
func TestMergeValues(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "moby"},
|
||||
Values: map[string]interface{}{
|
||||
"back": "exists",
|
||||
"bottom": "exists",
|
||||
"front": "exists",
|
||||
"left": "exists",
|
||||
"name": "moby",
|
||||
"nested": map[string]interface{}{"boat": true},
|
||||
"override": "bad",
|
||||
"right": "exists",
|
||||
"scope": "moby",
|
||||
"top": "nope",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l0": "moby"},
|
||||
},
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "pequod"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
"scope": "pequod",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "pequod"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "ahab"},
|
||||
Values: map[string]interface{}{
|
||||
"global": map[string]interface{}{
|
||||
"nested": map[string]interface{}{"foo": "bar"},
|
||||
"nested2": map[string]interface{}{"l2": "ahab"},
|
||||
},
|
||||
"scope": "ahab",
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"nested": map[string]interface{}{"foo": false, "bar": true},
|
||||
},
|
||||
},
|
||||
),
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "spouter"},
|
||||
Values: map[string]interface{}{
|
||||
"scope": "spouter",
|
||||
"global": map[string]interface{}{
|
||||
"nested2": map[string]interface{}{"l1": "spouter"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
vals, err := ReadValues(testCoalesceValuesYaml)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// taking a copy of the values before passing it
|
||||
// to MergeValues as argument, so that we can
|
||||
// use it for asserting later
|
||||
valsCopy := make(Values, len(vals))
|
||||
maps.Copy(valsCopy, vals)
|
||||
|
||||
v, err := MergeValues(c, vals)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", " ")
|
||||
t.Logf("Coalesced Values: %s", string(j))
|
||||
|
||||
tests := []struct {
|
||||
tpl string
|
||||
expect string
|
||||
}{
|
||||
{"{{.top}}", "yup"},
|
||||
{"{{.back}}", ""},
|
||||
{"{{.name}}", "moby"},
|
||||
{"{{.global.name}}", "Ishmael"},
|
||||
{"{{.global.subject}}", "Queequeg"},
|
||||
{"{{.global.harpooner}}", "<no value>"},
|
||||
{"{{.pequod.name}}", "pequod"},
|
||||
{"{{.pequod.ahab.name}}", "ahab"},
|
||||
{"{{.pequod.ahab.scope}}", "whale"},
|
||||
{"{{.pequod.ahab.nested.foo}}", "true"},
|
||||
{"{{.pequod.ahab.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.ahab.global.nested.foo}}", "bar"},
|
||||
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
|
||||
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
|
||||
{"{{.pequod.global.name}}", "Ishmael"},
|
||||
{"{{.pequod.global.nested.foo}}", "<no value>"},
|
||||
{"{{.pequod.global.subject}}", "Queequeg"},
|
||||
{"{{.spouter.global.name}}", "Ishmael"},
|
||||
{"{{.spouter.global.harpooner}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.boat}}", "true"},
|
||||
{"{{.spouter.global.nested.boat}}", "true"},
|
||||
{"{{.pequod.global.nested.sail}}", "true"},
|
||||
{"{{.spouter.global.nested.sail}}", "<no value>"},
|
||||
|
||||
{"{{.global.nested2.l0}}", "moby"},
|
||||
{"{{.global.nested2.l1}}", "<no value>"},
|
||||
{"{{.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.global.nested2.l2}}", "<no value>"},
|
||||
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
|
||||
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
|
||||
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
|
||||
{"{{.spouter.global.nested2.l0}}", "moby"},
|
||||
{"{{.spouter.global.nested2.l1}}", "spouter"},
|
||||
{"{{.spouter.global.nested2.l2}}", "<no value>"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
|
||||
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
|
||||
}
|
||||
}
|
||||
|
||||
// nullKeys is different from coalescing. Here the null/nil values are not
|
||||
// removed.
|
||||
nullKeys := []string{"bottom", "right", "left", "front"}
|
||||
for _, nullKey := range nullKeys {
|
||||
if vv, ok := v[nullKey]; !ok {
|
||||
t.Errorf("Expected key %q to be present but it was removed", nullKey)
|
||||
} else if vv != nil {
|
||||
t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok {
|
||||
t.Error("Expected nested boat key to be present but it was removed")
|
||||
}
|
||||
|
||||
subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{})
|
||||
if _, ok := subchart["boat"]; !ok {
|
||||
t.Error("Expected subchart boat key to be present but it was removed")
|
||||
}
|
||||
|
||||
if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok {
|
||||
t.Error("Expected subchart nested bar key to be present but it was removed")
|
||||
}
|
||||
|
||||
// CoalesceValues should not mutate the passed arguments
|
||||
is.Equal(valsCopy, vals)
|
||||
}
|
||||
|
||||
func TestCoalesceTables(t *testing.T) {
|
||||
dst := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": nil,
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": nil,
|
||||
}
|
||||
src := map[string]interface{}{
|
||||
"occupation": "whaler",
|
||||
"address": map[string]interface{}{
|
||||
"state": "MA",
|
||||
"street": "234 Spouter Inn Ct.",
|
||||
"country": "US",
|
||||
},
|
||||
"details": "empty",
|
||||
"boat": map[string]interface{}{
|
||||
"mast": true,
|
||||
},
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst overrides anything in src, but that
|
||||
// otherwise the values are coalesced.
|
||||
CoalesceTables(dst, src)
|
||||
|
||||
if dst["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst["name"])
|
||||
}
|
||||
if dst["occupation"] != "whaler" {
|
||||
t.Errorf("Unexpected occupation: %s", dst["occupation"])
|
||||
}
|
||||
|
||||
addr, ok := dst["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr["street"])
|
||||
}
|
||||
|
||||
if addr["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr["city"])
|
||||
}
|
||||
|
||||
if addr["state"].(string) != "MA" {
|
||||
t.Errorf("Unexpected state: %v", addr["state"])
|
||||
}
|
||||
|
||||
if _, ok = addr["country"]; ok {
|
||||
t.Error("The country is not left out.")
|
||||
}
|
||||
|
||||
if det, ok := dst["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst["details"])
|
||||
} else if _, ok := det["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst["boat"])
|
||||
}
|
||||
|
||||
if _, ok = dst["hole"]; ok {
|
||||
t.Error("The hole still exists.")
|
||||
}
|
||||
|
||||
dst2 := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": "US",
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst should have all values set,
|
||||
// this happens when the --reuse-values flag is set but the chart has no modifications yet
|
||||
CoalesceTables(dst2, nil)
|
||||
|
||||
if dst2["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst2["name"])
|
||||
}
|
||||
|
||||
addr2, ok := dst2["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr2["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr2["street"])
|
||||
}
|
||||
|
||||
if addr2["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr2["city"])
|
||||
}
|
||||
|
||||
if addr2["country"].(string) != "US" {
|
||||
t.Errorf("Unexpected Country: %v", addr2["country"])
|
||||
}
|
||||
|
||||
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst2["details"])
|
||||
} else if _, ok := det2["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst2["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["hole"].(string) != "black" {
|
||||
t.Errorf("Expected hole string, got %v", dst2["boat"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTables(t *testing.T) {
|
||||
dst := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": nil,
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": nil,
|
||||
}
|
||||
src := map[string]interface{}{
|
||||
"occupation": "whaler",
|
||||
"address": map[string]interface{}{
|
||||
"state": "MA",
|
||||
"street": "234 Spouter Inn Ct.",
|
||||
"country": "US",
|
||||
},
|
||||
"details": "empty",
|
||||
"boat": map[string]interface{}{
|
||||
"mast": true,
|
||||
},
|
||||
"hole": "black",
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst overrides anything in src, but that
|
||||
// otherwise the values are coalesced.
|
||||
MergeTables(dst, src)
|
||||
|
||||
if dst["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst["name"])
|
||||
}
|
||||
if dst["occupation"] != "whaler" {
|
||||
t.Errorf("Unexpected occupation: %s", dst["occupation"])
|
||||
}
|
||||
|
||||
addr, ok := dst["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr["street"])
|
||||
}
|
||||
|
||||
if addr["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr["city"])
|
||||
}
|
||||
|
||||
if addr["state"].(string) != "MA" {
|
||||
t.Errorf("Unexpected state: %v", addr["state"])
|
||||
}
|
||||
|
||||
// This is one test that is different from CoalesceTables. Because country
|
||||
// is a nil value and it's not removed it's still present.
|
||||
if _, ok = addr["country"]; !ok {
|
||||
t.Error("The country is left out.")
|
||||
}
|
||||
|
||||
if det, ok := dst["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst["details"])
|
||||
} else if _, ok := det["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst["boat"])
|
||||
}
|
||||
|
||||
// This is one test that is different from CoalesceTables. Because hole
|
||||
// is a nil value and it's not removed it's still present.
|
||||
if _, ok = dst["hole"]; !ok {
|
||||
t.Error("The hole no longer exists.")
|
||||
}
|
||||
|
||||
dst2 := map[string]interface{}{
|
||||
"name": "Ishmael",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Spouter Inn Ct.",
|
||||
"city": "Nantucket",
|
||||
"country": "US",
|
||||
},
|
||||
"details": map[string]interface{}{
|
||||
"friends": []string{"Tashtego"},
|
||||
},
|
||||
"boat": "pequod",
|
||||
"hole": "black",
|
||||
"nilval": nil,
|
||||
}
|
||||
|
||||
// What we expect is that anything in dst should have all values set,
|
||||
// this happens when the --reuse-values flag is set but the chart has no modifications yet
|
||||
MergeTables(dst2, nil)
|
||||
|
||||
if dst2["name"] != "Ishmael" {
|
||||
t.Errorf("Unexpected name: %s", dst2["name"])
|
||||
}
|
||||
|
||||
addr2, ok := dst2["address"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("Address went away.")
|
||||
}
|
||||
|
||||
if addr2["street"].(string) != "123 Spouter Inn Ct." {
|
||||
t.Errorf("Unexpected address: %v", addr2["street"])
|
||||
}
|
||||
|
||||
if addr2["city"].(string) != "Nantucket" {
|
||||
t.Errorf("Unexpected city: %v", addr2["city"])
|
||||
}
|
||||
|
||||
if addr2["country"].(string) != "US" {
|
||||
t.Errorf("Unexpected Country: %v", addr2["country"])
|
||||
}
|
||||
|
||||
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("Details is the wrong type: %v", dst2["details"])
|
||||
} else if _, ok := det2["friends"]; !ok {
|
||||
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
||||
}
|
||||
|
||||
if dst2["boat"].(string) != "pequod" {
|
||||
t.Errorf("Expected boat string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["hole"].(string) != "black" {
|
||||
t.Errorf("Expected hole string, got %v", dst2["boat"])
|
||||
}
|
||||
|
||||
if dst2["nilval"] != nil {
|
||||
t.Error("Expected nilvalue to have nil value but it does not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoalesceValuesWarnings(t *testing.T) {
|
||||
|
||||
c := withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level1"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "moby",
|
||||
},
|
||||
},
|
||||
withDeps(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level2"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "pequod",
|
||||
},
|
||||
},
|
||||
&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "level3"},
|
||||
Values: map[string]interface{}{
|
||||
"name": "ahab",
|
||||
"boat": true,
|
||||
"spear": map[string]interface{}{
|
||||
"tip": true,
|
||||
"sail": map[string]interface{}{
|
||||
"cotton": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"level2": map[string]interface{}{
|
||||
"level3": map[string]interface{}{
|
||||
"boat": map[string]interface{}{"mast": true},
|
||||
"spear": map[string]interface{}{
|
||||
"tip": map[string]interface{}{
|
||||
"sharp": true,
|
||||
},
|
||||
"sail": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
warnings := make([]string, 0)
|
||||
printf := func(format string, v ...interface{}) {
|
||||
t.Logf(format, v...)
|
||||
warnings = append(warnings, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
_, err := coalesce(printf, c, vals, "", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("vals: %v", vals)
|
||||
assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.")
|
||||
assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)")
|
||||
assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])")
|
||||
|
||||
}
|
||||
|
||||
func TestConcatPrefix(t *testing.T) {
|
||||
assert.Equal(t, "b", concatPrefix("", "b"))
|
||||
assert.Equal(t, "a.b", concatPrefix("a", "b"))
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema
|
||||
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
|
||||
var sb strings.Builder
|
||||
if chrt.Schema != nil {
|
||||
slog.Debug("chart name", "chart-name", chrt.Name())
|
||||
err := ValidateAgainstSingleSchema(values, chrt.Schema)
|
||||
if err != nil {
|
||||
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
|
||||
sb.WriteString(err.Error())
|
||||
}
|
||||
}
|
||||
slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies()))
|
||||
// For each dependency, recursively call this function with the coalesced values
|
||||
for _, subchart := range chrt.Dependencies() {
|
||||
subchartValues := values[subchart.Name()].(map[string]interface{})
|
||||
if err := ValidateAgainstSchema(subchart, subchartValues); err != nil {
|
||||
sb.WriteString(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if sb.Len() > 0 {
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
|
||||
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
reterr = fmt.Errorf("unable to validate schema: %s", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// This unmarshal function leverages UseNumber() for number precision. The parser
|
||||
// used for values does this as well.
|
||||
schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Debug("unmarshalled JSON schema", "schema", schemaJSON)
|
||||
|
||||
compiler := jsonschema.NewCompiler()
|
||||
err = compiler.AddResource("file:///values.schema.json", schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validator, err := compiler.Compile("file:///values.schema.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validator.Validate(values.AsMap())
|
||||
if err != nil {
|
||||
return JSONSchemaValidationError{err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note, JSONSchemaValidationError is used to wrap the error from the underlying
|
||||
// validation package so that Helm has a clean interface and the validation package
|
||||
// could be replaced without changing the Helm SDK API.
|
||||
|
||||
// JSONSchemaValidationError is the error returned when there is a schema validation
|
||||
// error.
|
||||
type JSONSchemaValidationError struct {
|
||||
embeddedErr error
|
||||
}
|
||||
|
||||
// Error prints the error message
|
||||
func (e JSONSchemaValidationError) Error() string {
|
||||
errStr := e.embeddedErr.Error()
|
||||
|
||||
// This string prefixes all of our error details. Further up the stack of helm error message
|
||||
// building more detail is provided to users. This is removed.
|
||||
errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n")
|
||||
|
||||
// The extra new line is needed for when there are sub-charts.
|
||||
return errStr + "\n"
|
||||
}
|
||||
@ -1,247 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
func TestValidateAgainstSingleSchema(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#'
|
||||
- at '': got number, want boolean or object`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
|
||||
values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
schema, err := os.ReadFile("./testdata/test-values.schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading JSON file: %s", err)
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `- at '': missing property 'employmentInfo'
|
||||
- at '/age': minimum: got -5, want 0
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
const subchartSchema = `{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"age": {
|
||||
"description": "Age",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"age"
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
const subchartSchema2020 = `{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"contains": { "type": "string" },
|
||||
"unevaluatedItems": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"required": ["data"]
|
||||
}
|
||||
`
|
||||
|
||||
func TestValidateAgainstSchema(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"age": 25,
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchemaNegative(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{},
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSchema(chrt, vals); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `subchart:
|
||||
- at '': missing property 'age'
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchema2020(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema2020)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"data": []any{"hello", 12},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
t.Errorf("Error validating Values against Schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAgainstSchema2020Negative(t *testing.T) {
|
||||
subchartJSON := []byte(subchartSchema2020)
|
||||
subchart := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "subchart",
|
||||
},
|
||||
Schema: subchartJSON,
|
||||
}
|
||||
chrt := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "chrt",
|
||||
},
|
||||
}
|
||||
chrt.AddDependency(subchart)
|
||||
|
||||
vals := map[string]interface{}{
|
||||
"name": "John",
|
||||
"subchart": map[string]interface{}{
|
||||
"data": []any{12},
|
||||
},
|
||||
}
|
||||
|
||||
var errString string
|
||||
if err := ValidateAgainstSchema(chrt, vals); err == nil {
|
||||
t.Fatalf("Expected an error, but got nil")
|
||||
} else {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
expectedErrString := `subchart:
|
||||
- at '/data': no items match contains schema
|
||||
- at '/data/0': got number, want string
|
||||
`
|
||||
if errString != expectedErrString {
|
||||
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
|
||||
}
|
||||
}
|
||||
@ -1,220 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
// GlobalKey is the name of the Values key that is used for storing global vars.
|
||||
const GlobalKey = "global"
|
||||
|
||||
// Values represents a collection of chart values.
|
||||
type Values map[string]interface{}
|
||||
|
||||
// YAML encodes the Values into a YAML string.
|
||||
func (v Values) YAML() (string, error) {
|
||||
b, err := yaml.Marshal(v)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// Table gets a table (YAML subsection) from a Values object.
|
||||
//
|
||||
// The table is returned as a Values.
|
||||
//
|
||||
// Compound table names may be specified with dots:
|
||||
//
|
||||
// foo.bar
|
||||
//
|
||||
// The above will be evaluated as "The table bar inside the table
|
||||
// foo".
|
||||
//
|
||||
// An ErrNoTable is returned if the table does not exist.
|
||||
func (v Values) Table(name string) (Values, error) {
|
||||
table := v
|
||||
var err error
|
||||
|
||||
for _, n := range parsePath(name) {
|
||||
if table, err = tableLookup(table, n); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
// AsMap is a utility function for converting Values to a map[string]interface{}.
|
||||
//
|
||||
// It protects against nil map panics.
|
||||
func (v Values) AsMap() map[string]interface{} {
|
||||
if len(v) == 0 {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Encode writes serialized Values information to the given io.Writer.
|
||||
func (v Values) Encode(w io.Writer) error {
|
||||
out, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func tableLookup(v Values, simple string) (Values, error) {
|
||||
v2, ok := v[simple]
|
||||
if !ok {
|
||||
return v, ErrNoTable{simple}
|
||||
}
|
||||
if vv, ok := v2.(map[string]interface{}); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// This catches a case where a value is of type Values, but doesn't (for some
|
||||
// reason) match the map[string]interface{}. This has been observed in the
|
||||
// wild, and might be a result of a nil map of type Values.
|
||||
if vv, ok := v2.(Values); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
return Values{}, ErrNoTable{simple}
|
||||
}
|
||||
|
||||
// ReadValues will parse YAML byte data into a Values.
|
||||
func ReadValues(data []byte) (vals Values, err error) {
|
||||
err = yaml.Unmarshal(data, &vals)
|
||||
if len(vals) == 0 {
|
||||
vals = Values{}
|
||||
}
|
||||
return vals, err
|
||||
}
|
||||
|
||||
// ReadValuesFile will parse a YAML file into a map of values.
|
||||
func ReadValuesFile(filename string) (Values, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
return ReadValues(data)
|
||||
}
|
||||
|
||||
// ReleaseOptions represents the additional release options needed
|
||||
// for the composition of the final values struct
|
||||
type ReleaseOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Revision int
|
||||
IsUpgrade bool
|
||||
IsInstall bool
|
||||
}
|
||||
|
||||
// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// This takes both ReleaseOptions and Capabilities to merge into the render values.
|
||||
func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) {
|
||||
return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false)
|
||||
}
|
||||
|
||||
// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// This takes both ReleaseOptions and Capabilities to merge into the render values.
|
||||
func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) {
|
||||
if caps == nil {
|
||||
caps = DefaultCapabilities
|
||||
}
|
||||
top := map[string]interface{}{
|
||||
"Chart": chrt.Metadata,
|
||||
"Capabilities": caps,
|
||||
"Release": map[string]interface{}{
|
||||
"Name": options.Name,
|
||||
"Namespace": options.Namespace,
|
||||
"IsUpgrade": options.IsUpgrade,
|
||||
"IsInstall": options.IsInstall,
|
||||
"Revision": options.Revision,
|
||||
"Service": "Helm",
|
||||
},
|
||||
}
|
||||
|
||||
vals, err := CoalesceValues(chrt, chrtVals)
|
||||
if err != nil {
|
||||
return top, err
|
||||
}
|
||||
|
||||
if !skipSchemaValidation {
|
||||
if err := ValidateAgainstSchema(chrt, vals); err != nil {
|
||||
return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err)
|
||||
}
|
||||
}
|
||||
|
||||
top["Values"] = vals
|
||||
return top, nil
|
||||
}
|
||||
|
||||
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
|
||||
func istable(v interface{}) bool {
|
||||
_, ok := v.(map[string]interface{})
|
||||
return ok
|
||||
}
|
||||
|
||||
// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
|
||||
// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
|
||||
// Given the following YAML data the value at path "chapter.one.title" is "Loomings".
|
||||
//
|
||||
// chapter:
|
||||
// one:
|
||||
// title: "Loomings"
|
||||
func (v Values) PathValue(path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("YAML path cannot be empty")
|
||||
}
|
||||
return v.pathValue(parsePath(path))
|
||||
}
|
||||
|
||||
func (v Values) pathValue(path []string) (interface{}, error) {
|
||||
if len(path) == 1 {
|
||||
// if exists must be root key not table
|
||||
if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
|
||||
return v[path[0]], nil
|
||||
}
|
||||
return nil, ErrNoValue{path[0]}
|
||||
}
|
||||
|
||||
key, path := path[len(path)-1], path[:len(path)-1]
|
||||
// get our table for table path
|
||||
t, err := v.Table(joinPath(path...))
|
||||
if err != nil {
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
// check table for key and ensure value is not a table
|
||||
if k, ok := t[key]; ok && !istable(k) {
|
||||
return k, nil
|
||||
}
|
||||
return nil, ErrNoValue{key}
|
||||
}
|
||||
|
||||
func parsePath(key string) []string { return strings.Split(key, ".") }
|
||||
|
||||
func joinPath(path ...string) string { return strings.Join(path, ".") }
|
||||
@ -1,293 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||
)
|
||||
|
||||
func TestReadValues(t *testing.T) {
|
||||
doc := `# Test YAML parse
|
||||
poet: "Coleridge"
|
||||
title: "Rime of the Ancient Mariner"
|
||||
stanza:
|
||||
- "at"
|
||||
- "length"
|
||||
- "did"
|
||||
- cross
|
||||
- an
|
||||
- Albatross
|
||||
|
||||
mariner:
|
||||
with: "crossbow"
|
||||
shot: "ALBATROSS"
|
||||
|
||||
water:
|
||||
water:
|
||||
where: "everywhere"
|
||||
nor: "any drop to drink"
|
||||
`
|
||||
|
||||
data, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing bytes: %s", err)
|
||||
}
|
||||
matchValues(t, data)
|
||||
|
||||
tests := []string{`poet: "Coleridge"`, "# Just a comment", ""}
|
||||
|
||||
for _, tt := range tests {
|
||||
data, err = ReadValues([]byte(tt))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing bytes (%s): %s", tt, err)
|
||||
}
|
||||
if data == nil {
|
||||
t.Errorf(`YAML string "%s" gave a nil map`, tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToRenderValues(t *testing.T) {
|
||||
|
||||
chartValues := map[string]interface{}{
|
||||
"name": "al Rashid",
|
||||
"where": map[string]interface{}{
|
||||
"city": "Basrah",
|
||||
"title": "caliph",
|
||||
},
|
||||
}
|
||||
|
||||
overrideValues := map[string]interface{}{
|
||||
"name": "Haroun",
|
||||
"where": map[string]interface{}{
|
||||
"city": "Baghdad",
|
||||
"date": "809 CE",
|
||||
},
|
||||
}
|
||||
|
||||
c := &chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "test"},
|
||||
Templates: []*chart.File{},
|
||||
Values: chartValues,
|
||||
Files: []*chart.File{
|
||||
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
|
||||
},
|
||||
}
|
||||
c.AddDependency(&chart.Chart{
|
||||
Metadata: &chart.Metadata{Name: "where"},
|
||||
})
|
||||
|
||||
o := ReleaseOptions{
|
||||
Name: "Seven Voyages",
|
||||
Namespace: "default",
|
||||
Revision: 1,
|
||||
IsInstall: true,
|
||||
}
|
||||
|
||||
res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure that the top-level values are all set.
|
||||
if name := res["Chart"].(*chart.Metadata).Name; name != "test" {
|
||||
t.Errorf("Expected chart name 'test', got %q", name)
|
||||
}
|
||||
relmap := res["Release"].(map[string]interface{})
|
||||
if name := relmap["Name"]; name.(string) != "Seven Voyages" {
|
||||
t.Errorf("Expected release name 'Seven Voyages', got %q", name)
|
||||
}
|
||||
if namespace := relmap["Namespace"]; namespace.(string) != "default" {
|
||||
t.Errorf("Expected namespace 'default', got %q", namespace)
|
||||
}
|
||||
if revision := relmap["Revision"]; revision.(int) != 1 {
|
||||
t.Errorf("Expected revision '1', got %d", revision)
|
||||
}
|
||||
if relmap["IsUpgrade"].(bool) {
|
||||
t.Error("Expected upgrade to be false.")
|
||||
}
|
||||
if !relmap["IsInstall"].(bool) {
|
||||
t.Errorf("Expected install to be true.")
|
||||
}
|
||||
if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
|
||||
t.Error("Expected Capabilities to have v1 as an API")
|
||||
}
|
||||
if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" {
|
||||
t.Error("Expected Capabilities to have a Kube version")
|
||||
}
|
||||
|
||||
vals := res["Values"].(Values)
|
||||
if vals["name"] != "Haroun" {
|
||||
t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals)
|
||||
}
|
||||
where := vals["where"].(map[string]interface{})
|
||||
expects := map[string]string{
|
||||
"city": "Baghdad",
|
||||
"date": "809 CE",
|
||||
"title": "caliph",
|
||||
}
|
||||
for field, expect := range expects {
|
||||
if got := where[field]; got != expect {
|
||||
t.Errorf("Expected %q, got %q (%v)", expect, got, where)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadValuesFile(t *testing.T) {
|
||||
data, err := ReadValuesFile("./testdata/coleridge.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading YAML file: %s", err)
|
||||
}
|
||||
matchValues(t, data)
|
||||
}
|
||||
|
||||
func ExampleValues() {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ch1, err := d.Table("chapter.one")
|
||||
if err != nil {
|
||||
panic("could not find chapter one")
|
||||
}
|
||||
fmt.Print(ch1["title"])
|
||||
// Output:
|
||||
// Loomings
|
||||
}
|
||||
|
||||
func TestTable(t *testing.T) {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse the White Whale: %s", err)
|
||||
}
|
||||
|
||||
if _, err := d.Table("title"); err == nil {
|
||||
t.Fatalf("Title is not a table.")
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter"); err != nil {
|
||||
t.Fatalf("Failed to get the chapter table: %s\n%v", err, d)
|
||||
}
|
||||
|
||||
if v, err := d.Table("chapter.one"); err != nil {
|
||||
t.Errorf("Failed to get chapter.one: %s", err)
|
||||
} else if v["title"] != "Loomings" {
|
||||
t.Errorf("Unexpected title: %s", v["title"])
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter.three"); err != nil {
|
||||
t.Errorf("Chapter three is missing: %s\n%v", err, d)
|
||||
}
|
||||
|
||||
if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil {
|
||||
t.Errorf("I think you mean 'Epilogue'")
|
||||
}
|
||||
}
|
||||
|
||||
func matchValues(t *testing.T, data map[string]interface{}) {
|
||||
t.Helper()
|
||||
if data["poet"] != "Coleridge" {
|
||||
t.Errorf("Unexpected poet: %s", data["poet"])
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{len .stanza}}", data); err != nil {
|
||||
t.Errorf("len stanza: %s", err)
|
||||
} else if o != "6" {
|
||||
t.Errorf("Expected 6, got %s", o)
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{.mariner.shot}}", data); err != nil {
|
||||
t.Errorf(".mariner.shot: %s", err)
|
||||
} else if o != "ALBATROSS" {
|
||||
t.Errorf("Expected that mariner shot ALBATROSS")
|
||||
}
|
||||
|
||||
if o, err := ttpl("{{.water.water.where}}", data); err != nil {
|
||||
t.Errorf(".water.water.where: %s", err)
|
||||
} else if o != "everywhere" {
|
||||
t.Errorf("Expected water water everywhere")
|
||||
}
|
||||
}
|
||||
|
||||
func ttpl(tpl string, v map[string]interface{}) (string, error) {
|
||||
var b bytes.Buffer
|
||||
tt := template.Must(template.New("t").Parse(tpl))
|
||||
err := tt.Execute(&b, v)
|
||||
return b.String(), err
|
||||
}
|
||||
|
||||
func TestPathValue(t *testing.T) {
|
||||
doc := `
|
||||
title: "Moby Dick"
|
||||
chapter:
|
||||
one:
|
||||
title: "Loomings"
|
||||
two:
|
||||
title: "The Carpet-Bag"
|
||||
three:
|
||||
title: "The Spouter Inn"
|
||||
`
|
||||
d, err := ReadValues([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse the White Whale: %s", err)
|
||||
}
|
||||
|
||||
if v, err := d.PathValue("chapter.one.title"); err != nil {
|
||||
t.Errorf("Got error instead of title: %s\n%v", err, d)
|
||||
} else if v != "Loomings" {
|
||||
t.Errorf("No error but got wrong value for title: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil {
|
||||
t.Errorf("Non-existent key should return error: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil {
|
||||
t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d)
|
||||
}
|
||||
if _, err := d.PathValue(""); err == nil {
|
||||
t.Error("Asking for the value from an empty path should yield an error")
|
||||
}
|
||||
if v, err := d.PathValue("title"); err == nil {
|
||||
if v != "Moby Dick" {
|
||||
t.Errorf("Failed to return values for root key title")
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue