feat: support multi-document values files for default chart values

Signed-off-by: lubingtan <bingtanlu@gmail.com>
pull/13655/head
lubingtan 8 months ago
parent 0d36cb664a
commit 92087f6e33

@ -17,14 +17,17 @@ limitations under the License.
package loader package loader
import ( import (
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart"
@ -104,13 +107,11 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
return c, errors.Wrap(err, "cannot load Chart.lock") return c, errors.Wrap(err, "cannot load Chart.lock")
} }
case f.Name == "values.yaml": case f.Name == "values.yaml":
c.Values = make(map[string]interface{}) values, err := LoadValues(f.Data)
if err := yaml.Unmarshal(f.Data, &c.Values, func(d *json.Decoder) *json.Decoder { if err != nil {
d.UseNumber()
return d
}); err != nil {
return c, errors.Wrap(err, "cannot load values.yaml") return c, errors.Wrap(err, "cannot load values.yaml")
} }
c.Values = values
case f.Name == "values.schema.json": case f.Name == "values.schema.json":
c.Schema = f.Data c.Schema = f.Data
@ -205,3 +206,45 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
return c, nil return c, nil
} }
func LoadValues(data []byte) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data)))
for {
currentMap := map[string]interface{}{}
raw, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrap(err, "error reading yaml document")
}
if err := yaml.Unmarshal(raw, &currentMap, func(d *json.Decoder) *json.Decoder {
d.UseNumber()
return d
}); err != nil {
return nil, errors.Wrap(err, "cannot unmarshal yaml document")
}
values = MergeMaps(values, currentMap)
}
return values, nil
}
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = MergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}

@ -24,6 +24,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
@ -488,6 +489,118 @@ func TestLoadInvalidArchive(t *testing.T) {
} }
} }
func TestLoadValues(t *testing.T) {
testDatas := []struct {
name string
data []byte
expctedValues map[string]interface{}
}{
{
name: "It should load values correctly",
data: []byte(`
foo:
image: foo:v1
bar:
version: v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
"image": "foo:v1",
},
"bar": map[string]interface{}{
"version": "v2",
},
},
},
{
name: "It should load values correctly with multiple documents in one file",
data: []byte(`
foo:
image: foo:v1
bar:
version: v2
---
foo:
image: foo:v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
"image": "foo:v2",
},
"bar": map[string]interface{}{
"version": "v2",
},
},
},
}
for _, testData := range testDatas {
t.Run(testData.name, func(tt *testing.T) {
values, err := LoadValues(testData.data)
if err != nil {
tt.Fatal(err)
}
if !reflect.DeepEqual(values, testData.expctedValues) {
tt.Errorf("Expected values: %v, got %v", testData.expctedValues, values)
}
})
}
}
func TestMergeValues(t *testing.T) {
nestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
"testing": "fun",
}
testMap := MergeMaps(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
testMap = MergeMaps(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
testMap = MergeMaps(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
}
func verifyChart(t *testing.T, c *chart.Chart) { func verifyChart(t *testing.T, c *chart.Chart) {
t.Helper() t.Helper()
if c.Name() == "" { if c.Name() == "" {

@ -17,7 +17,6 @@ limitations under the License.
package values package values
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io" "io"
"net/url" "net/url"
@ -25,8 +24,8 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/strvals" "helm.sh/helm/v4/pkg/strvals"
) )
@ -48,23 +47,16 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
// User specified a values files via -f/--values // User specified a values files via -f/--values
for _, filePath := range opts.ValueFiles { for _, filePath := range opts.ValueFiles {
raw, err := readFile(filePath, p) bytes, err := readFile(filePath, p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
currentMap, err := loader.LoadValues(bytes)
decoder := utilyaml.NewYAMLOrJSONDecoder(bytes.NewReader(raw), 4096) if err != nil {
for {
currentMap := map[string]interface{}{}
if err := decoder.Decode(&currentMap); err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrapf(err, "failed to parse %s", filePath) return nil, errors.Wrapf(err, "failed to parse %s", filePath)
} }
// Merge with the previous map // Merge with the previous map
base = mergeMaps(base, currentMap) base = loader.MergeMaps(base, currentMap)
}
} }
// User specified a value via --set-json // User specified a value via --set-json
@ -123,25 +115,6 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
return base, nil return base, nil
} }
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = mergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}
// readFile load a file from stdin, the local directory, or a remote file with a url. // readFile load a file from stdin, the local directory, or a remote file with a url.
func readFile(filePath string, p getter.Providers) ([]byte, error) { func readFile(filePath string, p getter.Providers) ([]byte, error) {
if strings.TrimSpace(filePath) == "-" { if strings.TrimSpace(filePath) == "-" {

@ -23,61 +23,6 @@ import (
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
) )
func TestMergeMaps(t *testing.T) {
nestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
"testing": "fun",
}
testMap := mergeMaps(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
testMap = mergeMaps(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
testMap = mergeMaps(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
testMap = mergeMaps(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
}
func TestReadFile(t *testing.T) { func TestReadFile(t *testing.T) {
var p getter.Providers var p getter.Providers
filePath := "%a.txt" filePath := "%a.txt"

Loading…
Cancel
Save