Skip to content

Commit

Permalink
feat: override state(former "enviroment") values via command-line args (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
mumoshu authored Jun 4, 2019
1 parent e2d6dc4 commit 1d3f5f8
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 11 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
41 changes: 39 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -380,6 +389,8 @@ func main() {

type configImpl struct {
c *cli.Context

set map[string]interface{}
}

func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand Down
32 changes: 30 additions & 2 deletions pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type App struct {
Selectors []string
HelmBinary string
Args string
ValuesFiles []string
Set map[string]interface{}

FileOrDir string

Expand All @@ -52,6 +54,8 @@ func New(conf ConfigProvider) *App {
HelmBinary: conf.HelmBinary(),
Args: conf.Args(),
FileOrDir: conf.FileOrDir(),
ValuesFiles: conf.ValuesFiles(),
Set: conf.Set(),
})
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
62 changes: 62 additions & 0 deletions pkg/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pkg/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type ConfigProvider interface {
KubeContext() string
Namespace() string
Selectors() []string
Set() map[string]interface{}
ValuesFiles() []string
Env() string

loggingConfig
Expand Down
6 changes: 0 additions & 6 deletions pkg/app/desired_state_file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions pkg/app/load_opts.go
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions pkg/maputil/maputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 1d3f5f8

Please sign in to comment.