parent
7c793fdb88
commit
ed0854a5c0
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
64
README.md
64
README.md
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package environment
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
Name string
|
||||||
|
Values map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var EmptyEnvironment Environment
|
||||||
24
main.go
24
main.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
type EnvironmentSpec struct {
|
||||||
|
Values []string `yaml:"values"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: "./..",
|
||||||
|
|
|
||||||
14
tmpl/file.go
14
tmpl/file.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue