From f813ac2642c203d87337a19f7870c2da384b94a7 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Tue, 22 Jan 2019 01:19:07 +0900 Subject: [PATCH] feat: Release Template (#439) This feature is supposed to help advanced use-cases like Conventional Directory Structure explained in several issues like #428. Newly added configuration keys `templates`, `missingFileHandler`, and the ability to defer executing template expressions in `values`, `secrets`, `namespace`, and `chart` of releases allows you to abstract away repetitions into a reusable template: ```yaml templates: default: &default missingFileHandler: Warn namespace: "{{`{{ .Release.Name }}`}}" chart: stable/{{`{{ .Release.Name }}`}} values: - config/{{`{{ .Release.Name }}`}}/values.yaml - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml secrets: - config/{{`{{ .Release.Name }}`}}/secrets.yaml - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml releases: - name: envoy <<: *default ``` See the updated documentation for more details. Resolves #428 --- README.md | 6 +- docs/writing-helmfile.md | 59 +++++ main.go | 9 +- state/create.go | 15 +- state/release.go | 69 +++++ state/state.go | 241 ++++++++++-------- state/state_exec_tmpl.go | 32 +++ state/state_exec_tmpl_test.go | 76 ++++++ state/types.go | 24 ++ tmpl/{funcs.go => context_funcs.go} | 0 tmpl/{funcs_test.go => context_funcs_test.go} | 0 tmpl/{tmpl.go => context_tmpl.go} | 0 tmpl/{tmpl_test.go => context_tmpl_test.go} | 0 tmpl/file.go | 66 ----- tmpl/file_renderer.go | 78 ++++++ .../file_renderer_test.go | 15 +- tmpl/{text.go => text_renderer.go} | 0 valuesfile/valuesfile.go | 39 --- 18 files changed, 498 insertions(+), 231 deletions(-) create mode 100644 state/release.go create mode 100644 state/state_exec_tmpl.go create mode 100644 state/state_exec_tmpl_test.go create mode 100644 state/types.go rename tmpl/{funcs.go => context_funcs.go} (100%) rename tmpl/{funcs_test.go => context_funcs_test.go} (100%) rename tmpl/{tmpl.go => context_tmpl.go} (100%) rename tmpl/{tmpl_test.go => context_tmpl_test.go} (100%) delete mode 100644 tmpl/file.go create mode 100644 tmpl/file_renderer.go rename valuesfile/valuesfile_test.go => tmpl/file_renderer_test.go (82%) rename tmpl/{text.go => text_renderer.go} (100%) delete mode 100644 valuesfile/valuesfile.go diff --git a/README.md b/README.md index b7ef2294..a5794c4e 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,11 @@ helmfile apply Congratulations! You now have your first Prometheus deployment running inside your cluster. -Iterate on the `helmfile.yaml` by referencing the [configuration syntax](#configuration-syntax) and the [cli reference](#cli-reference). +Iterate on the `helmfile.yaml` by referencing: + +- [Configuration syntax](#configuration-syntax) +- [CLI reference](#cli-reference). +- [Helmfile Best Practices Guide](https://github.com/roboll/helmfile/blob/master/docs/writing-helmfile.md) ## cli reference diff --git a/docs/writing-helmfile.md b/docs/writing-helmfile.md index b90d19aa..c8ddaafe 100644 --- a/docs/writing-helmfile.md +++ b/docs/writing-helmfile.md @@ -30,6 +30,65 @@ If you want a kind of default values that is used when a missing key was referen Now, you get `1` when there is no `eventApi.replicas` defined in environment values. +## Release Template / Conventional Directory Structure + +Introducing helmfile into a large-scale project that involes dozens of releases often results in a lot of repetitions in `helmfile.yaml` files. + +The example below shows repetitions in `namespace`, `chart`, `values`, and `secrets`: + +```yaml +releases: +# *snip* +- name: heapster + namespace: kube-system + chart: stable/heapster + version: 0.3.2 + values: + - "./config/heapster/values.yaml" + - "./config/heapster/{{ .Environment.Name }}.yaml" + secrets: + - "./config/heapster/secrets.yaml" + - "./config/heapster/{{ .Environment.Name }}-secrets.yaml" + + - name: kubernetes-dashboard + namespace: kube-system + chart: stable/kubernetes-dashboard + version: 0.10.0 + values: + - "./config/kubernetes-dashboard/values.yaml" + - "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml" + values: + - "./config/kubernetes-dashboard/secrets.yaml" + - "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml" +``` + +This is where Helmfile's advanced feature called Release Template comes handy. + +It allows you to abstract away the repetitions in releases into a template, which is then included and executed by using YAML anchor/alias: + +```yaml +templates: + default: &default + chart: stable/{{`{{ .Release.Name }}`}} + namespace: kube-system + # This prevents helmfile exiting when it encounters a missing file + missingFileHandler: Warn + values: + - config/{{`{{ .Release.Name }}`}}/values.yaml + - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml + secrets: + - config/{{`{{ .Release.Name }}`}}/secrets.yaml + - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml + +releases: +- name: heapster + <<: *default +- name: kubernetes-dashboard + <<: *default +``` + +See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work. + ## Layering You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default. diff --git a/main.go b/main.go index 6c1acfb8..eee34d1d 100644 --- a/main.go +++ b/main.go @@ -756,7 +756,8 @@ func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) // try a first pass render. This will always succeed, but can produce a limited env firstPassEnv := r.renderEnvironment(content) - secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), firstPassEnv, r.namespace) + tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace} + secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData) yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content) if err != nil { if r.logger != nil { @@ -840,6 +841,12 @@ func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat } noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles } else { + var err error + st, err = st.ExecuteTemplates() + if err != nil { + return fmt.Errorf("failed executing release templates in \"%s\": %v", fileOrDir, err) + } + var processed bool processed, errs = converge(st, helm) noMatchInHelmfiles = noMatchInHelmfiles && !processed diff --git a/state/create.go b/state/create.go index 2217093a..808ad1b2 100644 --- a/state/create.go +++ b/state/create.go @@ -6,7 +6,7 @@ import ( "github.com/imdario/mergo" "github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/helmexec" - "github.com/roboll/helmfile/valuesfile" + "github.com/roboll/helmfile/tmpl" "go.uber.org/zap" "gopkg.in/yaml.v2" "io" @@ -118,13 +118,14 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm return &state, nil } -func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) { +func (st *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) { envVals := map[string]interface{}{} - envSpec, ok := state.Environments[name] + envSpec, ok := st.Environments[name] if ok { for _, envvalFile := range envSpec.Values { - envvalFullPath := filepath.Join(state.basePath, envvalFile) - r := valuesfile.NewRenderer(readFile, filepath.Dir(envvalFullPath), environment.EmptyEnvironment) + envvalFullPath := filepath.Join(st.basePath, envvalFile) + tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""} + r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData) bytes, err := r.RenderToBytes(envvalFullPath) if err != nil { return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err) @@ -139,9 +140,9 @@ func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, erro } if len(envSpec.Secrets) > 0 { - helm := helmexec.New(state.logger, "") + helm := helmexec.New(st.logger, "") for _, secFile := range envSpec.Secrets { - path := filepath.Join(state.basePath, secFile) + path := filepath.Join(st.basePath, secFile) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } diff --git a/state/release.go b/state/release.go new file mode 100644 index 00000000..1c53ab71 --- /dev/null +++ b/state/release.go @@ -0,0 +1,69 @@ +package state + +import ( + "fmt" + "github.com/roboll/helmfile/tmpl" + "gopkg.in/yaml.v2" +) + +func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*ReleaseSpec, error) { + var result *ReleaseSpec + var err error + + result, err = r.Clone() + if err != nil { + return nil, fmt.Errorf("failed executing template expressions in release \"%s\": %v", r.Name, err) + } + + { + ts := result.Chart + result.Chart, err = renderer.RenderTemplateContentToString([]byte(ts)) + if err != nil { + return nil, fmt.Errorf("failed executing template expressions in release \"%s\".chart = \"%s\": %v", r.Name, ts, err) + } + } + + { + ts := result.Namespace + result.Namespace, err = renderer.RenderTemplateContentToString([]byte(ts)) + if err != nil { + return nil, fmt.Errorf("failed executing template expressions in release \"%s\".namespace = \"%s\": %v", r.Name, ts, err) + } + } + + for i, t := range result.Values { + 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\".values[%d] = \"%s\": %v", r.Name, i, ts, err) + } + result.Values[i] = s.String() + } + } + + 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) + } + result.Secrets[i] = s.String() + } + + return result, nil +} + +func (r ReleaseSpec) Clone() (*ReleaseSpec, error) { + serialized, err := yaml.Marshal(r) + if err != nil { + return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err) + } + + var deserialized ReleaseSpec + + if err := yaml.Unmarshal(serialized, &deserialized); err != nil { + return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err) + } + + return &deserialized, nil +} diff --git a/state/state.go b/state/state.go index fd80c988..c37aff47 100644 --- a/state/state.go +++ b/state/state.go @@ -21,7 +21,7 @@ import ( "github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/event" - "github.com/roboll/helmfile/valuesfile" + "github.com/roboll/helmfile/tmpl" "go.uber.org/zap" "gopkg.in/yaml.v2" ) @@ -39,6 +39,8 @@ type HelmState struct { Repositories []RepositorySpec `yaml:"repositories"` Releases []ReleaseSpec `yaml:"releases"` + Templates map[string]TemplateSpec `yaml:"templates"` + Env environment.Environment logger *zap.SugaredLogger @@ -95,6 +97,10 @@ type ReleaseSpec struct { // Installed, when set to true, `delete --purge` the release Installed *bool `yaml:"installed"` + // MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues. + // The default value for MissingFileHandler is "Error". + MissingFileHandler *string `yaml:"missingFileHandler"` + // Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile Hooks []event.Hook `yaml:"hooks"` @@ -125,9 +131,9 @@ type SetValue struct { const DefaultEnv = "default" -func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) { - if state.Namespace != "" { - spec.Namespace = state.Namespace +func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) { + if st.Namespace != "" { + spec.Namespace = st.Namespace } } @@ -137,10 +143,10 @@ type RepoUpdater interface { } // SyncRepos will update the given helm releases -func (state *HelmState) SyncRepos(helm RepoUpdater) []error { +func (st *HelmState) SyncRepos(helm RepoUpdater) []error { errs := []error{} - for _, repo := range state.Repositories { + for _, repo := range st.Repositories { if err := helm.AddRepo(repo.Name, repo.URL, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil { errs = append(errs, err) } @@ -176,8 +182,8 @@ type syncPrepareResult struct { } // SyncReleases wrapper for executing helm upgrade on the releases -func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) { - releases := state.Releases +func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) { + releases := st.Releases numReleases := len(releases) jobs := make(chan *ReleaseSpec, numReleases) results := make(chan syncPrepareResult, numReleases) @@ -193,9 +199,9 @@ func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalV for w := 1; w <= concurrency; w++ { go func() { for release := range jobs { - state.applyDefaultsTo(release) + st.applyDefaultsTo(release) - flags, flagsErr := state.flagsForUpgrade(helm, release) + flags, flagsErr := st.flagsForUpgrade(helm, release) if flagsErr != nil { results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}} continue @@ -248,10 +254,10 @@ func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalV return res, errs } -func (state *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) { +func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) { detected := []*ReleaseSpec{} - for i, _ := range state.Releases { - release := state.Releases[i] + for i, _ := range st.Releases { + release := st.Releases[i] if release.Installed != nil && !*release.Installed { err := helm.ReleaseStatus(release.Name) if err != nil { @@ -274,8 +280,8 @@ func (state *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*R } // SyncReleases wrapper for executing helm upgrade on the releases -func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error { - preps, prepErrs := state.prepareSyncReleases(helm, additionalValues, workerLimit) +func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error { + preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit) if len(prepErrs) > 0 { return prepErrs } @@ -298,7 +304,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ for prep := range jobQueue { release := prep.release flags := prep.flags - chart := normalizeChart(state.basePath, release.Chart) + chart := normalizeChart(st.basePath, release.Chart) if release.Installed != nil && !*release.Installed { if err := helm.ReleaseStatus(release.Name); err == nil { if err := helm.DeleteRelease(release.Name, "--purge"); err != nil { @@ -313,8 +319,8 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ results <- syncResult{} } - if _, err := state.triggerCleanupEvent(prep.release, "sync"); err != nil { - state.logger.Warnf("warn: %v\n", err) + if _, err := st.triggerCleanupEvent(prep.release, "sync"); err != nil { + st.logger.Warnf("warn: %v\n", err) } } waitGroup.Done() @@ -349,8 +355,8 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ } // downloadCharts will download and untar charts for Lint and Template -func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, workerLimit int, helmfileCommand string) (map[string]string, []error) { - temp := make(map[string]string, len(state.Releases)) +func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, workerLimit int, helmfileCommand string) (map[string]string, []error) { + temp := make(map[string]string, len(st.Releases)) type downloadResults struct { releaseName string chartPath string @@ -358,20 +364,20 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work errs := []error{} var wgFetch sync.WaitGroup - jobQueue := make(chan *ReleaseSpec, len(state.Releases)) - results := make(chan *downloadResults, len(state.Releases)) - wgFetch.Add(len(state.Releases)) + jobQueue := make(chan *ReleaseSpec, len(st.Releases)) + results := make(chan *downloadResults, len(st.Releases)) + wgFetch.Add(len(st.Releases)) if workerLimit < 1 { - workerLimit = len(state.Releases) + workerLimit = len(st.Releases) } for w := 1; w <= workerLimit; w++ { go func() { for release := range jobQueue { chartPath := "" - if pathExists(normalizeChart(state.basePath, release.Chart)) { - chartPath = normalizeChart(state.basePath, release.Chart) + if pathExists(normalizeChart(st.basePath, release.Chart)) { + chartPath = normalizeChart(st.basePath, release.Chart) } else { fetchFlags := []string{} if release.Version != "" { @@ -400,12 +406,12 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work wgFetch.Done() }() } - for i := 0; i < len(state.Releases); i++ { - jobQueue <- &state.Releases[i] + for i := 0; i < len(st.Releases); i++ { + jobQueue <- &st.Releases[i] } close(jobQueue) - for i := 0; i < len(state.Releases); i++ { + for i := 0; i < len(st.Releases); i++ { downloadRes := <-results temp[downloadRes.releaseName] = downloadRes.chartPath } @@ -419,7 +425,7 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work } // TemplateReleases wrapper for executing helm template on the releases -func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { +func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { errs := []error{} // Create tmp directory and bail immediately if it fails dir, err := ioutil.TempDir("", "") @@ -429,7 +435,7 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu } defer os.RemoveAll(dir) - temp, errs := state.downloadCharts(helm, dir, workerLimit, "template") + temp, errs := st.downloadCharts(helm, dir, workerLimit, "template") if errs != nil { errs = append(errs, err) @@ -440,8 +446,8 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu helm.SetExtraArgs(args...) } - for _, release := range state.Releases { - flags, err := state.flagsForTemplate(helm, &release) + for _, release := range st.Releases { + flags, err := st.flagsForTemplate(helm, &release) if err != nil { errs = append(errs, err) } @@ -463,8 +469,8 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu } } - if _, err := state.triggerCleanupEvent(&release, "template"); err != nil { - state.logger.Warnf("warn: %v\n", err) + if _, err := st.triggerCleanupEvent(&release, "template"); err != nil { + st.logger.Warnf("warn: %v\n", err) } } @@ -476,7 +482,7 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu } // LintReleases wrapper for executing helm lint on the releases -func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { +func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { errs := []error{} // Create tmp directory and bail immediately if it fails dir, err := ioutil.TempDir("", "") @@ -486,7 +492,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [ } defer os.RemoveAll(dir) - temp, errs := state.downloadCharts(helm, dir, workerLimit, "lint") + temp, errs := st.downloadCharts(helm, dir, workerLimit, "lint") if errs != nil { errs = append(errs, err) return errs @@ -496,8 +502,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [ helm.SetExtraArgs(args...) } - for _, release := range state.Releases { - flags, err := state.flagsForLint(helm, &release) + for _, release := range st.Releases { + flags, err := st.flagsForLint(helm, &release) if err != nil { errs = append(errs, err) } @@ -519,8 +525,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [ } } - if _, err := state.triggerCleanupEvent(&release, "lint"); err != nil { - state.logger.Warnf("warn: %v\n", err) + if _, err := st.triggerCleanupEvent(&release, "lint"); err != nil { + st.logger.Warnf("warn: %v\n", err) } } @@ -551,9 +557,9 @@ type diffPrepareResult struct { errors []*ReleaseError } -func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) { +func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) { releases := []ReleaseSpec{} - for _, r := range state.Releases { + for _, r := range st.Releases { if r.Installed == nil || *r.Installed { releases = append(releases, r) } @@ -575,9 +581,9 @@ func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalV for release := range jobs { errs := []error{} - state.applyDefaultsTo(release) + st.applyDefaultsTo(release) - flags, err := state.flagsForDiff(helm, release) + flags, err := st.flagsForDiff(helm, release) if err != nil { errs = append(errs, err) } @@ -644,8 +650,8 @@ func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalV // DiffReleases wrapper for executing helm diff on the releases // It returns releases that had any changes -func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool) ([]*ReleaseSpec, []error) { - preps, prepErrs := state.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets) +func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool) ([]*ReleaseSpec, []error) { + preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets) if len(prepErrs) > 0 { return []*ReleaseSpec{}, prepErrs } @@ -668,7 +674,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [ for prep := range jobQueue { flags := prep.flags release := prep.release - if err := helm.DiffRelease(release.Name, normalizeChart(state.basePath, release.Chart), flags...); err != nil { + if err := helm.DiffRelease(release.Name, normalizeChart(st.basePath, release.Chart), flags...); err != nil { switch e := err.(type) { case *exec.ExitError: // Propagate any non-zero exit status from the external command like `helm` that is failed under the hood @@ -683,8 +689,8 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [ } if triggerCleanupEvents { - if _, err := state.triggerCleanupEvent(prep.release, "diff"); err != nil { - state.logger.Warnf("warn: %v\n", err) + if _, err := st.triggerCleanupEvent(prep.release, "diff"); err != nil { + st.logger.Warnf("warn: %v\n", err) } } } @@ -718,14 +724,14 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [ return rs, errs } -func (state *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error { +func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error { var errs []error jobQueue := make(chan ReleaseSpec) doneQueue := make(chan bool) errQueue := make(chan error) if workerLimit < 1 { - workerLimit = len(state.Releases) + workerLimit = len(st.Releases) } // WaitGroup is required to wait until goroutine per job in job queue cleanly stops. @@ -745,13 +751,13 @@ func (state *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int } go func() { - for _, release := range state.Releases { + for _, release := range st.Releases { jobQueue <- release } close(jobQueue) }() - for i := 0; i < len(state.Releases); { + for i := 0; i < len(st.Releases); { select { case err := <-errQueue: errs = append(errs, err) @@ -770,11 +776,11 @@ func (state *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int } // DeleteReleases wrapper for executing helm delete on the releases -func (state *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error { +func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error { var wg sync.WaitGroup errs := []error{} - for _, release := range state.Releases { + for _, release := range st.Releases { wg.Add(1) go func(wg *sync.WaitGroup, release ReleaseSpec) { flags := []string{} @@ -797,11 +803,11 @@ func (state *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []er } // TestReleases wrapper for executing helm test on the releases -func (state *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int) []error { +func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int) []error { var wg sync.WaitGroup errs := []error{} - for _, release := range state.Releases { + for _, release := range st.Releases { wg.Add(1) go func(wg *sync.WaitGroup, release ReleaseSpec) { flags := []string{} @@ -825,10 +831,10 @@ func (state *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, time } // Clean will remove any generated secrets -func (state *HelmState) Clean() []error { +func (st *HelmState) Clean() []error { errs := []error{} - for _, release := range state.Releases { + for _, release := range st.Releases { for _, value := range release.generatedValues { err := os.Remove(value) if err != nil { @@ -845,7 +851,7 @@ func (state *HelmState) Clean() []error { } // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile. -func (state *HelmState) FilterReleases(labels []string) error { +func (st *HelmState) FilterReleases(labels []string) error { var filteredReleases []ReleaseSpec releaseSet := map[string][]ReleaseSpec{} filters := []ReleaseFilter{} @@ -856,7 +862,7 @@ func (state *HelmState) FilterReleases(labels []string) error { } filters = append(filters, f) } - for _, r := range state.Releases { + for _, r := range st.Releases { if r.Labels == nil { r.Labels = map[string]string{} } @@ -875,17 +881,17 @@ func (state *HelmState) FilterReleases(labels []string) error { for _, r := range releaseSet { filteredReleases = append(filteredReleases, r...) } - state.Releases = filteredReleases + st.Releases = filteredReleases numFound := len(filteredReleases) - state.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(labels, ","), state.FilePath) + st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(labels, ","), st.FilePath) return nil } -func (state *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error { +func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error { errs := []error{} - for _, release := range state.Releases { - if _, err := state.triggerPrepareEvent(&release, helmfileCommand); err != nil { + for _, release := range st.Releases { + if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil { errs = append(errs, &ReleaseError{&release, err}) continue } @@ -896,23 +902,23 @@ func (state *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand return nil } -func (state *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { - return state.triggerReleaseEvent("prepare", r, helmfileCommand) +func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { + return st.triggerReleaseEvent("prepare", r, helmfileCommand) } -func (state *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { - return state.triggerReleaseEvent("cleanup", r, helmfileCommand) +func (st *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { + return st.triggerReleaseEvent("cleanup", r, helmfileCommand) } -func (state *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd string) (bool, error) { +func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd string) (bool, error) { bus := &event.Bus{ Hooks: r.Hooks, - StateFilePath: state.FilePath, - BasePath: state.basePath, - Namespace: state.Namespace, - Env: state.Env, - Logger: state.logger, - ReadFile: state.readFile, + StateFilePath: st.FilePath, + BasePath: st.basePath, + Namespace: st.Namespace, + Env: st.Env, + Logger: st.logger, + ReadFile: st.readFile, } data := map[string]interface{}{ "Release": r, @@ -922,12 +928,12 @@ func (state *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfile } // UpdateDeps wrapper for updating dependencies on the releases -func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error { +func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error { errs := []error{} - for _, release := range state.Releases { + for _, release := range st.Releases { if isLocalChart(release.Chart) { - if err := helm.UpdateDeps(normalizeChart(state.basePath, release.Chart)); err != nil { + if err := helm.UpdateDeps(normalizeChart(st.basePath, release.Chart)); err != nil { errs = append(errs, err) } } @@ -939,16 +945,16 @@ func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error { } // JoinBase returns an absolute path in the form basePath/relative -func (state *HelmState) JoinBase(relPath string) string { - return filepath.Join(state.basePath, relPath) +func (st *HelmState) JoinBase(relPath string) string { + return filepath.Join(st.basePath, relPath) } // normalizes relative path to absolute one -func (state *HelmState) normalizePath(path string) string { +func (st *HelmState) normalizePath(path string) string { if filepath.IsAbs(path) { return path } else { - return state.JoinBase(path) + return st.JoinBase(path) } } @@ -1000,25 +1006,25 @@ func findChartDirectory(topLevelDir string) (string, error) { return topLevelDir, errors.New("No Chart.yaml found") } -func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { +func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { flags := []string{} if release.Version != "" { flags = append(flags, "--version", release.Version) } - if state.isDevelopment(release) { + if st.isDevelopment(release) { flags = append(flags, "--devel") } - if release.Verify != nil && *release.Verify || state.HelmDefaults.Verify { + if release.Verify != nil && *release.Verify || st.HelmDefaults.Verify { flags = append(flags, "--verify") } - if release.Wait != nil && *release.Wait || state.HelmDefaults.Wait { + if release.Wait != nil && *release.Wait || st.HelmDefaults.Wait { flags = append(flags, "--wait") } - timeout := state.HelmDefaults.Timeout + timeout := st.HelmDefaults.Timeout if release.Timeout != nil { timeout = *release.Timeout } @@ -1026,51 +1032,51 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas flags = append(flags, "--timeout", fmt.Sprintf("%d", timeout)) } - if release.Force != nil && *release.Force || state.HelmDefaults.Force { + if release.Force != nil && *release.Force || st.HelmDefaults.Force { flags = append(flags, "--force") } - if release.RecreatePods != nil && *release.RecreatePods || state.HelmDefaults.RecreatePods { + if release.RecreatePods != nil && *release.RecreatePods || st.HelmDefaults.RecreatePods { flags = append(flags, "--recreate-pods") } - common, err := state.namespaceAndValuesFlags(helm, release) + common, err := st.namespaceAndValuesFlags(helm, release) if err != nil { return nil, err } return append(flags, common...), nil } -func (state *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { +func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { flags := []string{ "--name", release.Name, } - common, err := state.namespaceAndValuesFlags(helm, release) + common, err := st.namespaceAndValuesFlags(helm, release) if err != nil { return nil, err } return append(flags, common...), nil } -func (state *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { +func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { flags := []string{} if release.Version != "" { flags = append(flags, "--version", release.Version) } - if state.isDevelopment(release) { + if st.isDevelopment(release) { flags = append(flags, "--devel") } - common, err := state.namespaceAndValuesFlags(helm, release) + common, err := st.namespaceAndValuesFlags(helm, release) if err != nil { return nil, err } return append(flags, common...), nil } -func (state *HelmState) isDevelopment(release *ReleaseSpec) bool { - result := state.HelmDefaults.Devel +func (st *HelmState) isDevelopment(release *ReleaseSpec) bool { + result := st.HelmDefaults.Devel if release.Devel != nil { result = *release.Devel } @@ -1078,16 +1084,16 @@ func (state *HelmState) isDevelopment(release *ReleaseSpec) bool { return result } -func (state *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { - return state.namespaceAndValuesFlags(helm, release) +func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { + return st.namespaceAndValuesFlags(helm, release) } -func (state *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) { - r := valuesfile.NewRenderer(state.readFile, filepath.Dir(path), state.Env) +func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) { + r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.envTemplateData()) return r.RenderToBytes(path) } -func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { +func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { flags := []string{} if release.Namespace != "" { flags = append(flags, "--namespace", release.Namespace) @@ -1095,13 +1101,18 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release for _, value := range release.Values { switch typedValue := value.(type) { case string: - path := state.normalizePath(release.ValuesPathPrefix + typedValue) + path := st.normalizePath(release.ValuesPathPrefix + typedValue) if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, err + if release.MissingFileHandler == nil && *release.MissingFileHandler == "Error" { + return nil, err + } else { + st.logger.Warnf("skipping missing values file \"%s\"", path) + continue + } } - yamlBytes, err := state.RenderValuesFileToBytes(path) + yamlBytes, err := st.RenderValuesFileToBytes(path) if err != nil { return nil, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err) } @@ -1115,7 +1126,7 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release if _, err := valfile.Write(yamlBytes); err != nil { return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err) } - state.logger.Debugf("successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes)) + st.logger.Debugf("successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes)) flags = append(flags, "--values", valfile.Name()) case map[interface{}]interface{}: @@ -1133,10 +1144,16 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release flags = append(flags, "--values", valfile.Name()) } } + for _, value := range release.Secrets { - path := state.normalizePath(release.ValuesPathPrefix + value) + path := st.normalizePath(release.ValuesPathPrefix + value) if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, err + if release.MissingFileHandler == nil && *release.MissingFileHandler == "Error" { + return nil, err + } else { + st.logger.Warnf("skipping missing secrets file \"%s\"", path) + continue + } } valfile, err := helm.DecryptSecret(path) @@ -1152,7 +1169,7 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release if set.Value != "" { flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(set.Value))) } else if set.File != "" { - flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), state.normalizePath(set.File))) + flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.normalizePath(set.File))) } else if len(set.Values) > 0 { items := make([]string, len(set.Values)) for i, raw := range set.Values { diff --git a/state/state_exec_tmpl.go b/state/state_exec_tmpl.go new file mode 100644 index 00000000..9b27c9b4 --- /dev/null +++ b/state/state_exec_tmpl.go @@ -0,0 +1,32 @@ +package state + +import ( + "fmt" + "github.com/roboll/helmfile/tmpl" +) + +func (st *HelmState) envTemplateData() EnvironmentTemplateData { + return EnvironmentTemplateData{ + st.Env, + st.Namespace, + } +} + +func (st *HelmState) ExecuteTemplates() (*HelmState, error) { + r := *st + + for i, rt := range st.Releases { + tmplData := ReleaseTemplateData{ + Environment: st.Env, + Release: rt, + } + renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData) + r, err := rt.ExecuteTemplateExpressions(renderer) + if err != nil { + return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err) + } + st.Releases[i] = *r + } + + return &r, nil +} diff --git a/state/state_exec_tmpl_test.go b/state/state_exec_tmpl_test.go new file mode 100644 index 00000000..ff21084d --- /dev/null +++ b/state/state_exec_tmpl_test.go @@ -0,0 +1,76 @@ +package state + +import ( + "reflect" + "testing" + + "github.com/roboll/helmfile/environment" +) + +func TestHelmState_executeTemplates(t *testing.T) { + tests := []struct { + name string + input ReleaseSpec + want ReleaseSpec + }{ + { + name: "Has template expressions in chart, values, and secrets", + input: ReleaseSpec{ + Chart: "test-charts/{{ .Release.Name }}", + Version: "0.1", + Verify: nil, + Name: "test-app", + Namespace: "test-namespace-{{ .Release.Name }}", + Values: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"}, + Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"}, + }, + want: ReleaseSpec{ + Chart: "test-charts/test-app", + Version: "0.1", + Verify: nil, + 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"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &HelmState{ + basePath: ".", + HelmDefaults: HelmSpec{ + KubeContext: "test_context", + }, + Env: environment.Environment{Name: "test_env"}, + Namespace: "test-namespace_", + Repositories: nil, + Releases: []ReleaseSpec{ + tt.input, + }, + } + + r, err := state.ExecuteTemplates() + if err != nil { + t.Errorf("Unexpected error: %v", err) + t.FailNow() + } + + actual := r.Releases[0] + + if !reflect.DeepEqual(actual.Chart, tt.want.Chart) { + t.Errorf("expected %+v, got %+v", tt.want.Chart, actual.Chart) + } + if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) { + t.Errorf("expected %+v, got %+v", tt.want.Namespace, actual.Namespace) + } + if !reflect.DeepEqual(actual.Values, tt.want.Values) { + t.Errorf("expected %+v, got %+v", tt.want.Values, actual.Values) + } + if !reflect.DeepEqual(actual.Secrets, tt.want.Secrets) { + t.Errorf("expected %+v, got %+v", tt.want.Secrets, actual.Secrets) + } + }) + } +} diff --git a/state/types.go b/state/types.go new file mode 100644 index 00000000..bf9dd019 --- /dev/null +++ b/state/types.go @@ -0,0 +1,24 @@ +package state + +import "github.com/roboll/helmfile/environment" + +// TemplateSpec defines the structure of a reusable and composable template for helm releases. +type TemplateSpec struct { + ReleaseSpec `yaml:",inline"` +} + +// EnvironmentTemplateData provides variables accessible while executing golang text/template expressions in helmfile and values YAML files +type EnvironmentTemplateData struct { + // Environment is accessible as `.Environment` from any template executed by the renderer + Environment environment.Environment + // Namespace is accessible as `.Namespace` from any non-values template executed by the renderer + Namespace string +} + +// ReleaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file +type ReleaseTemplateData struct { + // Environment is accessible as `.Environment` from any template expression executed by the renderer + Environment environment.Environment + // Release is accessible as `.Release` from any template expression executed by the renderer + Release ReleaseSpec +} diff --git a/tmpl/funcs.go b/tmpl/context_funcs.go similarity index 100% rename from tmpl/funcs.go rename to tmpl/context_funcs.go diff --git a/tmpl/funcs_test.go b/tmpl/context_funcs_test.go similarity index 100% rename from tmpl/funcs_test.go rename to tmpl/context_funcs_test.go diff --git a/tmpl/tmpl.go b/tmpl/context_tmpl.go similarity index 100% rename from tmpl/tmpl.go rename to tmpl/context_tmpl.go diff --git a/tmpl/tmpl_test.go b/tmpl/context_tmpl_test.go similarity index 100% rename from tmpl/tmpl_test.go rename to tmpl/context_tmpl_test.go diff --git a/tmpl/file.go b/tmpl/file.go deleted file mode 100644 index ad2d83fe..00000000 --- a/tmpl/file.go +++ /dev/null @@ -1,66 +0,0 @@ -package tmpl - -import ( - "bytes" - "io/ioutil" - - "github.com/roboll/helmfile/environment" -) - -type templateFileRenderer struct { - ReadFile func(string) ([]byte, error) - Context *Context - Data TemplateData -} - -type TemplateData struct { - // Environment is accessible as `.Environment` from any template executed by the renderer - Environment environment.Environment - // Namespace is accessible as `.Namespace` from any non-values template executed by the renderer - Namespace string -} - -type FileRenderer interface { - RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) -} - -func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment, namespace string) *templateFileRenderer { - return &templateFileRenderer{ - ReadFile: readFile, - Context: &Context{ - basePath: basePath, - readFile: readFile, - }, - Data: TemplateData{ - Environment: env, - Namespace: namespace, - }, - } -} - -func NewFirstPassRenderer(basePath string, env environment.Environment) *templateFileRenderer { - return &templateFileRenderer{ - ReadFile: ioutil.ReadFile, - Context: &Context{ - preRender: true, - basePath: basePath, - readFile: ioutil.ReadFile, - }, - Data: TemplateData{ - Environment: env, - }, - } -} - -func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) { - content, err := r.ReadFile(file) - if err != nil { - return nil, err - } - - return r.RenderTemplateContentToBuffer(content) -} - -func (r *templateFileRenderer) RenderTemplateContentToBuffer(content []byte) (*bytes.Buffer, error) { - return r.Context.RenderTemplateToBuffer(string(content), r.Data) -} diff --git a/tmpl/file_renderer.go b/tmpl/file_renderer.go new file mode 100644 index 00000000..3193635c --- /dev/null +++ b/tmpl/file_renderer.go @@ -0,0 +1,78 @@ +package tmpl + +import ( + "bytes" + "io/ioutil" + + "fmt" + "strings" +) + +type FileRenderer struct { + ReadFile func(string) ([]byte, error) + Context *Context + Data interface{} +} + +func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *FileRenderer { + return &FileRenderer{ + ReadFile: readFile, + Context: &Context{ + basePath: basePath, + readFile: readFile, + }, + Data: data, + } +} + +func NewFirstPassRenderer(basePath string, data interface{}) *FileRenderer { + return &FileRenderer{ + ReadFile: ioutil.ReadFile, + Context: &Context{ + preRender: true, + basePath: basePath, + readFile: ioutil.ReadFile, + }, + Data: data, + } +} + +func (r *FileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) { + content, err := r.ReadFile(file) + if err != nil { + return nil, err + } + + return r.RenderTemplateContentToBuffer(content) +} + +func (r *FileRenderer) RenderToBytes(path string) ([]byte, error) { + var yamlBytes []byte + splits := strings.Split(path, ".") + if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" { + yamlBuf, err := r.RenderTemplateFileToBuffer(path) + if err != nil { + return nil, fmt.Errorf("failed to render [%s], because of %v", path, err) + } + yamlBytes = yamlBuf.Bytes() + } else { + var err error + yamlBytes, err = r.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to load [%s]: %v", path, err) + } + } + return yamlBytes, nil +} + +func (r *FileRenderer) RenderTemplateContentToBuffer(content []byte) (*bytes.Buffer, error) { + return r.Context.RenderTemplateToBuffer(string(content), r.Data) +} + +func (r *FileRenderer) RenderTemplateContentToString(content []byte) (string, error) { + buf, err := r.Context.RenderTemplateToBuffer(string(content), r.Data) + if err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/valuesfile/valuesfile_test.go b/tmpl/file_renderer_test.go similarity index 82% rename from valuesfile/valuesfile_test.go rename to tmpl/file_renderer_test.go index 2d1dbd8d..51b0db8e 100644 --- a/valuesfile/valuesfile_test.go +++ b/tmpl/file_renderer_test.go @@ -1,4 +1,4 @@ -package valuesfile +package tmpl import ( "fmt" @@ -7,6 +7,11 @@ import ( "testing" ) +var emptyEnvTmplData = map[string]interface{}{ + "Environment": environment.EmptyEnvironment, + "Namespace": "", +} + func TestRenderToBytes_Gotmpl(t *testing.T) { valuesYamlTmplContent := `foo: bar: '{{ readFile "data.txt" }}' @@ -17,7 +22,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) { ` dataFile := "data.txt" valuesTmplFile := "values.yaml.gotmpl" - r := NewRenderer(func(filename string) ([]byte, error) { + r := NewFileRenderer(func(filename string) ([]byte, error) { switch filename { case valuesTmplFile: return []byte(valuesYamlTmplContent), nil @@ -25,7 +30,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) { return []byte(dataFileContent), nil } return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename) - }, "", environment.EmptyEnvironment) + }, "", emptyEnvTmplData) buf, err := r.RenderToBytes(valuesTmplFile) if err != nil { t.Errorf("unexpected error: %v", err) @@ -44,13 +49,13 @@ func TestRenderToBytes_Yaml(t *testing.T) { bar: '{{ readFile "data.txt" }}' ` valuesFile := "values.yaml" - r := NewRenderer(func(filename string) ([]byte, error) { + r := NewFileRenderer(func(filename string) ([]byte, error) { switch filename { case valuesFile: return []byte(valuesYamlContent), nil } return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename) - }, "", environment.EmptyEnvironment) + }, "", emptyEnvTmplData) buf, err := r.RenderToBytes(valuesFile) if err != nil { t.Errorf("unexpected error: %v", err) diff --git a/tmpl/text.go b/tmpl/text_renderer.go similarity index 100% rename from tmpl/text.go rename to tmpl/text_renderer.go diff --git a/valuesfile/valuesfile.go b/valuesfile/valuesfile.go deleted file mode 100644 index 0bed26a4..00000000 --- a/valuesfile/valuesfile.go +++ /dev/null @@ -1,39 +0,0 @@ -package valuesfile - -import ( - "fmt" - "github.com/roboll/helmfile/environment" - "github.com/roboll/helmfile/tmpl" - "strings" -) - -type renderer struct { - readFile func(string) ([]byte, error) - tmplFileRenderer tmpl.FileRenderer -} - -func NewRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *renderer { - return &renderer{ - readFile: readFile, - tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath, env, ""), - } -} - -func (r *renderer) RenderToBytes(path string) ([]byte, error) { - var yamlBytes []byte - splits := strings.Split(path, ".") - if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" { - yamlBuf, err := r.tmplFileRenderer.RenderTemplateFileToBuffer(path) - if err != nil { - return nil, fmt.Errorf("failed to render [%s], because of %v", path, err) - } - yamlBytes = yamlBuf.Bytes() - } else { - var err error - yamlBytes, err = r.readFile(path) - if err != nil { - return nil, fmt.Errorf("failed to load [%s]: %v", path, err) - } - } - return yamlBytes, nil -}