Commit 55b83f3a authored by Matt Butcher's avatar Matt Butcher Committed by GitHub

Merge pull request #1656 from thomastaylor312/feat/multiple-values

feat(helm): add support for multiple values files
parents bae2dce3 1a1d84ce
...@@ -55,6 +55,12 @@ or ...@@ -55,6 +55,12 @@ or
$ helm install --set name=prod ./redis $ helm install --set name=prod ./redis
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
contained a key called 'Test', the value set in override.yaml would take precedence:
$ helm install -f myvalues.yaml -f override.yaml ./redis
To check the generated manifests of a release without installing the chart, To check the generated manifests of a release without installing the chart,
the '--debug' and '--dry-run' flags can be combined. This will still require a the '--debug' and '--dry-run' flags can be combined. This will still require a
round-trip to the Tiller server. round-trip to the Tiller server.
...@@ -86,7 +92,7 @@ charts in a repository, use 'helm search'. ...@@ -86,7 +92,7 @@ charts in a repository, use 'helm search'.
type installCmd struct { type installCmd struct {
name string name string
namespace string namespace string
valuesFile string valueFiles valueFiles
chartPath string chartPath string
dryRun bool dryRun bool
disableHooks bool disableHooks bool
...@@ -100,6 +106,23 @@ type installCmd struct { ...@@ -100,6 +106,23 @@ type installCmd struct {
version string version string
} }
type valueFiles []string
func (v *valueFiles) String() string {
return fmt.Sprint(*v)
}
func (v *valueFiles) Type() string {
return "valueFiles"
}
func (v *valueFiles) Set(value string) error {
for _, filePath := range strings.Split(value, ",") {
*v = append(*v, filePath)
}
return nil
}
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
inst := &installCmd{ inst := &installCmd{
out: out, out: out,
...@@ -126,7 +149,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { ...@@ -126,7 +149,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
f.StringVarP(&inst.valuesFile, "values", "f", "", "specify values in a YAML file") f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you") f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you")
f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into") f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into")
f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install")
...@@ -197,19 +220,54 @@ func (i *installCmd) run() error { ...@@ -197,19 +220,54 @@ func (i *installCmd) run() error {
return nil return nil
} }
// Merges source and destination map, preferring values from the source map
func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = v
continue
}
nextMap, ok := v.(map[string]interface{})
// If it isn't another map, overwrite the value
if !ok {
dest[k] = v
continue
}
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = nextMap
continue
}
// Edge case: If the key exists in the destination, but isn't a map
destMap, isMap := dest[k].(map[string]interface{})
// If the source map has a map for this key, prefer it
if !isMap {
dest[k] = v
continue
}
// If we got to this point, it is a map in both, so merge them
dest[k] = mergeValues(destMap, nextMap)
}
return dest
}
func (i *installCmd) vals() ([]byte, error) { func (i *installCmd) vals() ([]byte, error) {
base := map[string]interface{}{} base := map[string]interface{}{}
// User specified a values file via -f/--values // User specified a values files via -f/--values
if i.valuesFile != "" { for _, filePath := range i.valueFiles {
bytes, err := ioutil.ReadFile(i.valuesFile) currentMap := map[string]interface{}{}
bytes, err := ioutil.ReadFile(filePath)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
if err := yaml.Unmarshal(bytes, &base); err != nil { if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
return []byte{}, fmt.Errorf("failed to parse %s: %s", i.valuesFile, err) return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
} }
// Merge with the previous map
base = mergeValues(base, currentMap)
} }
if err := strvals.ParseInto(i.values, base); err != nil { if err := strvals.ParseInto(i.values, base); err != nil {
......
...@@ -18,6 +18,7 @@ package main ...@@ -18,6 +18,7 @@ package main
import ( import (
"io" "io"
"reflect"
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
...@@ -51,6 +52,22 @@ func TestInstall(t *testing.T) { ...@@ -51,6 +52,22 @@ func TestInstall(t *testing.T) {
resp: releaseMock(&releaseOptions{name: "virgil"}), resp: releaseMock(&releaseOptions{name: "virgil"}),
expected: "virgil", expected: "virgil",
}, },
// Install, values from yaml
{
name: "install with values",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("-f testdata/testcharts/alpine/extra_values.yaml", " "),
resp: releaseMock(&releaseOptions{name: "virgil"}),
expected: "virgil",
},
// Install, values from multiple yaml
{
name: "install with values",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("-f testdata/testcharts/alpine/extra_values.yaml -f testdata/testcharts/alpine/more_values.yaml", " "),
resp: releaseMock(&releaseOptions{name: "virgil"}),
expected: "virgil",
},
// Install, no charts // Install, no charts
{ {
name: "install with no chart specified", name: "install with no chart specified",
...@@ -172,3 +189,58 @@ func TestNameTemplate(t *testing.T) { ...@@ -172,3 +189,58 @@ func TestNameTemplate(t *testing.T) {
} }
} }
} }
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 := mergeValues(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 = mergeValues(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 = mergeValues(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 = mergeValues(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)
}
}
...@@ -12,6 +12,7 @@ metadata: ...@@ -12,6 +12,7 @@ metadata:
release: {{.Release.Name | quote }} release: {{.Release.Name | quote }}
# This makes it easy to audit chart usage. # This makes it easy to audit chart usage.
chart: "{{.Chart.Name}}-{{.Chart.Version}}" chart: "{{.Chart.Name}}-{{.Chart.Version}}"
values: {{.Values.test.Name}}
annotations: annotations:
"helm.sh/created": {{.Release.Time.Seconds | quote }} "helm.sh/created": {{.Release.Time.Seconds | quote }}
spec: spec:
......
...@@ -40,6 +40,12 @@ version will be specified unless the '--version' flag is set. ...@@ -40,6 +40,12 @@ version will be specified unless the '--version' flag is set.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use either the '--values' flag and pass in a file
or use the '--set' flag and pass configuration from the command line. or use the '--set' flag and pass configuration from the command line.
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
contained a key called 'Test', the value set in override.yaml would take precedence:
$ helm install -f myvalues.yaml -f override.yaml ./redis
` `
type upgradeCmd struct { type upgradeCmd struct {
...@@ -49,7 +55,7 @@ type upgradeCmd struct { ...@@ -49,7 +55,7 @@ type upgradeCmd struct {
client helm.Interface client helm.Interface
dryRun bool dryRun bool
disableHooks bool disableHooks bool
valuesFile string valueFiles valueFiles
values string values string
verify bool verify bool
keyring string keyring string
...@@ -84,7 +90,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { ...@@ -84,7 +90,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
f.StringVarP(&upgrade.valuesFile, "values", "f", "", "path to a values YAML file") f.VarP(&upgrade.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade")
f.StringVar(&upgrade.values, "set", "", "set values on the command line. Separate values with commas: key1=val1,key2=val2") f.StringVar(&upgrade.values, "set", "", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks") f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks")
...@@ -121,7 +127,7 @@ func (u *upgradeCmd) run() error { ...@@ -121,7 +127,7 @@ func (u *upgradeCmd) run() error {
client: u.client, client: u.client,
out: u.out, out: u.out,
name: u.release, name: u.release,
valuesFile: u.valuesFile, valueFiles: u.valueFiles,
dryRun: u.dryRun, dryRun: u.dryRun,
verify: u.verify, verify: u.verify,
disableHooks: u.disableHooks, disableHooks: u.disableHooks,
...@@ -164,16 +170,19 @@ func (u *upgradeCmd) run() error { ...@@ -164,16 +170,19 @@ func (u *upgradeCmd) run() error {
func (u *upgradeCmd) vals() ([]byte, error) { func (u *upgradeCmd) vals() ([]byte, error) {
base := map[string]interface{}{} base := map[string]interface{}{}
// User specified a values file via -f/--values // User specified a values files via -f/--values
if u.valuesFile != "" { for _, filePath := range u.valueFiles {
bytes, err := ioutil.ReadFile(u.valuesFile) currentMap := map[string]interface{}{}
bytes, err := ioutil.ReadFile(filePath)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
if err := yaml.Unmarshal(bytes, &base); err != nil { if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
return []byte{}, fmt.Errorf("failed to parse %s: %s", u.valuesFile, err) return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
} }
// Merge with the previous map
base = mergeValues(base, currentMap)
} }
if err := strvals.ParseInto(u.values, base); err != nil { if err := strvals.ParseInto(u.values, base); err != nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment