feat: optionally allow missing environment values/secrets files (#620)
```yaml
environments:
  default:
    missingFileHandler: Warn
    values:
    - path/to/values.yaml
    secrets:
    - path/to/secrets.yaml
```
`missingFileHandler` set to `Warn`, `Info`, or `Debug` results in helmfile NOT stop when `path/to/values.yaml` or `path/to/secrets.yaml` is missing.
Resolves #548
While implementing the above feature, I also found a bug that has been causing #559. This also fixes that.
To verify it is actually fixed, create an example helmfile.yaml that looks like the below, and run `helmfile diff`:
```
$ cat helmfile.yaml
environments:
  default:
    secrets:
      - env-secrets.yaml
releases:
  - name: myapp
    chart: nginx
    namespace: default
    secrets: [secrets.yaml]    # Notice this file does not exist
    values:
      - ingress:
          enabled: true
$ helmfile diff
could not deduce `environment:` block, configuring only .Environment.Name. error: failed to read helmfile.yaml.part.0: environment values file matching "env-secrets.yaml" does not exist
in ./helmfile.yaml: failed to read helmfile.yaml: environment values file matching "env-secrets.yaml" does not exist
```
Fixes #559
			
			
This commit is contained in:
		
							parent
							
								
									4a5996d083
								
							
						
					
					
						commit
						a896f801ab
					
				
							
								
								
									
										46
									
								
								README.md
								
								
								
								
							
							
						
						
									
										46
									
								
								README.md
								
								
								
								
							|  | @ -30,6 +30,10 @@ To avoid upgrades for each iteration of `helm`, the `helmfile` executable delega | ||||||
| The default helmfile is `helmfile.yaml`: | The default helmfile is `helmfile.yaml`: | ||||||
| 
 | 
 | ||||||
| ```yaml | ```yaml | ||||||
|  | # Chart repositories used from within this state file | ||||||
|  | # | ||||||
|  | # Use `helm-s3` and `helm-git` and whatever Helm Downloader plugins | ||||||
|  | # to use repositories other than the official repository or one backend by chartmuseum. | ||||||
| repositories: | repositories: | ||||||
|   - name: roboll |   - name: roboll | ||||||
|     url: http://roboll.io/charts |     url: http://roboll.io/charts | ||||||
|  | @ -63,6 +67,9 @@ helmDefaults: | ||||||
|   # path to TLS key file (default "$HELM_HOME/key.pem") |   # path to TLS key file (default "$HELM_HOME/key.pem") | ||||||
|   tlsKey: "path/to/key.pem" |   tlsKey: "path/to/key.pem" | ||||||
| 
 | 
 | ||||||
|  | # The desired states of Helm releases. | ||||||
|  | # | ||||||
|  | # Helmfile runs various helm commands to converge the current state in the live cluster to the desired state defined here. | ||||||
| releases: | releases: | ||||||
|   # Published chart example |   # Published chart example | ||||||
|   - name: vault                            # name of this release |   - name: vault                            # name of this release | ||||||
|  | @ -71,7 +78,7 @@ releases: | ||||||
|       foo: bar |       foo: bar | ||||||
|     chart: roboll/vault-secret-manager     # the chart being installed to create this release, referenced by `repository/chart` syntax |     chart: roboll/vault-secret-manager     # the chart being installed to create this release, referenced by `repository/chart` syntax | ||||||
|     version: ~1.24.1                       # the semver of the chart. range constraint is supported |     version: ~1.24.1                       # the semver of the chart. range constraint is supported | ||||||
|     missingFileHandler: warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues. |     missingFileHandler: Warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues. | ||||||
|     values: |     values: | ||||||
|       # value files passed via --values |       # value files passed via --values | ||||||
|       - vault.yaml |       - vault.yaml | ||||||
|  | @ -134,6 +141,43 @@ releases: | ||||||
|     - ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment. |     - ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment. | ||||||
|     wait: true |     wait: true | ||||||
| 
 | 
 | ||||||
|  | # | ||||||
|  | # Advanced Configuration: Helmfile Environments | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | # The list of environments managed by helmfile. | ||||||
|  | # | ||||||
|  | # The default is `environments: {"default": {}}` which implies: | ||||||
|  | # | ||||||
|  | # - `{{ .Environment.Name }}` evaluates to "default" | ||||||
|  | # - `{{ .Environment.Values }}` being empty | ||||||
|  | environments: | ||||||
|  |   # The "default" environment is available and used when `helmfile` is run without `--environment NAME`. | ||||||
|  |   default: | ||||||
|  |     # Everything from the values.yaml is available via `{{ .Environment.Values.KEY }}`. | ||||||
|  |     # Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below, | ||||||
|  |     # `{{ .Environment.Values.foo.bar }}` is evaluated to `1`. | ||||||
|  |     values: | ||||||
|  |     - environments/default/values.yaml | ||||||
|  |   # Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`. | ||||||
|  |   # That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`. | ||||||
|  |   production: | ||||||
|  |     values: | ||||||
|  |     - environment/production/values.yaml | ||||||
|  |     ## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Secrets.KEY }}` | ||||||
|  |     secrets: | ||||||
|  |     - environment/production/secrets.yaml | ||||||
|  |     # Overrides the `environmentDefaults.missingFileHandler` for this environment  | ||||||
|  |     missingFileHandler: Error | ||||||
|  | 
 | ||||||
|  | environmentDefaults: | ||||||
|  |   # Instructs helmfile to fail when unable to find a environment values file listed under `environments.NAME.values`. | ||||||
|  |   # | ||||||
|  |   # Possible values are  "Error", "Warn", "Info", "Debug". The default is "Error". | ||||||
|  |   # | ||||||
|  |   # Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving | ||||||
|  |   # a message about the missing file at the log-level. | ||||||
|  |   missingFileHandler: Error | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Templating | ## Templating | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ type App struct { | ||||||
| 	Selectors   []string | 	Selectors   []string | ||||||
| 
 | 
 | ||||||
| 	readFile          func(string) ([]byte, error) | 	readFile          func(string) ([]byte, error) | ||||||
|  | 	fileExists        func(string) (bool, error) | ||||||
| 	glob              func(string) ([]string, error) | 	glob              func(string) ([]string, error) | ||||||
| 	abs               func(string) (string, error) | 	abs               func(string) (string, error) | ||||||
| 	fileExistsAt      func(string) bool | 	fileExistsAt      func(string) bool | ||||||
|  | @ -42,6 +43,7 @@ func Init(app *App) *App { | ||||||
| 	app.getwd = os.Getwd | 	app.getwd = os.Getwd | ||||||
| 	app.chdir = os.Chdir | 	app.chdir = os.Chdir | ||||||
| 	app.fileExistsAt = fileExistsAt | 	app.fileExistsAt = fileExistsAt | ||||||
|  | 	app.fileExists = fileExists | ||||||
| 	app.directoryExistsAt = directoryExistsAt | 	app.directoryExistsAt = directoryExistsAt | ||||||
| 	return app | 	return app | ||||||
| } | } | ||||||
|  | @ -114,6 +116,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error { | ||||||
| func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) { | func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) { | ||||||
| 	ld := &desiredStateLoader{ | 	ld := &desiredStateLoader{ | ||||||
| 		readFile:   a.readFile, | 		readFile:   a.readFile, | ||||||
|  | 		fileExists: a.fileExists, | ||||||
| 		env:        a.Env, | 		env:        a.Env, | ||||||
| 		namespace:  a.Namespace, | 		namespace:  a.Namespace, | ||||||
| 		logger:     a.Logger, | 		logger:     a.Logger, | ||||||
|  | @ -316,6 +319,18 @@ func fileExistsAt(path string) bool { | ||||||
| 	return err == nil && fileInfo.Mode().IsRegular() | 	return err == nil && fileInfo.Mode().IsRegular() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func fileExists(path string) (bool, error) { | ||||||
|  | 	_, err := os.Stat(path) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		if os.IsNotExist(err) { | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func directoryExistsAt(path string) bool { | func directoryExistsAt(path string) bool { | ||||||
| 	fileInfo, err := os.Stat(path) | 	fileInfo, err := os.Stat(path) | ||||||
| 	return err == nil && fileInfo.Mode().IsDir() | 	return err == nil && fileInfo.Mode().IsDir() | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ func injectFs(app *App, fs *state.TestFs) *App { | ||||||
| 	app.getwd = fs.Getwd | 	app.getwd = fs.Getwd | ||||||
| 	app.chdir = fs.Chdir | 	app.chdir = fs.Chdir | ||||||
| 	app.fileExistsAt = fs.FileExistsAt | 	app.fileExistsAt = fs.FileExistsAt | ||||||
|  | 	app.fileExists = fs.FileExists | ||||||
| 	app.directoryExistsAt = fs.DirectoryExistsAt | 	app.directoryExistsAt = fs.DirectoryExistsAt | ||||||
| 	return app | 	return app | ||||||
| } | } | ||||||
|  | @ -155,12 +156,66 @@ releases: | ||||||
| 		t.Fatal("expected error did not occur") | 		t.Fatal("expected error did not occur") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	expected := "in ./helmfile.yaml: failed to read helmfile.yaml: no file matching env.*.yaml found" | 	expected := "in ./helmfile.yaml: failed to read helmfile.yaml: environment values file matching \"env.*.yaml\" does not exist" | ||||||
| 	if err.Error() != expected { | 	if err.Error() != expected { | ||||||
| 		t.Errorf("unexpected error: expected=%s, got=%v", expected, err) | 		t.Errorf("unexpected error: expected=%s, got=%v", expected, err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFileHandler(t *testing.T) { | ||||||
|  | 	testcases := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		handler     string | ||||||
|  | 		filePattern string | ||||||
|  | 		expectErr   bool | ||||||
|  | 	}{ | ||||||
|  | 		{name: "error handler with no files matching glob", handler: "Error", filePattern: "env.*.yaml", expectErr: true}, | ||||||
|  | 		{name: "warn handler with no files matching glob", handler: "Warn", filePattern: "env.*.yaml", expectErr: false}, | ||||||
|  | 		{name: "info handler with no files matching glob", handler: "Info", filePattern: "env.*.yaml", expectErr: false}, | ||||||
|  | 		{name: "debug handler with no files matching glob", handler: "Debug", filePattern: "env.*.yaml", expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := range testcases { | ||||||
|  | 		testcase := testcases[i] | ||||||
|  | 		t.Run(testcase.name, func(t *testing.T) { | ||||||
|  | 			files := map[string]string{ | ||||||
|  | 				"/path/to/helmfile.yaml": fmt.Sprintf(` | ||||||
|  | environments: | ||||||
|  |   default: | ||||||
|  |     missingFileHandler: %s | ||||||
|  |     values: | ||||||
|  |     - %s | ||||||
|  | releases: | ||||||
|  | - name: zipkin | ||||||
|  |   chart: stable/zipkin | ||||||
|  | `, testcase.handler, testcase.filePattern), | ||||||
|  | 			} | ||||||
|  | 			fs := state.NewTestFs(files) | ||||||
|  | 			app := &App{ | ||||||
|  | 				KubeContext: "default", | ||||||
|  | 				Logger:      helmexec.NewLogger(os.Stderr, "debug"), | ||||||
|  | 				Namespace:   "", | ||||||
|  | 				Env:         "default", | ||||||
|  | 			} | ||||||
|  | 			app = injectFs(app, fs) | ||||||
|  | 			noop := func(st *state.HelmState, helm helmexec.Interface) []error { | ||||||
|  | 				return []error{} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			err := app.VisitDesiredStatesWithReleasesFiltered( | ||||||
|  | 				"helmfile.yaml", noop, | ||||||
|  | 			) | ||||||
|  | 			if testcase.expectErr && err == nil { | ||||||
|  | 				t.Fatal("expected error did not occur") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if !testcase.expectErr && err != nil { | ||||||
|  | 				t.Errorf("not error expected, but got: %v", err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // See https://github.com/roboll/helmfile/issues/193
 | // See https://github.com/roboll/helmfile/issues/193
 | ||||||
| func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) { | func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) { | ||||||
| 	files := map[string]string{ | 	files := map[string]string{ | ||||||
|  | @ -749,13 +804,15 @@ helmDefaults: | ||||||
| 		readFile:     testFs.ReadFile, | 		readFile:     testFs.ReadFile, | ||||||
| 		glob:         testFs.Glob, | 		glob:         testFs.Glob, | ||||||
| 		abs:          testFs.Abs, | 		abs:          testFs.Abs, | ||||||
|  | 		fileExistsAt: testFs.FileExistsAt, | ||||||
|  | 		fileExists:   testFs.FileExists, | ||||||
| 		KubeContext:  "default", | 		KubeContext:  "default", | ||||||
| 		Env:          "default", | 		Env:          "default", | ||||||
| 		Logger:       helmexec.NewLogger(os.Stderr, "debug"), | 		Logger:       helmexec.NewLogger(os.Stderr, "debug"), | ||||||
| 	} | 	} | ||||||
| 	st, err := app.loadDesiredStateFromYaml(yamlFile) | 	st, err := app.loadDesiredStateFromYaml(yamlFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if st.HelmDefaults.TillerNamespace != "TILLER_NS" { | 	if st.HelmDefaults.TillerNamespace != "TILLER_NS" { | ||||||
|  | @ -826,6 +883,7 @@ helmDefaults: | ||||||
| 	}) | 	}) | ||||||
| 	app := &App{ | 	app := &App{ | ||||||
| 		readFile:   testFs.ReadFile, | 		readFile:   testFs.ReadFile, | ||||||
|  | 		fileExists: testFs.FileExists, | ||||||
| 		glob:       testFs.Glob, | 		glob:       testFs.Glob, | ||||||
| 		abs:        testFs.Abs, | 		abs:        testFs.Abs, | ||||||
| 		Env:        "default", | 		Env:        "default", | ||||||
|  | @ -901,6 +959,7 @@ foo: FOO | ||||||
| 	}) | 	}) | ||||||
| 	app := &App{ | 	app := &App{ | ||||||
| 		readFile:   testFs.ReadFile, | 		readFile:   testFs.ReadFile, | ||||||
|  | 		fileExists: testFs.FileExists, | ||||||
| 		glob:       testFs.Glob, | 		glob:       testFs.Glob, | ||||||
| 		abs:        testFs.Abs, | 		abs:        testFs.Abs, | ||||||
| 		Env:        "default", | 		Env:        "default", | ||||||
|  | @ -979,6 +1038,7 @@ helmDefaults: | ||||||
| 	}) | 	}) | ||||||
| 	app := &App{ | 	app := &App{ | ||||||
| 		readFile:   testFs.ReadFile, | 		readFile:   testFs.ReadFile, | ||||||
|  | 		fileExists: testFs.FileExists, | ||||||
| 		glob:       testFs.Glob, | 		glob:       testFs.Glob, | ||||||
| 		abs:        testFs.Abs, | 		abs:        testFs.Abs, | ||||||
| 		Env:        "test", | 		Env:        "test", | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ type desiredStateLoader struct { | ||||||
| 	namespace string | 	namespace string | ||||||
| 
 | 
 | ||||||
| 	readFile   func(string) ([]byte, error) | 	readFile   func(string) ([]byte, error) | ||||||
|  | 	fileExists func(string) (bool, error) | ||||||
| 	abs        func(string) (string, error) | 	abs        func(string) (string, error) | ||||||
| 	glob       func(string) ([]string, error) | 	glob       func(string) ([]string, error) | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +97,7 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *desiredStateLoader) underlying() *state.StateCreator { | func (a *desiredStateLoader) underlying() *state.StateCreator { | ||||||
| 	c := state.NewCreator(a.logger, a.readFile, a.abs, a.glob) | 	c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob) | ||||||
| 	c.LoadFile = a.loadFile | 	c.LoadFile = a.loadFile | ||||||
| 	return c | 	return c | ||||||
| } | } | ||||||
|  | @ -109,10 +110,13 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas | ||||||
| 
 | 
 | ||||||
| 	helmfiles := []state.SubHelmfileSpec{} | 	helmfiles := []state.SubHelmfileSpec{} | ||||||
| 	for _, hf := range st.Helmfiles { | 	for _, hf := range st.Helmfiles { | ||||||
| 		matches, err := st.ExpandPaths([]string{hf.Path}, a.glob) | 		matches, err := st.ExpandPaths(hf.Path) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | 		if len(matches) == 0 { | ||||||
|  | 			return nil, fmt.Errorf("no file matching %s found", hf.Path) | ||||||
|  | 		} | ||||||
| 		for _, match := range matches { | 		for _, match := range matches { | ||||||
| 			newHelmfile := hf | 			newHelmfile := hf | ||||||
| 			newHelmfile.Path = match | 			newHelmfile.Path = match | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ func makeLoader(files map[string]string, env string) (*desiredStateLoader, *stat | ||||||
| 		namespace:  "namespace", | 		namespace:  "namespace", | ||||||
| 		logger:     helmexec.NewLogger(os.Stdout, "debug"), | 		logger:     helmexec.NewLogger(os.Stdout, "debug"), | ||||||
| 		readFile:   testfs.ReadFile, | 		readFile:   testfs.ReadFile, | ||||||
|  | 		fileExists: testfs.FileExists, | ||||||
| 		abs:        testfs.Abs, | 		abs:        testfs.Abs, | ||||||
| 		glob:       testfs.Glob, | 		glob:       testfs.Glob, | ||||||
| 	}, testfs | 	}, testfs | ||||||
|  |  | ||||||
|  | @ -37,6 +37,7 @@ func (e *UndefinedEnvError) Error() string { | ||||||
| type StateCreator struct { | type StateCreator struct { | ||||||
| 	logger     *zap.SugaredLogger | 	logger     *zap.SugaredLogger | ||||||
| 	readFile   func(string) ([]byte, error) | 	readFile   func(string) ([]byte, error) | ||||||
|  | 	fileExists func(string) (bool, error) | ||||||
| 	abs        func(string) (string, error) | 	abs        func(string) (string, error) | ||||||
| 	glob       func(string) ([]string, error) | 	glob       func(string) ([]string, error) | ||||||
| 
 | 
 | ||||||
|  | @ -45,10 +46,11 @@ type StateCreator struct { | ||||||
| 	LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) | 	LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator { | func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator { | ||||||
| 	return &StateCreator{ | 	return &StateCreator{ | ||||||
| 		logger:     logger, | 		logger:     logger, | ||||||
| 		readFile:   readFile, | 		readFile:   readFile, | ||||||
|  | 		fileExists: fileExists, | ||||||
| 		abs:        abs, | 		abs:        abs, | ||||||
| 		glob:       glob, | 		glob:       glob, | ||||||
| 		Strict:     true, | 		Strict:     true, | ||||||
|  | @ -102,17 +104,8 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, | ||||||
| 
 | 
 | ||||||
| 	state.readFile = c.readFile | 	state.readFile = c.readFile | ||||||
| 	state.removeFile = os.Remove | 	state.removeFile = os.Remove | ||||||
| 	state.fileExists = func(path string) (bool, error) { | 	state.fileExists = c.fileExists | ||||||
| 		_, err := os.Stat(path) | 	state.glob = c.glob | ||||||
| 
 |  | ||||||
| 		if err != nil { |  | ||||||
| 			if os.IsNotExist(err) { |  | ||||||
| 				return false, nil |  | ||||||
| 			} |  | ||||||
| 			return false, err |  | ||||||
| 		} |  | ||||||
| 		return true, nil |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	return &state, nil | 	return &state, nil | ||||||
| } | } | ||||||
|  | @ -171,28 +164,17 @@ func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmSta | ||||||
| 	return layers[0], nil | 	return layers[0], nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (st *HelmState) ExpandPaths(patterns []string, glob func(string) ([]string, error)) ([]string, error) { | func (st *HelmState) ExpandPaths(globPattern string) ([]string, error) { | ||||||
| 	result := []string{} | 	result := []string{} | ||||||
| 	for _, globPattern := range patterns { | 	absPathPattern := st.normalizePath(globPattern) | ||||||
| 		var absPathPattern string | 	matches, err := st.glob(absPathPattern) | ||||||
| 		if filepath.IsAbs(globPattern) { |  | ||||||
| 			absPathPattern = globPattern |  | ||||||
| 		} else { |  | ||||||
| 			absPathPattern = st.JoinBase(globPattern) |  | ||||||
| 		} |  | ||||||
| 		matches, err := glob(absPathPattern) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) | 		return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		if len(matches) == 0 { |  | ||||||
| 			return nil, fmt.Errorf("no file matching %s found", globPattern) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 	sort.Strings(matches) | 	sort.Strings(matches) | ||||||
| 
 | 
 | ||||||
| 	result = append(result, matches...) | 	result = append(result, matches...) | ||||||
| 	} |  | ||||||
| 	return result, nil | 	return result, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -200,12 +182,20 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, | ||||||
| 	envVals := map[string]interface{}{} | 	envVals := map[string]interface{}{} | ||||||
| 	envSpec, ok := st.Environments[name] | 	envSpec, ok := st.Environments[name] | ||||||
| 	if ok { | 	if ok { | ||||||
| 		valuesFiles, err := st.ExpandPaths(envSpec.Values, glob) | 		var envValuesFiles []string | ||||||
|  | 		for _, urlOrPath := range envSpec.Values { | ||||||
|  | 			resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  | 			if skipped { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 		for _, envvalFullPath := range valuesFiles { | 			envValuesFiles = append(envValuesFiles, resolved...) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, envvalFullPath := range envValuesFiles { | ||||||
| 			tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""} | 			tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""} | ||||||
| 			r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData) | 			r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData) | ||||||
| 			bytes, err := r.RenderToBytes(envvalFullPath) | 			bytes, err := r.RenderToBytes(envvalFullPath) | ||||||
|  | @ -222,16 +212,22 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if len(envSpec.Secrets) > 0 { | 		if len(envSpec.Secrets) > 0 { | ||||||
| 			secretsFiles, err := st.ExpandPaths(envSpec.Secrets, glob) | 			helm := helmexec.New(st.logger, "") | ||||||
|  | 
 | ||||||
|  | 			var envSecretFiles []string | ||||||
|  | 			for _, urlOrPath := range envSpec.Secrets { | ||||||
|  | 				resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return nil, err | 					return nil, err | ||||||
| 				} | 				} | ||||||
| 
 | 				if skipped { | ||||||
| 			helm := helmexec.New(st.logger, "") | 					continue | ||||||
| 			for _, path := range secretsFiles { |  | ||||||
| 				if _, err := os.Stat(path); os.IsNotExist(err) { |  | ||||||
| 					return nil, err |  | ||||||
| 				} | 				} | ||||||
|  | 
 | ||||||
|  | 				envSecretFiles = append(envSecretFiles, resolved...) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			for _, path := range envSecretFiles { | ||||||
| 				// Work-around to allow decrypting environment secrets
 | 				// Work-around to allow decrypting environment secrets
 | ||||||
| 				//
 | 				//
 | ||||||
| 				// We don't have releases loaded yet and therefore unable to decide whether
 | 				// We don't have releases loaded yet and therefore unable to decide whether
 | ||||||
|  |  | ||||||
|  | @ -106,7 +106,7 @@ bar: {{ readFile "bar.txt" }} | ||||||
| 	}) | 	}) | ||||||
| 	testFs.Cwd = "/example/path/to" | 	testFs.Cwd = "/example/path/to" | ||||||
| 
 | 
 | ||||||
| 	state, err := NewCreator(logger, testFs.ReadFile, testFs.Abs, testFs.Glob).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil) | 	state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -3,4 +3,13 @@ package state | ||||||
| type EnvironmentSpec struct { | type EnvironmentSpec struct { | ||||||
| 	Values  []string `yaml:"values"` | 	Values  []string `yaml:"values"` | ||||||
| 	Secrets []string `yaml:"secrets"` | 	Secrets []string `yaml:"secrets"` | ||||||
|  | 
 | ||||||
|  | 	// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
 | ||||||
|  | 	// under `environments.NAME.values`.
 | ||||||
|  | 	//
 | ||||||
|  | 	// Possible values are  "Error", "Warn", "Info", "Debug". The default is "Error".
 | ||||||
|  | 	//
 | ||||||
|  | 	// Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
 | ||||||
|  | 	// a message about the missing file at the log-level.
 | ||||||
|  | 	MissingFileHandler *string `yaml:"missingFileHandler"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,9 +28,10 @@ import ( | ||||||
| // HelmState structure for the helmfile
 | // HelmState structure for the helmfile
 | ||||||
| type HelmState struct { | type HelmState struct { | ||||||
| 	basePath string | 	basePath string | ||||||
| 	Environments map[string]EnvironmentSpec |  | ||||||
| 	FilePath string | 	FilePath string | ||||||
| 
 | 
 | ||||||
|  | 	Environments map[string]EnvironmentSpec `yaml:"environments"` | ||||||
|  | 
 | ||||||
| 	Bases              []string          `yaml:"bases"` | 	Bases              []string          `yaml:"bases"` | ||||||
| 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | ||||||
| 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles"` | 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles"` | ||||||
|  | @ -51,6 +52,7 @@ type HelmState struct { | ||||||
| 
 | 
 | ||||||
| 	removeFile func(string) error | 	removeFile func(string) error | ||||||
| 	fileExists func(string) (bool, error) | 	fileExists func(string) (bool, error) | ||||||
|  | 	glob       func(string) ([]string, error) | ||||||
| 	tempDir    func(string, string) (string, error) | 	tempDir    func(string, string) (string, error) | ||||||
| 
 | 
 | ||||||
| 	runner helmexec.Runner | 	runner helmexec.Runner | ||||||
|  | @ -171,6 +173,11 @@ type AffectedReleases struct { | ||||||
| 
 | 
 | ||||||
| const DefaultEnv = "default" | const DefaultEnv = "default" | ||||||
| 
 | 
 | ||||||
|  | const MissingFileHandlerError = "Error" | ||||||
|  | const MissingFileHandlerInfo = "Info" | ||||||
|  | const MissingFileHandlerWarn = "Warn" | ||||||
|  | const MissingFileHandlerDebug = "Debug" | ||||||
|  | 
 | ||||||
| func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) { | func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) { | ||||||
| 	if st.Namespace != "" { | 	if st.Namespace != "" { | ||||||
| 		spec.Namespace = st.Namespace | 		spec.Namespace = st.Namespace | ||||||
|  | @ -1252,26 +1259,18 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF | ||||||
| 	for _, value := range values { | 	for _, value := range values { | ||||||
| 		switch typedValue := value.(type) { | 		switch typedValue := value.(type) { | ||||||
| 		case string: | 		case string: | ||||||
| 			path := st.normalizePath(typedValue) | 			paths, skip, err := st.resolveFile(missingFileHandler, "values", typedValue) | ||||||
| 
 |  | ||||||
| 			ok, err := st.fileExists(path) |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
| 			if !ok { | 			if skip { | ||||||
| 				if missingFileHandler == nil || *missingFileHandler == "Error" { |  | ||||||
| 					return nil, fmt.Errorf("file does not exist: %s", path) |  | ||||||
| 				} else if *missingFileHandler == "Warn" { |  | ||||||
| 					st.logger.Warnf("skipping missing values file \"%s\"", path) |  | ||||||
| 					continue |  | ||||||
| 				} else if *missingFileHandler == "Info" { |  | ||||||
| 					st.logger.Infof("skipping missing values file \"%s\"", path) |  | ||||||
| 					continue |  | ||||||
| 				} else { |  | ||||||
| 					st.logger.Debugf("skipping missing values file \"%s\"", path) |  | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			if len(paths) > 1 { | ||||||
|  | 				return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary") | ||||||
| 			} | 			} | ||||||
|  | 			path := paths[0] | ||||||
| 
 | 
 | ||||||
| 			yamlBytes, err := st.RenderValuesFileToBytes(path) | 			yamlBytes, err := st.RenderValuesFileToBytes(path) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -1337,25 +1336,18 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R | ||||||
| 	release.generatedValues = append(release.generatedValues, generatedFiles...) | 	release.generatedValues = append(release.generatedValues, generatedFiles...) | ||||||
| 
 | 
 | ||||||
| 	for _, value := range release.Secrets { | 	for _, value := range release.Secrets { | ||||||
| 		path := st.normalizePath(release.ValuesPathPrefix + value) | 		paths, skip, err := st.resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value) | ||||||
| 		ok, err := st.fileExists(path) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		if !ok { | 		if skip { | ||||||
| 			if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" { |  | ||||||
| 				return nil, err |  | ||||||
| 			} else if *release.MissingFileHandler == "Warn" { |  | ||||||
| 				st.logger.Warnf("skipping missing secrets file \"%s\"", path) |  | ||||||
| 				continue |  | ||||||
| 			} else if *release.MissingFileHandler == "Info" { |  | ||||||
| 				st.logger.Infof("skipping missing secrets file \"%s\"", path) |  | ||||||
| 				continue |  | ||||||
| 			} else { |  | ||||||
| 				st.logger.Debugf("skipping missing secrets file \"%s\"", path) |  | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(paths) > 1 { | ||||||
|  | 			return nil, fmt.Errorf("glob patterns in release secret file is not supported yet. please submit a feature request if necessary") | ||||||
| 		} | 		} | ||||||
|  | 		path := paths[0] | ||||||
| 
 | 
 | ||||||
| 		decryptFlags := st.appendTillerFlags([]string{}, release) | 		decryptFlags := st.appendTillerFlags([]string{}, release) | ||||||
| 		valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...) | 		valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...) | ||||||
|  | @ -1413,6 +1405,49 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R | ||||||
| 	return flags, nil | 	return flags, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (st *HelmState) resolveFile(missingFileHandler *string, tpe, path string) ([]string, bool, error) { | ||||||
|  | 	title := fmt.Sprintf("%s file", tpe) | ||||||
|  | 
 | ||||||
|  | 	files, err := st.ExpandPaths(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, false, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var handlerId string | ||||||
|  | 
 | ||||||
|  | 	if missingFileHandler != nil { | ||||||
|  | 		handlerId = *missingFileHandler | ||||||
|  | 	} else { | ||||||
|  | 		handlerId = MissingFileHandlerError | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(files) == 0 { | ||||||
|  | 		switch handlerId { | ||||||
|  | 		case MissingFileHandlerError: | ||||||
|  | 			return nil, false, fmt.Errorf("%s matching \"%s\" does not exist", title, path) | ||||||
|  | 		case MissingFileHandlerWarn: | ||||||
|  | 			st.logger.Warnf("skipping missing %s matching \"%s\"", title, path) | ||||||
|  | 			return nil, true, nil | ||||||
|  | 		case MissingFileHandlerInfo: | ||||||
|  | 			st.logger.Infof("skipping missing %s matching \"%s\"", title, path) | ||||||
|  | 			return nil, true, nil | ||||||
|  | 		case MissingFileHandlerDebug: | ||||||
|  | 			st.logger.Debugf("skipping missing %s matching \"%s\"", title, path) | ||||||
|  | 			return nil, true, nil | ||||||
|  | 		default: | ||||||
|  | 			available := []string{ | ||||||
|  | 				MissingFileHandlerError, | ||||||
|  | 				MissingFileHandlerWarn, | ||||||
|  | 				MissingFileHandlerInfo, | ||||||
|  | 				MissingFileHandlerDebug, | ||||||
|  | 			} | ||||||
|  | 			return nil, false, fmt.Errorf("invalid missing file handler \"%s\" while processing \"%s\" in \"%s\": it must be one of %s", handlerId, path, st.FilePath, available) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return files, false, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // DisplayAffectedReleases logs the upgraded, deleted and in error releases
 | // DisplayAffectedReleases logs the upgraded, deleted and in error releases
 | ||||||
| func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) { | func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) { | ||||||
| 	if ar.Upgraded != nil { | 	if ar.Upgraded != nil { | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import ( | ||||||
| var logger = helmexec.NewLogger(os.Stdout, "warn") | var logger = helmexec.NewLogger(os.Stdout, "warn") | ||||||
| 
 | 
 | ||||||
| func injectFs(st *HelmState, fs *TestFs) *HelmState { | func injectFs(st *HelmState, fs *TestFs) *HelmState { | ||||||
|  | 	st.glob = fs.Glob | ||||||
| 	st.readFile = fs.ReadFile | 	st.readFile = fs.ReadFile | ||||||
| 	st.fileExists = fs.FileExists | 	st.fileExists = fs.FileExists | ||||||
| 	return st | 	return st | ||||||
|  | @ -1000,7 +1001,7 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing. | ||||||
| 				Values: []interface{}{"noexistent.values.yaml"}, | 				Values: []interface{}{"noexistent.values.yaml"}, | ||||||
| 			}, | 			}, | ||||||
| 			listResult:    ``, | 			listResult:    ``, | ||||||
| 			expectedError: `failed processing release foo: file does not exist: noexistent.values.yaml`, | 			expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist`, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "should fail upgrading due to missing values file", | 			name: "should fail upgrading due to missing values file", | ||||||
|  | @ -1011,7 +1012,7 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing. | ||||||
| 			}, | 			}, | ||||||
| 			listResult: `NAME 	REVISION	UPDATED                 	STATUS  	CHART                      	APP VERSION	NAMESPACE | 			listResult: `NAME 	REVISION	UPDATED                 	STATUS  	CHART                      	APP VERSION	NAMESPACE | ||||||
| 										foo	1       	Wed Apr 17 17:39:04 2019	DEPLOYED	foo-bar-2.0.4	0.1.0      	default`, | 										foo	1       	Wed Apr 17 17:39:04 2019	DEPLOYED	foo-bar-2.0.4	0.1.0      	default`, | ||||||
| 			expectedError: `failed processing release foo: file does not exist: noexistent.values.yaml`, | 			expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist`, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "should uninstall even when there is a missing values file", | 			name: "should uninstall even when there is a missing values file", | ||||||
|  | @ -1427,22 +1428,15 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) { | ||||||
| 			state := &HelmState{ | 			state := &HelmState{ | ||||||
| 				Releases: tt.releases, | 				Releases: tt.releases, | ||||||
| 				logger:   logger, | 				logger:   logger, | ||||||
| 				readFile: func(f string) ([]byte, error) { |  | ||||||
| 					if f != "someFile" { |  | ||||||
| 						return nil, fmt.Errorf("unexpected file to read: %s", f) |  | ||||||
| 					} |  | ||||||
| 					someFileContent := []byte(`foo: bar |  | ||||||
| `) |  | ||||||
| 					return someFileContent, nil |  | ||||||
| 				}, |  | ||||||
| 				removeFile: func(f string) error { | 				removeFile: func(f string) error { | ||||||
| 					numRemovedFiles += 1 | 					numRemovedFiles += 1 | ||||||
| 					return nil | 					return nil | ||||||
| 				}, | 				}, | ||||||
| 				fileExists: func(f string) (bool, error) { |  | ||||||
| 					return true, nil |  | ||||||
| 				}, |  | ||||||
| 			} | 			} | ||||||
|  | 			testfs := NewTestFs(map[string]string{ | ||||||
|  | 				"/path/to/someFile": `foo: FOO`, | ||||||
|  | 			}) | ||||||
|  | 			state = injectFs(state, testfs) | ||||||
| 			if errs := state.SyncReleases(&AffectedReleases{}, tt.helm, []string{}, 1); errs != nil && len(errs) > 0 { | 			if errs := state.SyncReleases(&AffectedReleases{}, tt.helm, []string{}, 1); errs != nil && len(errs) > 0 { | ||||||
| 				t.Errorf("unexpected errors: %v", errs) | 				t.Errorf("unexpected errors: %v", errs) | ||||||
| 			} | 			} | ||||||
|  | @ -1517,22 +1511,16 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) { | ||||||
| 			state := &HelmState{ | 			state := &HelmState{ | ||||||
| 				Releases: tt.releases, | 				Releases: tt.releases, | ||||||
| 				logger:   logger, | 				logger:   logger, | ||||||
| 				readFile: func(f string) ([]byte, error) { |  | ||||||
| 					if f != "someFile" { |  | ||||||
| 						return nil, fmt.Errorf("unexpected file to read: %s", f) |  | ||||||
| 					} |  | ||||||
| 					someFileContent := []byte(`foo: bar |  | ||||||
| `) |  | ||||||
| 					return someFileContent, nil |  | ||||||
| 				}, |  | ||||||
| 				removeFile: func(f string) error { | 				removeFile: func(f string) error { | ||||||
| 					numRemovedFiles += 1 | 					numRemovedFiles += 1 | ||||||
| 					return nil | 					return nil | ||||||
| 				}, | 				}, | ||||||
| 				fileExists: func(f string) (bool, error) { |  | ||||||
| 					return true, nil |  | ||||||
| 				}, |  | ||||||
| 			} | 			} | ||||||
|  | 			testfs := NewTestFs(map[string]string{ | ||||||
|  | 				"/path/to/someFile": `foo: bar | ||||||
|  | `, | ||||||
|  | 			}) | ||||||
|  | 			state = injectFs(state, testfs) | ||||||
| 			if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false); errs != nil && len(errs) > 0 { | 			if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false); errs != nil && len(errs) > 0 { | ||||||
| 				t.Errorf("unexpected errors: %v", errs) | 				t.Errorf("unexpected errors: %v", errs) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue