parent
7c793fdb88
commit
ed0854a5c0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,3 +9,7 @@
|
|||
[prune]
|
||||
go-tests = 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" }}
|
||||
```
|
||||
|
||||
## 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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +343,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
BaseChartPath: "./",
|
||||
basePath: "./",
|
||||
Context: "default",
|
||||
Releases: []ReleaseSpec{*tt.release},
|
||||
HelmDefaults: tt.defaults,
|
||||
|
|
@ -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: "./..",
|
||||
|
|
|
|||
14
tmpl/file.go
14
tmpl/file.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue