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:
parent
23178b398c
commit
f813ac2642
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
9
main.go
9
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
241
state/state.go
241
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
66
tmpl/file.go
66
tmpl/file.go
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue