feat: Environment and Environment Values (#267)

Resolves #253
This commit is contained in:
KUOKA Yusuke 2018-08-31 22:59:27 +09:00 committed by GitHub
parent 7c793fdb88
commit ed0854a5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 509 additions and 217 deletions

2
Gopkg.lock generated
View File

@ -85,6 +85,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "57e868f6ae57c81a07ee682742f3b71bf5c7956311a3bb8ea76459677fc104c7" inputs-digest = "b1f000751afc0a44973307c69b6a4b8e8c1b807fd9881a13f370c30fcbcab7a2"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -9,3 +9,7 @@
[prune] [prune]
go-tests = true go-tests = true
unused-packages = true unused-packages = true
[[constraint]]
name = "github.com/imdario/mergo"
version = "0.3.4"

View File

@ -371,6 +371,70 @@ proxy:
scheme: {{ env "SCHEME" | default "https" }} scheme: {{ env "SCHEME" | default "https" }}
``` ```
## Environment
When you want to customize the contents of `helmfile.yaml` or `values.yaml` files per environment, use this feature.
You can define as many environments as you want under `environments` in `helmfile.yaml`.
The environment name defaults to `default`, that is, `helmfile sync` implies the `default` environment.
The selected environment name can be referenced from `helmfile.yaml` and `values.yaml.gotmpl` by `{{ .Environment.Name }}`.
If you want to specify a non-default environment, provide a `--environment NAME` flag to `helmfile` like `helmfile --environment production sync`.
The below example shows how to define a production-only release:
```yaml
environments:
default:
production:
releases:
{{ if (eq .Environment.Name "production" }}
- name: newrelic-agent
# snip
{{ end }}
- name: myapp
# snip
```
## Environment Values
Environment Values allows you to inject a set of values specific to the selected environment, into values.yaml templates.
Use it to inject common values from the environment to multiple values files, to make your configuration DRY.
Suppose you have three files `helmfile.yaml`, `production.yaml` and `values.yaml.gotmpl`:
`helmfile.yaml`
```yaml
environments:
production:
values:
- production.yaml
releases:
- name: myapp
values:
- values.yaml.gotmpl
```
`production.yaml`
```yaml
domain: prod.example.com
```
`values.yaml.gotmpl`
```yaml
domain: {{ .Environment.Values.domain | default "dev.example.com" }}
```
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
whereas `helmfile --environment production sync` installs the app with the value `domain=production.example.com`.
## Separating helmfile.yaml into multiple independent files ## Separating helmfile.yaml into multiple independent files
Once your `helmfile.yaml` got to contain too many releases, Once your `helmfile.yaml` got to contain too many releases,

View File

@ -0,0 +1,8 @@
package environment
type Environment struct {
Name string
Values map[string]interface{}
}
var EmptyEnvironment Environment

24
main.go
View File

@ -12,6 +12,7 @@ import (
"os/exec" "os/exec"
"github.com/roboll/helmfile/args" "github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state" "github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl" "github.com/roboll/helmfile/tmpl"
@ -68,6 +69,10 @@ func main() {
Name: "file, f", Name: "file, f",
Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference", Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference",
}, },
cli.StringFlag{
Name: "environment, e",
Usage: "specify the environment name. defaults to `default`",
},
cli.BoolFlag{ cli.BoolFlag{
Name: "quiet, q", Name: "quiet, q",
Usage: "Silence output. Equivalent to log-level warn", Usage: "Silence output. Equivalent to log-level warn",
@ -463,10 +468,16 @@ func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*st
namespace := c.GlobalString("namespace") namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector") selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger) logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
return findAndIterateOverDesiredStates(fileOrDir, converge, kubeContext, namespace, selectors, logger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
return findAndIterateOverDesiredStates(fileOrDir, converge, kubeContext, namespace, selectors, env, logger)
} }
func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, kubeContext, namespace string, selectors []string, logger *zap.SugaredLogger) error { func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, kubeContext, namespace string, selectors []string, env string, logger *zap.SugaredLogger) error {
desiredStateFiles, err := findDesiredStateFiles(fileOrDir) desiredStateFiles, err := findDesiredStateFiles(fileOrDir)
if err != nil { if err != nil {
return err return err
@ -474,7 +485,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
allSelectorNotMatched := true allSelectorNotMatched := true
for _, f := range desiredStateFiles { for _, f := range desiredStateFiles {
logger.Debugf("Processing %s", f) logger.Debugf("Processing %s", f)
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "").RenderTemplateFileToBuffer(f) yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "", environment.EmptyEnvironment).RenderTemplateFileToBuffer(f)
if err != nil { if err != nil {
return err return err
} }
@ -484,6 +495,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
kubeContext, kubeContext,
namespace, namespace,
selectors, selectors,
env,
logger, logger,
) )
if err != nil { if err != nil {
@ -498,7 +510,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
} }
sort.Strings(matches) sort.Strings(matches)
for _, m := range matches { for _, m := range matches {
if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, logger); err != nil { if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, env, logger); err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err) return fmt.Errorf("failed processing %s: %v", globPattern, err)
} }
} }
@ -579,8 +591,8 @@ func directoryExistsAt(path string) bool {
return err == nil && fileInfo.Mode().IsDir() return err == nil && fileInfo.Mode().IsDir()
} }
func loadDesiredStateFromFile(yaml []byte, file string, kubeContext, namespace string, labels []string, logger *zap.SugaredLogger) (*state.HelmState, helmexec.Interface, bool, error) { func loadDesiredStateFromFile(yaml []byte, file string, kubeContext, namespace string, labels []string, env string, logger *zap.SugaredLogger) (*state.HelmState, helmexec.Interface, bool, error) {
st, err := state.CreateFromYaml(yaml, file, logger) st, err := state.CreateFromYaml(yaml, file, env, logger)
if err != nil { if err != nil {
return nil, nil, false, fmt.Errorf("failed to read %s: %v", file, err) return nil, nil, false, fmt.Errorf("failed to read %s: %v", file, err)
} }

View File

@ -16,7 +16,7 @@ func TestReadFromYaml_DuplicateReleaseName(t *testing.T) {
labels: labels:
stage: post stage: post
`) `)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, logger) _, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, "default", logger)
if err == nil { if err == nil {
t.Error("error expected but not happened") t.Error("error expected but not happened")
} }

