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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 11 deletions

View File

@ -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

41
main.go
View File

@ -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")
}

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 {

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:

View File

@ -10,6 +10,8 @@ type ConfigProvider interface {
KubeContext() string
Namespace() string
Selectors() []string
Set() map[string]interface{}
ValuesFiles() []string
Env() string
loggingConfig

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
pkg/app/load_opts.go Normal file
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
}

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
}