From 0fc0869671f60c2d5b176492d77a6a629abdc307 Mon Sep 17 00:00:00 2001 From: Yusuke Kuoka Date: Sat, 29 Aug 2020 13:18:17 +0900 Subject: [PATCH] feat: `helmfile build --embed-values` to embed release values and secrets into the output (#1436) --- main.go | 11 ++++- pkg/app/app.go | 27 ++++++++++- pkg/app/app_test.go | 4 ++ pkg/app/config.go | 1 + pkg/state/release.go | 13 +++-- pkg/state/state.go | 79 +++++++++++++++++++++++++++++-- pkg/state/state_exec_tmpl_test.go | 4 +- 7 files changed, 124 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index 6c0e1c55..96603f5e 100644 --- a/main.go +++ b/main.go @@ -467,7 +467,12 @@ func main() { { Name: "build", Usage: "output compiled helmfile state(s) as YAML", - Flags: []cli.Flag{}, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "embed-values", + Usage: "Read all the values files for every release and embed into the output helmfile.yaml", + }, + }, Action: action(func(run *app.App, c configImpl) error { return run.PrintState(c) }), @@ -674,6 +679,10 @@ func (c configImpl) Context() int { return c.c.Int("context") } +func (c configImpl) EmbedValues() bool { + return c.c.Bool("embed-values") +} + func (c configImpl) Logger() *zap.SugaredLogger { return c.c.App.Metadata["logger"].(*zap.SugaredLogger) } diff --git a/pkg/app/app.go b/pkg/app/app.go index 5377aaf4..ea822a72 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -387,12 +387,35 @@ func (a *App) PrintState(c StateConfigProvider) error { err := run.withPreparedCharts("build", state.ChartPrepareOptions{ SkipRepos: true, }, func() { - state, err := run.state.ToYaml() + if c.EmbedValues() { + for i := range run.state.Releases { + r := run.state.Releases[i] + + values, err := run.state.LoadYAMLForEmbedding(r.Values, r.MissingFileHandler, r.ValuesPathPrefix) + if err != nil { + errs = []error{err} + return + } + + run.state.Releases[i].Values = values + + secrets, err := run.state.LoadYAMLForEmbedding(r.Secrets, r.MissingFileHandler, r.ValuesPathPrefix) + if err != nil { + errs = []error{err} + return + } + + run.state.Releases[i].Secrets = secrets + } + } + + stateYaml, err := run.state.ToYaml() if err != nil { errs = []error{err} return } - fmt.Printf("---\n# Source: %s\n\n%+v", run.state.FilePath, state) + + fmt.Printf("---\n# Source: %s\n\n%+v", run.state.FilePath, stateYaml) errs = []error{} }) diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 0e54e823..3c290e1d 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2272,6 +2272,10 @@ func (c configImpl) Concurrency() int { return 1 } +func (c configImpl) EmbedValues() bool { + return false +} + func (c configImpl) Output() string { return c.output } diff --git a/pkg/app/config.go b/pkg/app/config.go index 5ae6078e..4a44542f 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -144,6 +144,7 @@ type StatusesConfigProvider interface { } type StateConfigProvider interface { + EmbedValues() bool } type concurrencyConfig interface { diff --git a/pkg/state/release.go b/pkg/state/release.go index 219fd685..d8a2e387 100644 --- a/pkg/state/release.go +++ b/pkg/state/release.go @@ -133,12 +133,15 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R } } - for i, ts := range result.Secrets { - s, err := renderer.RenderTemplateContentToBuffer([]byte(ts)) - if err != nil { - return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err) + for i, t := range result.Secrets { + switch ts := t.(type) { + case string: + s, err := renderer.RenderTemplateContentToBuffer([]byte(ts)) + if err != nil { + return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err) + } + result.Secrets[i] = s.String() } - result.Secrets[i] = s.String() } if len(result.SetValuesTemplate) > 0 { diff --git a/pkg/state/state.go b/pkg/state/state.go index 38d7de47..486a22b0 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -211,7 +211,7 @@ type ReleaseSpec struct { Namespace string `yaml:"namespace,omitempty"` Labels map[string]string `yaml:"labels,omitempty"` Values []interface{} `yaml:"values,omitempty"` - Secrets []string `yaml:"secrets,omitempty"` + Secrets []interface{} `yaml:"secrets,omitempty"` SetValues []SetValue `yaml:"set,omitempty"` ValuesTemplate []interface{} `yaml:"valuesTemplate,omitempty"` @@ -2262,11 +2262,41 @@ func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string, func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) { var generatedFiles []string - for _, value := range release.Secrets { - paths, skip, err := st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value) - if err != nil { - return nil, err + for _, v := range release.Secrets { + var ( + paths []string + skip bool + err error + ) + + switch value := v.(type) { + case string: + paths, skip, err = st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value) + if err != nil { + return nil, err + } + default: + bs, err := yaml.Marshal(value) + if err != nil { + return nil, err + } + + path, err := ioutil.TempFile(os.TempDir(), "helmfile-embdedded-secrets-*.yaml.enc") + if err != nil { + return nil, err + } + _ = path.Close() + defer func() { + _ = os.Remove(path.Name()) + }() + + if err := ioutil.WriteFile(path.Name(), bs, 0644); err != nil { + return nil, err + } + + paths = []string{path.Name()} } + if skip { continue } @@ -2546,3 +2576,42 @@ func (st *HelmState) ToYaml() (string, error) { return string(result), nil } } + +func (st *HelmState) LoadYAMLForEmbedding(entries []interface{}, missingFileHandler *string, pathPrefix string) ([]interface{}, error) { + var result []interface{} + + for _, v := range entries { + switch t := v.(type) { + case string: + var values map[string]interface{} + + paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", pathPrefix+t) + if err != nil { + return nil, err + } + if skip { + continue + } + + if len(paths) > 1 { + return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary") + } + yamlOrTemplatePath := paths[0] + + yamlBytes, err := st.RenderValuesFileToBytes(yamlOrTemplatePath) + if err != nil { + return nil, fmt.Errorf("failed to render values files \"%s\": %v", t, err) + } + + if err := yaml.Unmarshal(yamlBytes, &values); err != nil { + return nil, err + } + + result = append(result, values) + default: + result = append(result, v) + } + } + + return result, nil +} diff --git a/pkg/state/state_exec_tmpl_test.go b/pkg/state/state_exec_tmpl_test.go index 7ba7acac..25aeeeee 100644 --- a/pkg/state/state_exec_tmpl_test.go +++ b/pkg/state/state_exec_tmpl_test.go @@ -36,7 +36,7 @@ func TestHelmState_executeTemplates(t *testing.T) { Name: "test-app", Namespace: "test-namespace-{{ .Release.Name }}", ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"}, - Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"}, + Secrets: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"}, Labels: map[string]string{"id": "{{ .Release.Name }}"}, }, want: ReleaseSpec{ @@ -45,7 +45,7 @@ func TestHelmState_executeTemplates(t *testing.T) { Name: "test-app", Namespace: "test-namespace-test-app", Values: []interface{}{"config/test_env/test-app/values.yaml"}, - Secrets: []string{"config/test_env/test-app/secrets.yaml"}, + Secrets: []interface{}{"config/test_env/test-app/secrets.yaml"}, Labels: map[string]string{"id": "test-app"}, }, },