feat: override state(former "enviroment") values via command-line args (#644)

The addition of `--set k1=v1,k2=v2` and `--values file1 --values file2` was originally planned in #361.

But it turned out we already had `--values` for existing helmfile commands like `sync`. Duplicated flags doesn't work, obviously.

So this actually add `--state-values-set k1=v1,k2=v2` and `--set-values-file file1 --set-values-file file2`.

They are called "state" values according to the discussion we had at #640

Resolves #361
This commit is contained in:
KUOKA Yusuke
2019-06-04 11:03:01 +09:00
committed by GitHub
parent e2d6dc4afa
commit 1d3f5f8a33
8 changed files with 191 additions and 11 deletions
+30 -2
View File
@@ -26,6 +26,8 @@ type App struct {
Selectors []string
HelmBinary string
Args string
ValuesFiles []string
Set map[string]interface{}
FileOrDir string
@@ -52,6 +54,8 @@ func New(conf ConfigProvider) *App {
HelmBinary: conf.HelmBinary(),
Args: conf.Args(),
FileOrDir: conf.FileOrDir(),
ValuesFiles: conf.ValuesFiles(),
Set: conf.Set(),
})
}
@@ -237,10 +241,16 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He
return ld.Load(file, op)
}
func (a *App) visitStates(fileOrDir string, opts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f, d string) error {
opts := defOpts.DeepCopy()
if opts.CalleePath == "" {
opts.CalleePath = f
}
st, err := a.loadDesiredStateFromYaml(f, opts)
sigs := make(chan os.Signal, 1)
@@ -343,7 +353,25 @@ func (a *App) ForEachState(do func(*Run) []error) error {
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
opts := LoadOpts{Selectors: a.Selectors}
opts := LoadOpts{
Selectors: a.Selectors,
}
envvals := []interface{}{}
if a.ValuesFiles != nil {
for i := range a.ValuesFiles {
envvals = append(envvals, a.ValuesFiles[i])
}
}
if a.Set != nil {
envvals = append(envvals, a.Set)
}
if len(envvals) > 0 {
opts.Environment.OverrideValues = envvals
}
err := a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(st.Selectors) > 0 {
+62
View File
@@ -838,6 +838,68 @@ releases:
}
}
func TestVisitDesiredStatesWithReleasesFiltered_StateValueOverrides(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- values.yaml
---
releases:
- name: {{ .Environment.Values.foo }}-{{ .Environment.Values.bar }}-{{ .Environment.Values.baz }}
chart: stable/zipkin
`,
"/path/to/values.yaml": `
foo: foo
bar: bar
baz: baz
`,
"/path/to/overrides.yaml": `
foo: "foo1"
bar: "bar1"
`,
}
testcases := []struct {
expected string
}{
{expected: "foo1-bar2-baz1"},
}
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
for _, r := range st.Releases {
actual = append(actual, r.Name)
}
return []error{}
}
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: false,
Namespace: "",
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]interface{}{"bar": "bar2", "baz": "baz1"},
}, files)
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
}
if actual[0] != testcase.expected {
t.Errorf("unexpected result: expected=%s, got=%s", testcase.expected, actual[0])
}
}
}
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
+2
View File
@@ -10,6 +10,8 @@ type ConfigProvider interface {
KubeContext() string
Namespace() string
Selectors() []string
Set() map[string]interface{}
ValuesFiles() []string
Env() string
loggingConfig
-6
View File
@@ -27,12 +27,6 @@ type desiredStateLoader struct {
logger *zap.SugaredLogger
}
type LoadOpts struct {
Selectors []string
Environment state.SubhelmfileEnvironmentSpec
CalleePath string
}
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
var overrodeEnv *environment.Environment
+28
View File
@@ -0,0 +1,28 @@
package app
import (
"github.com/roboll/helmfile/pkg/state"
"gopkg.in/yaml.v2"
)
type LoadOpts struct {
Selectors []string
Environment state.SubhelmfileEnvironmentSpec
// CalleePath is the absolute path to the file being loaded
CalleePath string
}
func (o LoadOpts) DeepCopy() LoadOpts {
bytes, err := yaml.Marshal(o)
if err != nil {
panic(err)
}
new := LoadOpts{}
if err := yaml.Unmarshal(bytes, &new); err != nil {
panic(err)
}
return new
}
+25
View File
@@ -48,3 +48,28 @@ func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
}
return new, nil
}
func Set(m map[string]interface{}, key []string, value string) map[string]interface{} {
if len(key) == 0 {
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
}
k := key[0]
if len(key) == 1 {
m[k] = value
return m
}
remain := key[1:]
nested, ok := m[k]
if !ok {
new_m := map[string]interface{}{}
nested = Set(new_m, remain, value)
}
m[k] = nested
return m
}