v1: Fix --state-values-set to override values of environments colocated with releases (#705)

This commit is contained in:
yxxhero 2023-03-05 16:03:00 +08:00 committed by GitHub
parent 522392c08c
commit 95c56d87fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 364 additions and 37 deletions

View File

@ -117,8 +117,8 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", app.DefaultHelmBinary, "Path to the helm binary")
fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.yaml.gotmpl` or `helmfile.d`(means `helmfile.d/*.yaml` or `helmfile.d/*.yaml.gotmpl`) in this preference. Specify - to load the config from the standard input.")
fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. defaults to "default"`)
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file")
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).")
fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file. Used to override .Values within the helmfile template (not values template).")
fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "Silence output. Equivalent to log-level warn")
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", "Set kubectl context. Uses current context by default")
fs.BoolVar(&globalOptions.Debug, "debug", false, "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect")

View File

@ -553,8 +553,8 @@ Flags:
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"
--state-values-file stringArray specify state values in a YAML file
--state-values-set stringArray set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--state-values-file stringArray specify state values in a YAML file. Used to override .Values within the helmfile template (not values template).
--state-values-set stringArray set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).
-v, --version version for helmfile
Use "helmfile [command] --help" for more information about a command.

View File

@ -100,7 +100,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
return st, nil
}
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
func (ld *desiredStateLoader) loadFile(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
path, err := ld.remote.Locate(file)
if err != nil {
return nil, fmt.Errorf("locate: %v", err)
@ -109,7 +109,7 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba
ld.logger.Debugf("fetched remote \"%s\" to local cache \"%s\" and loading the latter...", file, path)
}
file = path
return ld.loadFileWithOverrides(inheritedEnv, nil, baseDir, file, evaluateBases)
return ld.loadFileWithOverrides(inheritedEnv, overrodeEnv, baseDir, file, evaluateBases)
}
func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
@ -157,16 +157,24 @@ func (a *desiredStateLoader) underlying() *state.StateCreator {
}
func (a *desiredStateLoader) rawLoad(yaml []byte, baseDir, file string, evaluateBases bool, env, overrodeEnv *environment.Environment) (*state.HelmState, error) {
merged, err := env.Merge(overrodeEnv)
if err != nil {
return nil, err
}
var st *state.HelmState
var err error
if runtime.V1Mode {
st, err = a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, env, overrodeEnv)
if err != nil {
return nil, err
}
} else {
merged, err := env.Merge(overrodeEnv)
if err != nil {
return nil, err
}
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, merged)
if err != nil {
return nil, err
st, err = a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, merged, nil)
if err != nil {
return nil, err
}
}
helmfiles, err := st.ExpandedHelmfiles()
if err != nil {
return nil, err

View File

@ -22,8 +22,12 @@ func prependLineNumbers(text string) string {
return buf.String()
}
func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
tmplData := state.NewEnvironmentTemplateData(*firstPassEnv, r.namespace, map[string]interface{}{})
func (r *desiredStateLoader) renderPrestate(firstPassEnv, overrode *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
initEnv, err := firstPassEnv.Merge(overrode)
if err != nil {
return firstPassEnv, nil
}
tmplData := state.NewEnvironmentTemplateData(*initEnv, r.namespace, map[string]interface{}{})
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
// parse as much as we can, tolerate errors, this is a preparse
@ -49,7 +53,7 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
c := r.underlying()
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
prestate, err := c.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv, overrode)
if err != nil {
if _, ok := err.(*state.StateLoadError); ok {
r.logger.Debugf("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
@ -85,7 +89,7 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
}
r.logger.Debugf("%srendering starting for \"%s\": inherited=%v, overrode=%v", phase, filename, inherited, overrode)
initEnv, err := inherited.Merge(overrode)
initEnv, err := inherited.Merge(nil)
if err != nil {
return nil, err
}
@ -99,7 +103,10 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
if runtime.V1Mode {
var err error
finalEnv = initEnv
finalEnv, err = initEnv.Merge(overrode)
if err != nil {
return nil, err
}
vals, err = finalEnv.GetMergedValues()
if err != nil {
@ -107,8 +114,11 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
}
} else {
r.logger.Debugf("first-pass uses: %v", initEnv)
renderedEnv, prestate := r.renderPrestate(initEnv, baseDir, filename, content)
firstPassEnv, err := initEnv.Merge(nil)
if err != nil {
return nil, err
}
renderedEnv, prestate := r.renderPrestate(firstPassEnv, overrode, baseDir, filename, content)
r.logger.Debugf("first-pass produced: %v", renderedEnv)

View File

@ -47,7 +47,7 @@ type StateCreator struct {
Strict bool
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
LoadFile func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
getHelm func(*HelmState) helmexec.Interface
@ -138,10 +138,10 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
}
// LoadEnvValues loads environment values files relative to the `baseDir`
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment, failOnMissingEnv bool) (*HelmState, error) {
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv, overrode *environment.Environment, failOnMissingEnv bool) (*HelmState, error) {
state := *target
e, err := c.loadEnvValues(&state, env, failOnMissingEnv, ctxEnv)
e, err := c.loadEnvValues(&state, env, failOnMissingEnv, ctxEnv, overrode)
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
@ -162,7 +162,7 @@ func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *envi
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
// evaluateBases=true means that this is NOT a base helmfile
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues *environment.Environment) (*HelmState, error) {
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues, overrode *environment.Environment) (*HelmState, error) {
state, err := c.Parse(content, baseDir, file)
if err != nil {
return nil, err
@ -173,13 +173,13 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
}
} else {
state, err = c.loadBases(envValues, state, baseDir)
state, err = c.loadBases(envValues, overrode, state, baseDir)
if err != nil {
return nil, err
}
}
state, err = c.LoadEnvValues(state, envName, envValues, evaluateBases)
state, err = c.LoadEnvValues(state, envName, envValues, overrode, evaluateBases)
if err != nil {
return nil, err
}
@ -195,10 +195,10 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
return state, nil
}
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
layers := []*HelmState{}
for _, b := range st.Bases {
base, err := c.LoadFile(envValues, baseDir, b, false)
base, err := c.LoadFile(envValues, overrodeEnv, baseDir, b, false)
if err != nil {
return nil, err
}
@ -216,7 +216,7 @@ func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmSta
}
// nolint: unparam
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv *environment.Environment) (*environment.Environment, error) {
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv, overrode *environment.Environment) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name]
if ok {
@ -250,13 +250,23 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
newEnv := &environment.Environment{Name: name, Values: envVals}
if ctxEnv != nil {
intEnv := *ctxEnv
intCtxEnv := *ctxEnv
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
if err := mergo.Merge(&intCtxEnv, newEnv, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
}
newEnv = &intEnv
newEnv = &intCtxEnv
}
if overrode != nil {
intOverrodeEnv := *newEnv
if err := mergo.Merge(&intOverrodeEnv, overrode, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, fmt.Errorf("error while merging environment overrode values for \"%s\": %v", name, err)
}
newEnv = &intOverrodeEnv
}
return newEnv, nil

View File

@ -20,7 +20,7 @@ func createFromYaml(content []byte, file string, env string, logger *zap.Sugared
fs: filesystem.DefaultFileSystem(),
Strict: true,
}
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil)
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil, nil)
}
func TestReadFromYaml(t *testing.T) {
@ -84,7 +84,7 @@ func (testEnv stateTestEnv) MustLoadStateWithEnableLiveOutput(t *testing.T, file
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, enableLiveOutput, "").
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil)
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -154,7 +154,7 @@ releaseNamespace: mynamespace
Name: "production",
}
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, false, "").
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, &env)
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, &env, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -241,7 +241,7 @@ overrideNamespace: myns
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, false, "").
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

View File

@ -0,0 +1,29 @@
cli-overwrite-environment-values_input_dir="${cases_dir}/cli-overwrite-environment-values/input"
cli-overwrite-environment-values_output_dir="${cases_dir}/cli-overwrite-environment-values/output"
cli_overwrite_environment_values_tmp=$(mktemp -d)
cli_overwrite_environment_values_reverse=${cli_overwrite_environment_values_tmp}/cli.environment.override.build.yaml
case_title="cli overwrite environment values"
if [[ ${HELMFILE_V1MODE} = true ]]; then
test_start "$case_title for v1"
info "Comparing ${case_title} for v1 output ${cli_overwrite_environment_values_reverse} with ${cli-overwrite-environment-values_output_dir}/overwritten.yaml"
for i in $(seq 10); do
info "Comparing build/cli-overwrite-environment-values #$i"
${helmfile} -f ${cli-overwrite-environment-values_input_dir}/input_v1.yaml.gotmpl template --state-values-set ns=test3 > ${cli_overwrite_environment_values_reverse} || fail "\"helmfile template\" shouldn't fail"
diff -u ${cli-overwrite-environment-values_output_dir}/output_v1.yaml ${cli_overwrite_environment_values_reverse} || fail "\"helmfile template\" should be consistent"
echo code=$?
done
test_pass "cli overwrite environment values for v1"
else
test_start "${case_title}"
info "Comparing ${case_title} output ${cli_overwrite_environment_values_reverse} with ${cli-overwrite-environment-values_output_dir}/overwritten.yaml"
for i in $(seq 10); do
info "Comparing build/cli-overwrite-environment-values #$i"
${helmfile} -f ${cli-overwrite-environment-values_input_dir}/input_v1.yaml.gotmpl template --state-values-set ns=test3 > ${cli_overwrite_environment_values_reverse} || fail "\"helmfile template\" shouldn't fail"
diff -u ${cli-overwrite-environment-values_output_dir}/output_v1.yaml ${cli_overwrite_environment_values_reverse} || fail "\"helmfile template\" should be consistent"
echo code=$?
done
test_pass "${case_title}"
fi

View File

@ -0,0 +1,5 @@
chartifyTempDir: environment_overwrite_values
helmfileArgs:
- template
- --state-values-set
- ns=test3

View File

@ -0,0 +1,17 @@
environments:
default:
values:
- base.yaml
- override.yaml
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: test
chart: bitnami/nginx
namespace: {{ .Values.ns }}
version: 13.2.27
values:
- values.yaml.gotmpl

View File

@ -0,0 +1,17 @@
environments:
default:
values:
- base.yaml
- override.yaml
---
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: test
chart: bitnami/nginx
namespace: {{ .Values.ns }}
version: 13.2.27
values:
- values.yaml.gotmpl

View File

@ -0,0 +1,2 @@
image:
tag: {{ .Values.ns }}

View File

@ -0,0 +1,116 @@
Warning: environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part
Warning: environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part
Adding repo bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
Templating release=test, chart=bitnami/nginx
---
# Source: nginx/templates/svc.yaml
apiVersion: v1
kind: Service
metadata:
name: test-nginx
namespace: "test3"
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
annotations:
spec:
type: LoadBalancer
sessionAffinity: None
externalTrafficPolicy: "Cluster"
ports:
- name: http
port: 80
targetPort: http
selector:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-nginx
namespace: "test3"
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
strategy:
rollingUpdate: {}
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
template:
metadata:
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
annotations:
spec:
automountServiceAccountToken: false
shareProcessNamespace: false
serviceAccountName: default
affinity:
podAffinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
topologyKey: kubernetes.io/hostname
weight: 1
nodeAffinity:
hostNetwork: false
hostIPC: false
initContainers:
containers:
- name: nginx
image: docker.io/bitnami/nginx:test3
imagePullPolicy: "IfNotPresent"
env:
- name: BITNAMI_DEBUG
value: "false"
- name: NGINX_HTTP_PORT_NUMBER
value: "8080"
envFrom:
ports:
- name: http
containerPort: 8080
livenessProbe:
failureThreshold: 6
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
tcpSocket:
port: http
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
tcpSocket:
port: http
resources:
limits: {}
requests: {}
volumeMounts:
volumes:

View File

@ -0,0 +1,110 @@
---
# Source: nginx/templates/svc.yaml
apiVersion: v1
kind: Service
metadata:
name: test-nginx
namespace: "test3"
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
annotations:
spec:
type: LoadBalancer
sessionAffinity: None
externalTrafficPolicy: "Cluster"
ports:
- name: http
port: 80
targetPort: http
selector:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-nginx
namespace: "test3"
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
strategy:
rollingUpdate: {}
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
template:
metadata:
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-13.2.27
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: Helm
annotations:
spec:
automountServiceAccountToken: false
shareProcessNamespace: false
serviceAccountName: default
affinity:
podAffinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: test
topologyKey: kubernetes.io/hostname
weight: 1
nodeAffinity:
hostNetwork: false
hostIPC: false
initContainers:
containers:
- name: nginx
image: docker.io/bitnami/nginx:test3
imagePullPolicy: "IfNotPresent"
env:
- name: BITNAMI_DEBUG
value: "false"
- name: NGINX_HTTP_PORT_NUMBER
value: "8080"
envFrom:
ports:
- name: http
containerPort: 8080
livenessProbe:
failureThreshold: 6
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
tcpSocket:
port: http
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
tcpSocket:
port: http
resources:
limits: {}
requests: {}
volumeMounts:
volumes:

View File

@ -0,0 +1 @@
https://github.com/helmfile/helmfile/issues/700