diff --git a/README.md b/README.md index 30d71454..4cbc1383 100644 --- a/README.md +++ b/README.md @@ -320,9 +320,10 @@ USAGE: helmfile [global options] command [command options] [arguments...] VERSION: - v0.52.0 + v0.70.0 COMMANDS: + deps update charts based on the contents of requirements.yaml repos sync repositories from state file (helm repo add && helm repo update) charts DEPRECATED: sync releases from state file (helm upgrade --install) diff diff releases from state file against env (helm diff) @@ -339,6 +340,8 @@ GLOBAL OPTIONS: --helm-binary value, -b value path to helm binary --file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference --environment default, -e default specify the environment name. defaults to default + --state-values-set value set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --state-values-file value specify state values in a YAML file --quiet, -q Silence output. Equivalent to log-level warn --kube-context value Set kubectl context. Uses current context by default --log-level value Set log level, default info @@ -347,6 +350,7 @@ GLOBAL OPTIONS: A release must match all labels in a group in order to be used. Multiple groups can be specified at once. --selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases. The name of a release can be used as a label. --selector name=myrelease + --allow-no-matching-release Do not exit with an error code if the provided selector has no matching releases. --interactive, -i Request confirmation before attempting to modify clusters --help, -h show help --version, -v print the version diff --git a/main.go b/main.go index 6f499303..268d036c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/roboll/helmfile/pkg/app" "github.com/roboll/helmfile/pkg/helmexec" + "github.com/roboll/helmfile/pkg/maputil" "github.com/roboll/helmfile/pkg/state" "github.com/urfave/cli" "go.uber.org/zap" @@ -57,6 +58,14 @@ func main() { Name: "environment, e", Usage: "specify the environment name. defaults to `default`", }, + cli.StringSliceFlag{ + Name: "state-values-set", + Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", + }, + cli.StringSliceFlag{ + Name: "state-values-file", + Usage: "specify state values in a YAML file", + }, cli.BoolFlag{ Name: "quiet, q", Usage: "Silence output. Equivalent to log-level warn", @@ -380,6 +389,8 @@ func main() { type configImpl struct { c *cli.Context + + set map[string]interface{} } func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { @@ -388,9 +399,27 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) } - return configImpl{ + conf := configImpl{ c: c, - }, nil + } + + optsSet := c.GlobalStringSlice("state-values-set") + if len(optsSet) > 0 { + set := map[string]interface{}{} + for i := range optsSet { + ops := strings.Split(optsSet[i], ",") + for j := range ops { + op := strings.Split(ops[j], "=") + k := strings.Split(op[0], ".") + v := op[1] + + set = maputil.Set(set, k, v) + } + } + conf.set = set + } + + return conf, nil } func (c configImpl) Values() []string { @@ -461,6 +490,14 @@ func (c configImpl) Selectors() []string { return c.c.GlobalStringSlice("selector") } +func (c configImpl) Set() map[string]interface{} { + return c.set +} + +func (c configImpl) ValuesFiles() []string { + return c.c.GlobalStringSlice("state-values-file") +} + func (c configImpl) Interactive() bool { return c.c.GlobalBool("interactive") } diff --git a/pkg/app/app.go b/pkg/app/app.go index ae0b7233..924f7009 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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 { diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index b42728f2..8321a159 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -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: diff --git a/pkg/app/config.go b/pkg/app/config.go index e24833ca..c0fa96db 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -10,6 +10,8 @@ type ConfigProvider interface { KubeContext() string Namespace() string Selectors() []string + Set() map[string]interface{} + ValuesFiles() []string Env() string loggingConfig diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index e6cd8406..19820c34 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -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 diff --git a/pkg/app/load_opts.go b/pkg/app/load_opts.go new file mode 100644 index 00000000..f8403d0f --- /dev/null +++ b/pkg/app/load_opts.go @@ -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 +} diff --git a/pkg/maputil/maputil.go b/pkg/maputil/maputil.go index c26a207c..66b1cd0e 100644 --- a/pkg/maputil/maputil.go +++ b/pkg/maputil/maputil.go @@ -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 +}