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
This commit is contained in:
KUOKA Yusuke 2019-01-22 01:19:07 +09:00 committed by GitHub
parent 23178b398c
commit f813ac2642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 498 additions and 231 deletions

View File

@ -179,7 +179,11 @@ helmfile apply
Congratulations! You now have your first Prometheus deployment running inside your cluster. 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 ## cli reference

View File

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

View File

@ -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 // try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content) 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) yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil { if err != nil {
if r.logger != nil { if r.logger != nil {
@ -840,6 +841,12 @@ func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
} }
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
} else { } 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 var processed bool
processed, errs = converge(st, helm) processed, errs = converge(st, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed noMatchInHelmfiles = noMatchInHelmfiles && !processed

View File

@ -6,7 +6,7 @@ import (
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/valuesfile" "github.com/roboll/helmfile/tmpl"
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io" "io"
@ -118,13 +118,14 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm
return &state, nil 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{}{} envVals := map[string]interface{}{}
envSpec, ok := state.Environments[name] envSpec, ok := st.Environments[name]
if ok { if ok {
for _, envvalFile := range envSpec.Values { for _, envvalFile := range envSpec.Values {
envvalFullPath := filepath.Join(state.basePath, envvalFile) envvalFullPath := filepath.Join(st.basePath, envvalFile)
r := valuesfile.NewRenderer(readFile, filepath.Dir(envvalFullPath), environment.EmptyEnvironment) tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData)
bytes, err := r.RenderToBytes(envvalFullPath) bytes, err := r.RenderToBytes(envvalFullPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err) 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 { if len(envSpec.Secrets) > 0 {
helm := helmexec.New(state.logger, "") helm := helmexec.New(st.logger, "")
for _, secFile := range envSpec.Secrets { 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) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
} }

69
state/release.go Normal file
View File

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

View File

@ -21,7 +21,7 @@ import (
"github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/event" "github.com/roboll/helmfile/event"
"github.com/roboll/helmfile/valuesfile" "github.com/roboll/helmfile/tmpl"
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -39,6 +39,8 @@ type HelmState struct {
Repositories []RepositorySpec `yaml:"repositories"` Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"` Releases []ReleaseSpec `yaml:"releases"`
Templates map[string]TemplateSpec `yaml:"templates"`
Env environment.Environment Env environment.Environment
logger *zap.SugaredLogger logger *zap.SugaredLogger
@ -95,6 +97,10 @@ type ReleaseSpec struct {
// Installed, when set to true, `delete --purge` the release // Installed, when set to true, `delete --purge` the release
Installed *bool `yaml:"installed"` 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 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"` Hooks []event.Hook `yaml:"hooks"`
@ -125,9 +131,9 @@ type SetValue struct {
const DefaultEnv = "default" const DefaultEnv = "default"
func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) { func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if state.Namespace != "" { if st.Namespace != "" {
spec.Namespace = state.Namespace spec.Namespace = st.Namespace
} }
} }
@ -137,10 +143,10 @@ type RepoUpdater interface {
} }
// SyncRepos will update the given helm releases // SyncRepos will update the given helm releases
func (state *HelmState) SyncRepos(helm RepoUpdater) []error { func (st *HelmState) SyncRepos(helm RepoUpdater) []error {
errs := []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 { if err := helm.AddRepo(repo.Name, repo.URL, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -176,8 +182,8 @@ type syncPrepareResult struct {
} }
// SyncReleases wrapper for executing helm upgrade on the releases // SyncReleases wrapper for executing helm upgrade on the releases
func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) { func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
releases := state.Releases releases := st.Releases
numReleases := len(releases) numReleases := len(releases)
jobs := make(chan *ReleaseSpec, numReleases) jobs := make(chan *ReleaseSpec, numReleases)
results := make(chan syncPrepareResult, numReleases) results := make(chan syncPrepareResult, numReleases)
@ -193,9 +199,9 @@ func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalV
for w := 1; w <= concurrency; w++ { for w := 1; w <= concurrency; w++ {
go func() { go func() {
for release := range jobs { 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 { if flagsErr != nil {
results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}} results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}}
continue continue
@ -248,10 +254,10 @@ func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalV
return res, errs return res, errs
} }
func (state *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) { func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) {
detected := []*ReleaseSpec{} detected := []*ReleaseSpec{}
for i, _ := range state.Releases { for i, _ := range st.Releases {
release := state.Releases[i] release := st.Releases[i]
if release.Installed != nil && !*release.Installed { if release.Installed != nil && !*release.Installed {
err := helm.ReleaseStatus(release.Name) err := helm.ReleaseStatus(release.Name)
if err != nil { if err != nil {
@ -274,8 +280,8 @@ func (state *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*R
} }
// SyncReleases wrapper for executing helm upgrade on the releases // SyncReleases wrapper for executing helm upgrade on the releases
func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error { func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
preps, prepErrs := state.prepareSyncReleases(helm, additionalValues, workerLimit) preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit)
if len(prepErrs) > 0 { if len(prepErrs) > 0 {
return prepErrs return prepErrs
} }
@ -298,7 +304,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
for prep := range jobQueue { for prep := range jobQueue {
release := prep.release release := prep.release
flags := prep.flags flags := prep.flags
chart := normalizeChart(state.basePath, release.Chart) chart := normalizeChart(st.basePath, release.Chart)
if release.Installed != nil && !*release.Installed { if release.Installed != nil && !*release.Installed {
if err := helm.ReleaseStatus(release.Name); err == nil { if err := helm.ReleaseStatus(release.Name); err == nil {
if err := helm.DeleteRelease(release.Name, "--purge"); 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{} results <- syncResult{}
} }
if _, err := state.triggerCleanupEvent(prep.release, "sync"); err != nil { if _, err := st.triggerCleanupEvent(prep.release, "sync"); err != nil {
state.logger.Warnf("warn: %v\n", err) st.logger.Warnf("warn: %v\n", err)
} }
} }
waitGroup.Done() waitGroup.Done()
@ -349,8 +355,8 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
} }
// downloadCharts will download and untar charts for Lint and Template // 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) { func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, workerLimit int, helmfileCommand string) (map[string]string, []error) {
temp := make(map[string]string, len(state.Releases)) temp := make(map[string]string, len(st.Releases))
type downloadResults struct { type downloadResults struct {
releaseName string releaseName string
chartPath string chartPath string
@ -358,20 +364,20 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work
errs := []error{} errs := []error{}
var wgFetch sync.WaitGroup var wgFetch sync.WaitGroup
jobQueue := make(chan *ReleaseSpec, len(state.Releases)) jobQueue := make(chan *ReleaseSpec, len(st.Releases))
results := make(chan *downloadResults, len(state.Releases)) results := make(chan *downloadResults, len(st.Releases))
wgFetch.Add(len(state.Releases)) wgFetch.Add(len(st.Releases))
if workerLimit < 1 { if workerLimit < 1 {
workerLimit = len(state.Releases) workerLimit = len(st.Releases)
} }
for w := 1; w <= workerLimit; w++ { for w := 1; w <= workerLimit; w++ {
go func() { go func() {
for release := range jobQueue { for release := range jobQueue {
chartPath := "" chartPath := ""
if pathExists(normalizeChart(state.basePath, release.Chart)) { if pathExists(normalizeChart(st.basePath, release.Chart)) {
chartPath = normalizeChart(state.basePath, release.Chart) chartPath = normalizeChart(st.basePath, release.Chart)
} else { } else {
fetchFlags := []string{} fetchFlags := []string{}
if release.Version != "" { if release.Version != "" {
@ -400,12 +406,12 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work
wgFetch.Done() wgFetch.Done()
}() }()
} }
for i := 0; i < len(state.Releases); i++ { for i := 0; i < len(st.Releases); i++ {
jobQueue <- &state.Releases[i] jobQueue <- &st.Releases[i]
} }
close(jobQueue) close(jobQueue)
for i := 0; i < len(state.Releases); i++ { for i := 0; i < len(st.Releases); i++ {
downloadRes := <-results downloadRes := <-results
temp[downloadRes.releaseName] = downloadRes.chartPath 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 // 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{} errs := []error{}
// Create tmp directory and bail immediately if it fails // Create tmp directory and bail immediately if it fails
dir, err := ioutil.TempDir("", "") dir, err := ioutil.TempDir("", "")
@ -429,7 +435,7 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
temp, errs := state.downloadCharts(helm, dir, workerLimit, "template") temp, errs := st.downloadCharts(helm, dir, workerLimit, "template")
if errs != nil { if errs != nil {
errs = append(errs, err) errs = append(errs, err)
@ -440,8 +446,8 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu
helm.SetExtraArgs(args...) helm.SetExtraArgs(args...)
} }
for _, release := range state.Releases { for _, release := range st.Releases {
flags, err := state.flagsForTemplate(helm, &release) flags, err := st.flagsForTemplate(helm, &release)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -463,8 +469,8 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu
} }
} }
if _, err := state.triggerCleanupEvent(&release, "template"); err != nil { if _, err := st.triggerCleanupEvent(&release, "template"); err != nil {
state.logger.Warnf("warn: %v\n", err) 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 // 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{} errs := []error{}
// Create tmp directory and bail immediately if it fails // Create tmp directory and bail immediately if it fails
dir, err := ioutil.TempDir("", "") dir, err := ioutil.TempDir("", "")
@ -486,7 +492,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
temp, errs := state.downloadCharts(helm, dir, workerLimit, "lint") temp, errs := st.downloadCharts(helm, dir, workerLimit, "lint")
if errs != nil { if errs != nil {
errs = append(errs, err) errs = append(errs, err)
return errs return errs
@ -496,8 +502,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
helm.SetExtraArgs(args...) helm.SetExtraArgs(args...)
} }
for _, release := range state.Releases { for _, release := range st.Releases {
flags, err := state.flagsForLint(helm, &release) flags, err := st.flagsForLint(helm, &release)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -519,8 +525,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
} }
} }
if _, err := state.triggerCleanupEvent(&release, "lint"); err != nil { if _, err := st.triggerCleanupEvent(&release, "lint"); err != nil {
state.logger.Warnf("warn: %v\n", err) st.logger.Warnf("warn: %v\n", err)
} }
} }
@ -551,9 +557,9 @@ type diffPrepareResult struct {
errors []*ReleaseError 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{} releases := []ReleaseSpec{}
for _, r := range state.Releases { for _, r := range st.Releases {
if r.Installed == nil || *r.Installed { if r.Installed == nil || *r.Installed {
releases = append(releases, r) releases = append(releases, r)
} }
@ -575,9 +581,9 @@ func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalV
for release := range jobs { for release := range jobs {
errs := []error{} errs := []error{}
state.applyDefaultsTo(release) st.applyDefaultsTo(release)
flags, err := state.flagsForDiff(helm, release) flags, err := st.flagsForDiff(helm, release)
if err != nil { if err != nil {
errs = append(errs, err) 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 // DiffReleases wrapper for executing helm diff on the releases
// It returns releases that had any changes // 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) { func (st *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) preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets)
if len(prepErrs) > 0 { if len(prepErrs) > 0 {
return []*ReleaseSpec{}, prepErrs return []*ReleaseSpec{}, prepErrs
} }
@ -668,7 +674,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
for prep := range jobQueue { for prep := range jobQueue {
flags := prep.flags flags := prep.flags
release := prep.release 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) { switch e := err.(type) {
case *exec.ExitError: case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood // 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 triggerCleanupEvents {
if _, err := state.triggerCleanupEvent(prep.release, "diff"); err != nil { if _, err := st.triggerCleanupEvent(prep.release, "diff"); err != nil {
state.logger.Warnf("warn: %v\n", err) st.logger.Warnf("warn: %v\n", err)
} }
} }
} }
@ -718,14 +724,14 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
return rs, errs 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 var errs []error
jobQueue := make(chan ReleaseSpec) jobQueue := make(chan ReleaseSpec)
doneQueue := make(chan bool) doneQueue := make(chan bool)
errQueue := make(chan error) errQueue := make(chan error)
if workerLimit < 1 { 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. // 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() { go func() {
for _, release := range state.Releases { for _, release := range st.Releases {
jobQueue <- release jobQueue <- release
} }
close(jobQueue) close(jobQueue)
}() }()
for i := 0; i < len(state.Releases); { for i := 0; i < len(st.Releases); {
select { select {
case err := <-errQueue: case err := <-errQueue:
errs = append(errs, err) 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 // 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 var wg sync.WaitGroup
errs := []error{} errs := []error{}
for _, release := range state.Releases { for _, release := range st.Releases {
wg.Add(1) wg.Add(1)
go func(wg *sync.WaitGroup, release ReleaseSpec) { go func(wg *sync.WaitGroup, release ReleaseSpec) {
flags := []string{} 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 // 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 var wg sync.WaitGroup
errs := []error{} errs := []error{}
for _, release := range state.Releases { for _, release := range st.Releases {
wg.Add(1) wg.Add(1)
go func(wg *sync.WaitGroup, release ReleaseSpec) { go func(wg *sync.WaitGroup, release ReleaseSpec) {
flags := []string{} flags := []string{}
@ -825,10 +831,10 @@ func (state *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, time
} }
// Clean will remove any generated secrets // Clean will remove any generated secrets
func (state *HelmState) Clean() []error { func (st *HelmState) Clean() []error {
errs := []error{} errs := []error{}
for _, release := range state.Releases { for _, release := range st.Releases {
for _, value := range release.generatedValues { for _, value := range release.generatedValues {
err := os.Remove(value) err := os.Remove(value)
if err != nil { 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. // 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 var filteredReleases []ReleaseSpec
releaseSet := map[string][]ReleaseSpec{} releaseSet := map[string][]ReleaseSpec{}
filters := []ReleaseFilter{} filters := []ReleaseFilter{}
@ -856,7 +862,7 @@ func (state *HelmState) FilterReleases(labels []string) error {
} }
filters = append(filters, f) filters = append(filters, f)
} }
for _, r := range state.Releases { for _, r := range st.Releases {
if r.Labels == nil { if r.Labels == nil {
r.Labels = map[string]string{} r.Labels = map[string]string{}
} }
@ -875,17 +881,17 @@ func (state *HelmState) FilterReleases(labels []string) error {
for _, r := range releaseSet { for _, r := range releaseSet {
filteredReleases = append(filteredReleases, r...) filteredReleases = append(filteredReleases, r...)
} }
state.Releases = filteredReleases st.Releases = filteredReleases
numFound := len(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 return nil
} }
func (state *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error { func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error {
errs := []error{} errs := []error{}
for _, release := range state.Releases { for _, release := range st.Releases {
if _, err := state.triggerPrepareEvent(&release, helmfileCommand); err != nil { if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil {
errs = append(errs, &ReleaseError{&release, err}) errs = append(errs, &ReleaseError{&release, err})
continue continue
} }
@ -896,23 +902,23 @@ func (state *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand
return nil return nil
} }
func (state *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return state.triggerReleaseEvent("prepare", r, helmfileCommand) return st.triggerReleaseEvent("prepare", r, helmfileCommand)
} }
func (state *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { func (st *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return state.triggerReleaseEvent("cleanup", r, helmfileCommand) 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{ bus := &event.Bus{
Hooks: r.Hooks, Hooks: r.Hooks,
StateFilePath: state.FilePath, StateFilePath: st.FilePath,
BasePath: state.basePath, BasePath: st.basePath,
Namespace: state.Namespace, Namespace: st.Namespace,
Env: state.Env, Env: st.Env,
Logger: state.logger, Logger: st.logger,
ReadFile: state.readFile, ReadFile: st.readFile,
} }
data := map[string]interface{}{ data := map[string]interface{}{
"Release": r, "Release": r,
@ -922,12 +928,12 @@ func (state *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfile
} }
// UpdateDeps wrapper for updating dependencies on the releases // 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{} errs := []error{}
for _, release := range state.Releases { for _, release := range st.Releases {
if isLocalChart(release.Chart) { 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) 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 // JoinBase returns an absolute path in the form basePath/relative
func (state *HelmState) JoinBase(relPath string) string { func (st *HelmState) JoinBase(relPath string) string {
return filepath.Join(state.basePath, relPath) return filepath.Join(st.basePath, relPath)
} }
// normalizes relative path to absolute one // normalizes relative path to absolute one
func (state *HelmState) normalizePath(path string) string { func (st *HelmState) normalizePath(path string) string {
if filepath.IsAbs(path) { if filepath.IsAbs(path) {
return path return path
} else { } 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") 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{} flags := []string{}
if release.Version != "" { if release.Version != "" {
flags = append(flags, "--version", release.Version) flags = append(flags, "--version", release.Version)
} }
if state.isDevelopment(release) { if st.isDevelopment(release) {
flags = append(flags, "--devel") 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") 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") flags = append(flags, "--wait")
} }
timeout := state.HelmDefaults.Timeout timeout := st.HelmDefaults.Timeout
if release.Timeout != nil { if release.Timeout != nil {
timeout = *release.Timeout timeout = *release.Timeout
} }
@ -1026,51 +1032,51 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas
flags = append(flags, "--timeout", fmt.Sprintf("%d", timeout)) 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") 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") flags = append(flags, "--recreate-pods")
} }
common, err := state.namespaceAndValuesFlags(helm, release) common, err := st.namespaceAndValuesFlags(helm, release)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return append(flags, common...), nil 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{ flags := []string{
"--name", release.Name, "--name", release.Name,
} }
common, err := state.namespaceAndValuesFlags(helm, release) common, err := st.namespaceAndValuesFlags(helm, release)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return append(flags, common...), nil 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{} flags := []string{}
if release.Version != "" { if release.Version != "" {
flags = append(flags, "--version", release.Version) flags = append(flags, "--version", release.Version)
} }
if state.isDevelopment(release) { if st.isDevelopment(release) {
flags = append(flags, "--devel") flags = append(flags, "--devel")
} }
common, err := state.namespaceAndValuesFlags(helm, release) common, err := st.namespaceAndValuesFlags(helm, release)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return append(flags, common...), nil return append(flags, common...), nil
} }
func (state *HelmState) isDevelopment(release *ReleaseSpec) bool { func (st *HelmState) isDevelopment(release *ReleaseSpec) bool {
result := state.HelmDefaults.Devel result := st.HelmDefaults.Devel
if release.Devel != nil { if release.Devel != nil {
result = *release.Devel result = *release.Devel
} }
@ -1078,16 +1084,16 @@ func (state *HelmState) isDevelopment(release *ReleaseSpec) bool {
return result return result
} }
func (state *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
return state.namespaceAndValuesFlags(helm, release) return st.namespaceAndValuesFlags(helm, release)
} }
func (state *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) { func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
r := valuesfile.NewRenderer(state.readFile, filepath.Dir(path), state.Env) r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.envTemplateData())
return r.RenderToBytes(path) 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{} flags := []string{}
if release.Namespace != "" { if release.Namespace != "" {
flags = append(flags, "--namespace", 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 { for _, value := range release.Values {
switch typedValue := value.(type) { switch typedValue := value.(type) {
case string: case string:
path := state.normalizePath(release.ValuesPathPrefix + typedValue) path := st.normalizePath(release.ValuesPathPrefix + typedValue)
if _, err := os.Stat(path); os.IsNotExist(err) { 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 { if err != nil {
return nil, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err) 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 { if _, err := valfile.Write(yamlBytes); err != nil {
return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err) 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()) flags = append(flags, "--values", valfile.Name())
case map[interface{}]interface{}: case map[interface{}]interface{}:
@ -1133,10 +1144,16 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release
flags = append(flags, "--values", valfile.Name()) flags = append(flags, "--values", valfile.Name())
} }
} }
for _, value := range release.Secrets { 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) { 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) valfile, err := helm.DecryptSecret(path)
@ -1152,7 +1169,7 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release
if set.Value != "" { if set.Value != "" {
flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(set.Value))) flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(set.Value)))
} else if set.File != "" { } 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 { } else if len(set.Values) > 0 {
items := make([]string, len(set.Values)) items := make([]string, len(set.Values))
for i, raw := range set.Values { for i, raw := range set.Values {

32
state/state_exec_tmpl.go Normal file
View File

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

View File

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

24
state/types.go Normal file
View File

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

View File

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

78
tmpl/file_renderer.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package valuesfile package tmpl
import ( import (
"fmt" "fmt"
@ -7,6 +7,11 @@ import (
"testing" "testing"
) )
var emptyEnvTmplData = map[string]interface{}{
"Environment": environment.EmptyEnvironment,
"Namespace": "",
}
func TestRenderToBytes_Gotmpl(t *testing.T) { func TestRenderToBytes_Gotmpl(t *testing.T) {
valuesYamlTmplContent := `foo: valuesYamlTmplContent := `foo:
bar: '{{ readFile "data.txt" }}' bar: '{{ readFile "data.txt" }}'
@ -17,7 +22,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
` `
dataFile := "data.txt" dataFile := "data.txt"
valuesTmplFile := "values.yaml.gotmpl" valuesTmplFile := "values.yaml.gotmpl"
r := NewRenderer(func(filename string) ([]byte, error) { r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename { switch filename {
case valuesTmplFile: case valuesTmplFile:
return []byte(valuesYamlTmplContent), nil return []byte(valuesYamlTmplContent), nil
@ -25,7 +30,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
return []byte(dataFileContent), nil return []byte(dataFileContent), nil
} }
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename) return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
}, "", environment.EmptyEnvironment) }, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesTmplFile) buf, err := r.RenderToBytes(valuesTmplFile)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
@ -44,13 +49,13 @@ func TestRenderToBytes_Yaml(t *testing.T) {
bar: '{{ readFile "data.txt" }}' bar: '{{ readFile "data.txt" }}'
` `
valuesFile := "values.yaml" valuesFile := "values.yaml"
r := NewRenderer(func(filename string) ([]byte, error) { r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename { switch filename {
case valuesFile: case valuesFile:
return []byte(valuesYamlContent), nil return []byte(valuesYamlContent), nil
} }
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename) return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
}, "", environment.EmptyEnvironment) }, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesFile) buf, err := r.RenderToBytes(valuesFile)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)

View File

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