71
state/create.go Normal file
View File

@ -0,0 +1,71 @@
package state
import (
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io/ioutil"
"path/filepath"
)
func CreateFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
return createFromYamlWithFileReader(content, file, env, logger, ioutil.ReadFile)
}
func createFromYamlWithFileReader(content []byte, file string, env string, logger *zap.SugaredLogger, readFile func(string) ([]byte, error)) (*HelmState, error) {
var state HelmState
state.basePath, _ = filepath.Abs(filepath.Dir(file))
if err := yaml.UnmarshalStrict(content, &state); err != nil {
return nil, err
}
state.FilePath = file
if len(state.DeprecatedReleases) > 0 {
if len(state.Releases) > 0 {
return nil, fmt.Errorf("failed to parse %s: you can't specify both `charts` and `releases` sections", file)
}
state.Releases = state.DeprecatedReleases
state.DeprecatedReleases = []ReleaseSpec{}
}
state.logger = logger
e, err := state.loadEnv(env, readFile)
if err != nil {
return nil, err
}
state.env = *e
state.readFile = readFile
return &state, nil
}
func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := state.Environments[name]
if ok {
r := valuesfile.NewRenderer(readFile, state.basePath, environment.EmptyEnvironment)
for _, envvalFile := range envSpec.Values {
bytes, err := r.RenderToBytes(filepath.Join(state.basePath, envvalFile))
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFile, err)
}
}
} else if name != DefaultEnv {
return nil, fmt.Errorf("environment \"%s\" is not defined in \"%s\"", name, state.FilePath)
}
return &environment.Environment{Name: name, Values: envVals}, nil
}

248
state/create_test.go Normal file
View File

@ -0,0 +1,248 @@
package state
import (
"fmt"
"reflect"
"testing"
)
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
if state.Releases[0].Name != "myrelease" {
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
}
if state.Releases[0].Namespace != "mynamespace" {
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_InexistentEnv(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, "production", logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
production:
values:
- foo.yaml
- bar.yaml.gotmpl
releases:
- name: myrelease
namespace: mynamespace
chart: mychart
values:
- values.yaml.gotmpl
`)
fooYamlFile := "/example/path/to/foo.yaml"
fooYamlContent := []byte(`foo: foo
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
baz: "{{ readFile \"baz.txt\" }}"`)
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
barYamlContent := []byte(`foo: FOO
bar: {{ readFile "bar.txt" }}
`)
barTextFile := "/example/path/to/bar.txt"
barTextContent := []byte("BAR")
expected := map[string]interface{}{
"foo": "FOO",
"bar": "BAR",
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
"baz": "{{ readFile \"baz.txt\" }}",
}
valuesFile := "/example/path/to/values.yaml.gotmpl"
valuesContent := []byte(`env: {{ .Environment.Name }}`)
expectedValues := `env: production`
readFile := func(filename string) ([]byte, error) {
switch filename {
case fooYamlFile:
return fooYamlContent, nil
case barYamlFile:
return barYamlContent, nil
case barTextFile:
return barTextContent, nil
case valuesFile:
return valuesContent, nil
}
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
state, err := createFromYamlWithFileReader(yamlContent, yamlFile, "production", logger, readFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := state.env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actualValues := string(actualValuesData)
if !reflect.DeepEqual(expectedValues, actualValues) {
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
}
}
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
releases: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
}
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
if state.Releases[0].Name != "myrelease" {
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease1
chart: mychart1
releases:
- name: myrelease2
chart: mychart2
`)
_, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
tier: frontend
foo: bar
- name: myrelease2
chart: mychart2
labels:
tier: frontend
- name: myrelease3
chart: mychart3
labels:
tier: backend
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestReadFromYaml_FilterNegatives(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease2
chart: mychart2
labels:
stage: post
- name: myrelease3
chart: mychart3
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
[]bool{true, false, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}

5
state/environment.go Normal file
View File

@ -0,0 +1,5 @@
package state
type EnvironmentSpec struct {
Values []string `yaml:"values"`
}

View File

@ -14,6 +14,7 @@ import (
"regexp" "regexp"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/valuesfile" "github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap" "go.uber.org/zap"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -21,7 +22,8 @@ import (
// HelmState structure for the helmfile // HelmState structure for the helmfile
type HelmState struct { type HelmState struct {
BaseChartPath string basePath string
Environments map[string]EnvironmentSpec
FilePath string FilePath string
HelmDefaults HelmSpec `yaml:"helmDefaults"` HelmDefaults HelmSpec `yaml:"helmDefaults"`
Helmfiles []string `yaml:"helmfiles"` Helmfiles []string `yaml:"helmfiles"`
@ -31,7 +33,11 @@ type HelmState struct {
Repositories []RepositorySpec `yaml:"repositories"` Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"` Releases []ReleaseSpec `yaml:"releases"`
env environment.Environment
logger *zap.SugaredLogger logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
} }
// HelmSpec to defines helmDefault values // HelmSpec to defines helmDefault values
@ -98,27 +104,7 @@ type SetValue struct {
Values []string `yaml:"values"` Values []string `yaml:"values"`
} }
func CreateFromYaml(content []byte, file string, logger *zap.SugaredLogger) (*HelmState, error) { const DefaultEnv = "default"
var state HelmState
state.BaseChartPath, _ = filepath.Abs(filepath.Dir(file))
if err := yaml.UnmarshalStrict(content, &state); err != nil {
return nil, err
}
state.FilePath = file
if len(state.DeprecatedReleases) > 0 {
if len(state.Releases) > 0 {
return nil, fmt.Errorf("failed to parse %s: you can't specify both `charts` and `releases` sections", file)
}
state.Releases = state.DeprecatedReleases
state.DeprecatedReleases = []ReleaseSpec{}
}
state.logger = logger
return &state, nil
}
func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) { func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if state.Namespace != "" { if state.Namespace != "" {
@ -196,7 +182,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
continue continue
} }
chart := normalizeChart(state.BaseChartPath, release.Chart) chart := normalizeChart(state.basePath, release.Chart)
if err := helm.SyncRelease(release.Name, chart, flags...); err != nil { if err := helm.SyncRelease(release.Name, chart, flags...); err != nil {
errQueue <- &ReleaseError{release, err} errQueue <- &ReleaseError{release, err}
} }
@ -249,7 +235,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
state.applyDefaultsTo(release) state.applyDefaultsTo(release)
flags, err := state.flagsForDiff(helm, state.BaseChartPath, release) flags, err := state.flagsForDiff(helm, release)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -271,7 +257,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
} }
if len(errs) == 0 { if len(errs) == 0 {
if err := helm.DiffRelease(release.Name, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil { if err := helm.DiffRelease(release.Name, normalizeChart(state.basePath, release.Chart), flags...); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} }
@ -333,7 +319,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
go func() { go func() {
for release := range jobQueue { for release := range jobQueue {
errs := []error{} errs := []error{}
flags, err := state.flagsForLint(helm, state.BaseChartPath, release) flags, err := state.flagsForLint(helm, release)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -350,8 +336,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
} }
chartPath := "" chartPath := ""
if pathExists(normalizeChart(state.BaseChartPath, release.Chart)) { if pathExists(normalizeChart(state.basePath, release.Chart)) {
chartPath = normalizeChart(state.BaseChartPath, release.Chart) chartPath = normalizeChart(state.basePath, release.Chart)
} else { } else {
fetchFlags := []string{} fetchFlags := []string{}
if release.Version != "" { if release.Version != "" {
@ -571,7 +557,7 @@ func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error {
for _, release := range state.Releases { for _, release := range state.Releases {
if isLocalChart(release.Chart) { if isLocalChart(release.Chart) {
if err := helm.UpdateDeps(normalizeChart(state.BaseChartPath, release.Chart)); err != nil { if err := helm.UpdateDeps(normalizeChart(state.basePath, release.Chart)); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} }
@ -638,30 +624,35 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas
flags = append(flags, "--recreate-pods") flags = append(flags, "--recreate-pods")
} }
common, err := state.namespaceAndValuesFlags(helm, state.BaseChartPath, release) common, err := state.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, basePath string, release *ReleaseSpec) ([]string, error) { func (state *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)
} }
common, err := state.namespaceAndValuesFlags(helm, basePath, release) common, err := state.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) flagsForLint(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) { func (state *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
return state.namespaceAndValuesFlags(helm, basePath, release) return state.namespaceAndValuesFlags(helm, release)
} }
func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) { func (state *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
r := valuesfile.NewRenderer(state.readFile, state.basePath, state.env)
return r.RenderToBytes(path)
}
func (state *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)
@ -673,24 +664,20 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat
if filepath.IsAbs(typedValue) { if filepath.IsAbs(typedValue) {
path = typedValue path = typedValue
} else { } else {
path = filepath.Join(basePath, typedValue) path = filepath.Join(state.basePath, typedValue)
} }
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
} }
yamlBytes, err := state.RenderValuesFileToBytes(path)
valfile, err := ioutil.TempFile("", "values") valfile, err := ioutil.TempFile("", "values")
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer valfile.Close() defer valfile.Close()
r := valuesfile.NewRenderer(ioutil.ReadFile, state.BaseChartPath)
yamlBytes, err := r.RenderToBytes(path)
if err != nil {
return nil, err
}
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)
} }
@ -713,7 +700,7 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat
} }
} }
for _, value := range release.Secrets { for _, value := range release.Secrets {
path := filepath.Join(basePath, value) path := filepath.Join(state.basePath, value)
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
} }

View File

@ -12,158 +12,6 @@ import (
var logger = helmexec.NewLogger(os.Stdout, "warn") var logger = helmexec.NewLogger(os.Stdout, "warn")
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
if state.Releases[0].Name != "myrelease" {
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
}
if state.Releases[0].Namespace != "mynamespace" {
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
releases: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
}
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
if state.Releases[0].Name != "myrelease" {
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease1
chart: mychart1
releases:
- name: myrelease2
chart: mychart2
`)
_, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
tier: frontend
foo: bar
- name: myrelease2
chart: mychart2
labels:
tier: frontend
- name: myrelease3
chart: mychart3
labels:
tier: backend
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestReadFromYaml_FilterNegatives(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease2
chart: mychart2
labels:
stage: post
- name: myrelease3
chart: mychart3
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
[]bool{true, false, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestLabelParsing(t *testing.T) { func TestLabelParsing(t *testing.T) {
cases := []struct { cases := []struct {
labelString string labelString string
@ -266,7 +114,7 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
state := &HelmState{ state := &HelmState{
BaseChartPath: tt.fields.BaseChartPath, basePath: tt.fields.BaseChartPath,
Context: tt.fields.Context, Context: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases, DeprecatedReleases: tt.fields.DeprecatedReleases,
Namespace: tt.fields.Namespace, Namespace: tt.fields.Namespace,
@ -495,10 +343,10 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
state := &HelmState{ state := &HelmState{
BaseChartPath: "./", basePath: "./",
Context: "default", Context: "default",
Releases: []ReleaseSpec{*tt.release}, Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults, HelmDefaults: tt.defaults,
} }
helm := helmexec.New(logger, "default") helm := helmexec.New(logger, "default")
args, err := state.flagsForUpgrade(helm, tt.release) args, err := state.flagsForUpgrade(helm, tt.release)
@ -861,7 +709,7 @@ func TestHelmState_SyncReleases(t *testing.T) {
func TestHelmState_UpdateDeps(t *testing.T) { func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{ state := &HelmState{
BaseChartPath: "/src", basePath: "/src",
Releases: []ReleaseSpec{ Releases: []ReleaseSpec{
{ {
Chart: "./..", Chart: "./..",

View File

@ -2,24 +2,34 @@ package tmpl
import ( import (
"bytes" "bytes"
"github.com/roboll/helmfile/environment"
) )
type templateFileRenderer struct { type templateFileRenderer struct {
ReadFile func(string) ([]byte, error) ReadFile func(string) ([]byte, error)
Context *Context Context *Context
Data TemplateData
}
type TemplateData struct {
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
} }
type FileRenderer interface { type FileRenderer interface {
RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error)
} }
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string) *templateFileRenderer { func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *templateFileRenderer {
return &templateFileRenderer{ return &templateFileRenderer{
ReadFile: readFile, ReadFile: readFile,
Context: &Context{ Context: &Context{
basePath: basePath, basePath: basePath,
readFile: readFile, readFile: readFile,
}, },
Data: TemplateData{
Environment: env,
},
} }
} }
@ -29,5 +39,5 @@ func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.B
return nil, err return nil, err
} }
return r.Context.RenderTemplateToBuffer(string(content)) return r.Context.RenderTemplateToBuffer(string(content), r.Data)
} }

View File

@ -14,14 +14,18 @@ func (c *Context) stringTemplate() *template.Template {
return template.New("stringTemplate").Funcs(funcMap) return template.New("stringTemplate").Funcs(funcMap)
} }
func (c *Context) RenderTemplateToBuffer(s string) (*bytes.Buffer, error) { func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) {
var t, parseErr = c.stringTemplate().Parse(s) var t, parseErr = c.stringTemplate().Parse(s)
if parseErr != nil { if parseErr != nil {
return nil, parseErr return nil, parseErr
} }
var tplString bytes.Buffer var tplString bytes.Buffer
var execErr = t.Execute(&tplString, nil) var d interface{}
if len(data) > 0 {
d = data[0]
}
var execErr = t.Execute(&tplString, d)
if execErr != nil { if execErr != nil {
return nil, execErr return nil, execErr

View File

@ -31,6 +31,35 @@ func TestRenderTemplate_Values(t *testing.T) {
} }
} }
func TestRenderTemplate_WithData(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ .foo.bar }}
`
expected := `foo:
bar: FOO_BAR
`
expectedFilename := "values.yaml"
data := map[string]interface{}{
"foo": map[string]interface{}{
"bar": "FOO_BAR",
},
}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func renderTemplateToString(s string) (string, error) { func renderTemplateToString(s string) (string, error) {
ctx := &Context{readFile: func(filename string) ([]byte, error) { ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename) return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)

View File

@ -2,6 +2,7 @@ package valuesfile
import ( import (
"fmt" "fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/tmpl" "github.com/roboll/helmfile/tmpl"
"strings" "strings"
) )
@ -11,10 +12,10 @@ type renderer struct {
tmplFileRenderer tmpl.FileRenderer tmplFileRenderer tmpl.FileRenderer
} }
func NewRenderer(readFile func(filename string) ([]byte, error), basePath string) *renderer { func NewRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *renderer {
return &renderer{ return &renderer{
readFile: readFile, readFile: readFile,
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath), tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath, env),
} }
} }

View File

@ -2,6 +2,7 @@ package valuesfile
import ( import (
"fmt" "fmt"
"github.com/roboll/helmfile/environment"
"reflect" "reflect"
"testing" "testing"
) )
@ -24,7 +25,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)
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)
@ -49,7 +50,7 @@ func TestRenderToBytes_Yaml(t *testing.T) {
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)
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)