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:
parent
e2d6dc4afa
commit
1d3f5f8a33
|
|
@ -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
41
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ type ConfigProvider interface {
|
|||
KubeContext() string
|
||||
Namespace() string
|
||||
Selectors() []string
|
||||
Set() map[string]interface{}
|
||||
ValuesFiles() []string
|
||||
Env() string
|
||||
|
||||
loggingConfig
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue