mirror of https://github.com/helm/helm
Merge 3f45ceed40 into 53dfa521e0
commit
ddbaa06c1c
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.yaml.in/yaml/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// yaml11Booleans are the plain scalars that YAML 1.1 resolves to a boolean.
|
||||||
|
// Values are parsed with sigs.k8s.io/yaml, which speaks YAML 1.1, so such a
|
||||||
|
// scalar used as a mapping key becomes "true" or "false".
|
||||||
|
//
|
||||||
|
// The values passed to --set never go through a YAML parser, so the same key
|
||||||
|
// written there stays a string. The two ways of supplying a value therefore
|
||||||
|
// produce two different keys.
|
||||||
|
var yaml11Booleans = map[string]bool{
|
||||||
|
"y": true, "Y": true, "yes": true, "Yes": true, "YES": true,
|
||||||
|
"n": true, "N": true, "no": true, "No": true, "NO": true,
|
||||||
|
"on": true, "On": true, "ON": true, "off": true, "Off": true, "OFF": true,
|
||||||
|
"True": true, "False": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// booleanLikeKey describes a key that does not survive parsing under its own name.
|
||||||
|
type booleanLikeKey struct {
|
||||||
|
path string
|
||||||
|
line int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k booleanLikeKey) String() string {
|
||||||
|
return fmt.Sprintf("%s (line %d)", k.path, k.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBooleanLikeKeys walks a values document and collects keys that YAML 1.1
|
||||||
|
// turns into a boolean. Quoted keys are left alone: quoting is the fix, and it
|
||||||
|
// already works.
|
||||||
|
func findBooleanLikeKeys(data []byte) ([]booleanLikeKey, error) {
|
||||||
|
var doc yaml.Node
|
||||||
|
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||||
|
// Parse errors are reported by the values rule; nothing to add here.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var found []booleanLikeKey
|
||||||
|
for _, n := range doc.Content {
|
||||||
|
walkKeys(n, "", &found)
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkKeys(n *yaml.Node, path string, found *[]booleanLikeKey) {
|
||||||
|
switch n.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
for i := 0; i+1 < len(n.Content); i += 2 {
|
||||||
|
k, v := n.Content[i], n.Content[i+1]
|
||||||
|
name := k.Value
|
||||||
|
child := name
|
||||||
|
if path != "" {
|
||||||
|
child = path + "." + name
|
||||||
|
}
|
||||||
|
if k.Style == 0 && yaml11Booleans[name] {
|
||||||
|
*found = append(*found, booleanLikeKey{path: child, line: k.Line})
|
||||||
|
}
|
||||||
|
walkKeys(v, child, found)
|
||||||
|
}
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
for i, c := range n.Content {
|
||||||
|
walkKeys(c, fmt.Sprintf("%s[%d]", path, i), found)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Scalar, alias and document nodes carry no mapping keys to check.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateNoBooleanLikeKeys warns about keys that a template cannot reach by the
|
||||||
|
// name the chart author wrote.
|
||||||
|
func validateNoBooleanLikeKeys(valuesPath string) error {
|
||||||
|
data, err := os.ReadFile(valuesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil // absence and unreadability are reported by other rules
|
||||||
|
}
|
||||||
|
found, err := findBooleanLikeKeys(data)
|
||||||
|
if err != nil || len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(found))
|
||||||
|
for _, k := range found {
|
||||||
|
names = append(names, k.String())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("key(s) %s are read as booleans and become %q or %q in the values; "+
|
||||||
|
"a template cannot reach them under the original name and --set will not override them. Quote the key to keep it",
|
||||||
|
strings.Join(names, ", "), "true", "false")
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindBooleanLikeKeys(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
doc string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "plain keys that YAML 1.1 reads as booleans",
|
||||||
|
// A Grafana dashboard pasted into values is the common way to hit this.
|
||||||
|
doc: "gridPos:\n h: 8\n w: 12\n x: 0\n y: 0\n",
|
||||||
|
want: []string{"gridPos.y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inside a sequence",
|
||||||
|
doc: "panels:\n - title: cpu\n on: true\n",
|
||||||
|
want: []string{"panels[0].on"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every affected spelling",
|
||||||
|
doc: "a:\n y: 1\n Y: 1\n yes: 1\n no: 1\n N: 1\n off: 1\n ON: 1\n True: 1\n",
|
||||||
|
want: []string{"a.y", "a.Y", "a.yes", "a.no", "a.N", "a.off", "a.ON", "a.True"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted keys survive and are not reported",
|
||||||
|
doc: "a:\n \"y\": 1\n 'on': 2\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ordinary keys",
|
||||||
|
doc: "replicaCount: 1\nimage:\n tag: latest\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty document",
|
||||||
|
doc: "",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unparsable document is left to the values rule",
|
||||||
|
doc: "a:\n\t- broken\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
found, err := findBooleanLikeKeys([]byte(tt.doc))
|
||||||
|
require.NoError(t, err)
|
||||||
|
paths := make([]string, 0, len(found))
|
||||||
|
for _, k := range found {
|
||||||
|
paths = append(paths, k.path)
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, tt.want, paths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNoBooleanLikeKeys(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
affected := filepath.Join(dir, "values.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(affected, []byte("gridPos:\n x: 0\n y: 0\n"), 0o644))
|
||||||
|
err := validateNoBooleanLikeKeys(affected)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "gridPos.y")
|
||||||
|
assert.Contains(t, err.Error(), "line 3")
|
||||||
|
|
||||||
|
clean := filepath.Join(dir, "clean.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(clean, []byte("gridPos:\n x: 0\n \"y\": 0\n"), 0o644))
|
||||||
|
assert.NoError(t, validateNoBooleanLikeKeys(clean))
|
||||||
|
|
||||||
|
// A missing file is reported by validateValuesFileExistence, not here.
|
||||||
|
assert.NoError(t, validateNoBooleanLikeKeys(filepath.Join(dir, "nope.yaml")))
|
||||||
|
}
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.yaml.in/yaml/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// yaml11Booleans are the plain scalars that YAML 1.1 resolves to a boolean.
|
||||||
|
// Values are parsed with sigs.k8s.io/yaml, which speaks YAML 1.1, so such a
|
||||||
|
// scalar used as a mapping key becomes "true" or "false".
|
||||||
|
//
|
||||||
|
// The values passed to --set never go through a YAML parser, so the same key
|
||||||
|
// written there stays a string. The two ways of supplying a value therefore
|
||||||
|
// produce two different keys.
|
||||||
|
var yaml11Booleans = map[string]bool{
|
||||||
|
"y": true, "Y": true, "yes": true, "Yes": true, "YES": true,
|
||||||
|
"n": true, "N": true, "no": true, "No": true, "NO": true,
|
||||||
|
"on": true, "On": true, "ON": true, "off": true, "Off": true, "OFF": true,
|
||||||
|
"True": true, "False": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// booleanLikeKey describes a key that does not survive parsing under its own name.
|
||||||
|
type booleanLikeKey struct {
|
||||||
|
path string
|
||||||
|
line int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k booleanLikeKey) String() string {
|
||||||
|
return fmt.Sprintf("%s (line %d)", k.path, k.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBooleanLikeKeys walks a values document and collects keys that YAML 1.1
|
||||||
|
// turns into a boolean. Quoted keys are left alone: quoting is the fix, and it
|
||||||
|
// already works.
|
||||||
|
func findBooleanLikeKeys(data []byte) ([]booleanLikeKey, error) {
|
||||||
|
var doc yaml.Node
|
||||||
|
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||||
|
// Parse errors are reported by the values rule; nothing to add here.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var found []booleanLikeKey
|
||||||
|
for _, n := range doc.Content {
|
||||||
|
walkKeys(n, "", &found)
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkKeys(n *yaml.Node, path string, found *[]booleanLikeKey) {
|
||||||
|
switch n.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
for i := 0; i+1 < len(n.Content); i += 2 {
|
||||||
|
k, v := n.Content[i], n.Content[i+1]
|
||||||
|
name := k.Value
|
||||||
|
child := name
|
||||||
|
if path != "" {
|
||||||
|
child = path + "." + name
|
||||||
|
}
|
||||||
|
if k.Style == 0 && yaml11Booleans[name] {
|
||||||
|
*found = append(*found, booleanLikeKey{path: child, line: k.Line})
|
||||||
|
}
|
||||||
|
walkKeys(v, child, found)
|
||||||
|
}
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
for i, c := range n.Content {
|
||||||
|
walkKeys(c, fmt.Sprintf("%s[%d]", path, i), found)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Scalar, alias and document nodes carry no mapping keys to check.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateNoBooleanLikeKeys warns about keys that a template cannot reach by the
|
||||||
|
// name the chart author wrote.
|
||||||
|
func validateNoBooleanLikeKeys(valuesPath string) error {
|
||||||
|
data, err := os.ReadFile(valuesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil // absence and unreadability are reported by other rules
|
||||||
|
}
|
||||||
|
found, err := findBooleanLikeKeys(data)
|
||||||
|
if err != nil || len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(found))
|
||||||
|
for _, k := range found {
|
||||||
|
names = append(names, k.String())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("key(s) %s are read as booleans and become %q or %q in the values; "+
|
||||||
|
"a template cannot reach them under the original name and --set will not override them. Quote the key to keep it",
|
||||||
|
strings.Join(names, ", "), "true", "false")
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindBooleanLikeKeys(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
doc string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "plain keys that YAML 1.1 reads as booleans",
|
||||||
|
// A Grafana dashboard pasted into values is the common way to hit this.
|
||||||
|
doc: "gridPos:\n h: 8\n w: 12\n x: 0\n y: 0\n",
|
||||||
|
want: []string{"gridPos.y"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inside a sequence",
|
||||||
|
doc: "panels:\n - title: cpu\n on: true\n",
|
||||||
|
want: []string{"panels[0].on"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every affected spelling",
|
||||||
|
doc: "a:\n y: 1\n Y: 1\n yes: 1\n no: 1\n N: 1\n off: 1\n ON: 1\n True: 1\n",
|
||||||
|
want: []string{"a.y", "a.Y", "a.yes", "a.no", "a.N", "a.off", "a.ON", "a.True"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted keys survive and are not reported",
|
||||||
|
doc: "a:\n \"y\": 1\n 'on': 2\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ordinary keys",
|
||||||
|
doc: "replicaCount: 1\nimage:\n tag: latest\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty document",
|
||||||
|
doc: "",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unparsable document is left to the values rule",
|
||||||
|
doc: "a:\n\t- broken\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
found, err := findBooleanLikeKeys([]byte(tt.doc))
|
||||||
|
require.NoError(t, err)
|
||||||
|
paths := make([]string, 0, len(found))
|
||||||
|
for _, k := range found {
|
||||||
|
paths = append(paths, k.path)
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, tt.want, paths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNoBooleanLikeKeys(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
affected := filepath.Join(dir, "values.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(affected, []byte("gridPos:\n x: 0\n y: 0\n"), 0o644))
|
||||||
|
err := validateNoBooleanLikeKeys(affected)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "gridPos.y")
|
||||||
|
assert.Contains(t, err.Error(), "line 3")
|
||||||
|
|
||||||
|
clean := filepath.Join(dir, "clean.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(clean, []byte("gridPos:\n x: 0\n \"y\": 0\n"), 0o644))
|
||||||
|
assert.NoError(t, validateNoBooleanLikeKeys(clean))
|
||||||
|
|
||||||
|
// A missing file is reported by validateValuesFileExistence, not here.
|
||||||
|
assert.NoError(t, validateNoBooleanLikeKeys(filepath.Join(dir, "nope.yaml")))
|
||||||
|
}
|
||||||
Loading…
Reference in new issue