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,7 +343,7 @@ 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, | ||||||
|  | @ -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