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]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "57e868f6ae57c81a07ee682742f3b71bf5c7956311a3bb8ea76459677fc104c7"
inputs-digest = "b1f000751afc0a44973307c69b6a4b8e8c1b807fd9881a13f370c30fcbcab7a2"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -9,3 +9,7 @@
[prune]
go-tests = 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" }}
```
## 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
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"
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
@ -68,6 +69,10 @@ func main() {
Name: "file, f",
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{
Name: "quiet, q",
Usage: "Silence output. Equivalent to log-level warn",
@ -463,10 +468,16 @@ func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*st
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
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)
if err != nil {
return err
@ -474,7 +485,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
allSelectorNotMatched := true
for _, f := range desiredStateFiles {
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 {
return err
}
@ -484,6 +495,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
kubeContext,
namespace,
selectors,
env,
logger,
)
if err != nil {
@ -498,7 +510,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
}
sort.Strings(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)
}
}
@ -579,8 +591,8 @@ func directoryExistsAt(path string) bool {
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) {
st, err := state.CreateFromYaml(yaml, file, logger)
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, env, logger)
if err != nil {
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:
stage: post
`)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, logger)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, "default", logger)
if err == nil {
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"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
@ -21,7 +22,8 @@ import (
// HelmState structure for the helmfile
type HelmState struct {
BaseChartPath string
basePath string
Environments map[string]EnvironmentSpec
FilePath string
HelmDefaults HelmSpec `yaml:"helmDefaults"`
Helmfiles []string `yaml:"helmfiles"`
@ -31,7 +33,11 @@ type HelmState struct {
Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"`
env environment.Environment
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
}
// HelmSpec to defines helmDefault values
@ -98,27 +104,7 @@ type SetValue struct {
Values []string `yaml:"values"`
}
func CreateFromYaml(content []byte, file string, logger *zap.SugaredLogger) (*HelmState, error) {
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
}
const DefaultEnv = "default"
func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if state.Namespace != "" {
@ -196,7 +182,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
continue
}
chart := normalizeChart(state.BaseChartPath, release.Chart)
chart := normalizeChart(state.basePath, release.Chart)
if err := helm.SyncRelease(release.Name, chart, flags...); err != nil {
errQueue <- &ReleaseError{release, err}
}
@ -249,7 +235,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
state.applyDefaultsTo(release)
flags, err := state.flagsForDiff(helm, state.BaseChartPath, release)
flags, err := state.flagsForDiff(helm, release)
if err != nil {
errs = append(errs, err)
}
@ -271,7 +257,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
}
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)
}
}
@ -333,7 +319,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
go func() {
for release := range jobQueue {
errs := []error{}
flags, err := state.flagsForLint(helm, state.BaseChartPath, release)
flags, err := state.flagsForLint(helm, release)
if err != nil {
errs = append(errs, err)
}
@ -350,8 +336,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
}
chartPath := ""
if pathExists(normalizeChart(state.BaseChartPath, release.Chart)) {
chartPath = normalizeChart(state.BaseChartPath, release.Chart)
if pathExists(normalizeChart(state.basePath, release.Chart)) {
chartPath = normalizeChart(state.basePath, release.Chart)
} else {
fetchFlags := []string{}
if release.Version != "" {
@ -571,7 +557,7 @@ func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error {
for _, release := range state.Releases {
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)
}
}
@ -638,30 +624,35 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas
flags = append(flags, "--recreate-pods")
}
common, err := state.namespaceAndValuesFlags(helm, state.BaseChartPath, release)
common, err := state.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
}
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{}
if release.Version != "" {
flags = append(flags, "--version", release.Version)
}
common, err := state.namespaceAndValuesFlags(helm, basePath, release)
common, err := state.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
}
return append(flags, common...), nil
}
func (state *HelmState) flagsForLint(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
return state.namespaceAndValuesFlags(helm, basePath, release)
func (state *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
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{}
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
@ -673,24 +664,20 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat
if filepath.IsAbs(typedValue) {
path = typedValue
} else {
path = filepath.Join(basePath, typedValue)
path = filepath.Join(state.basePath, typedValue)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
yamlBytes, err := state.RenderValuesFileToBytes(path)
valfile, err := ioutil.TempFile("", "values")
if err != nil {
return nil, err
}
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 {
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 {
path := filepath.Join(basePath, value)
path := filepath.Join(state.basePath, value)
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}

View File

@ -12,158 +12,6 @@ import (
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) {
cases := []struct {
labelString string
@ -266,7 +114,7 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
BaseChartPath: tt.fields.BaseChartPath,
basePath: tt.fields.BaseChartPath,
Context: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
Namespace: tt.fields.Namespace,
@ -495,10 +343,10 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
BaseChartPath: "./",
Context: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
basePath: "./",
Context: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
}
helm := helmexec.New(logger, "default")
args, err := state.flagsForUpgrade(helm, tt.release)
@ -861,7 +709,7 @@ func TestHelmState_SyncReleases(t *testing.T) {
func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{
BaseChartPath: "/src",
basePath: "/src",
Releases: []ReleaseSpec{
{
Chart: "./..",

View File

@ -2,24 +2,34 @@ package tmpl
import (
"bytes"
"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
}
type FileRenderer interface {
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{
ReadFile: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: TemplateData{
Environment: env,
},
}
}
@ -29,5 +39,5 @@ func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.B
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)
}
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)
if parseErr != nil {
return nil, parseErr
}
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 {
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) {
ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)

View File

@ -2,6 +2,7 @@ package valuesfile
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/tmpl"
"strings"
)
@ -11,10 +12,10 @@ type renderer struct {
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{
readFile: readFile,
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath),
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath, env),
}
}

View File

@ -2,6 +2,7 @@ package valuesfile
import (
"fmt"
"github.com/roboll/helmfile/environment"
"reflect"
"testing"
)
@ -24,7 +25,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)
buf, err := r.RenderToBytes(valuesTmplFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
@ -49,7 +50,7 @@ func TestRenderToBytes_Yaml(t *testing.T) {
return []byte(valuesYamlContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
}, "")
}, "", environment.EmptyEnvironment)
buf, err := r.RenderToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)