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`:
|
||||
|
||||
```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:
|
||||
- name: roboll
|
||||
url: http://roboll.io/charts
|
||||
|
|
@ -63,6 +67,9 @@ helmDefaults:
|
|||
# path to TLS key file (default "$HELM_HOME/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:
|
||||
# Published chart example
|
||||
- name: vault # name of this release
|
||||
|
|
@ -71,7 +78,7 @@ releases:
|
|||
foo: bar
|
||||
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
|
||||
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:
|
||||
# value files passed via --values
|
||||
- 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.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type App struct {
|
|||
Selectors []string
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
fileExists func(string) (bool, error)
|
||||
glob func(string) ([]string, error)
|
||||
abs func(string) (string, error)
|
||||
fileExistsAt func(string) bool
|
||||
|
|
@ -42,6 +43,7 @@ func Init(app *App) *App {
|
|||
app.getwd = os.Getwd
|
||||
app.chdir = os.Chdir
|
||||
app.fileExistsAt = fileExistsAt
|
||||
app.fileExists = fileExists
|
||||
app.directoryExistsAt = directoryExistsAt
|
||||
return app
|
||||
}
|
||||
|
|
@ -113,11 +115,12 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
|||
|
||||
func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) {
|
||||
ld := &desiredStateLoader{
|
||||
readFile: a.readFile,
|
||||
env: a.Env,
|
||||
namespace: a.Namespace,
|
||||
logger: a.Logger,
|
||||
abs: a.abs,
|
||||
readFile: a.readFile,
|
||||
fileExists: a.fileExists,
|
||||
env: a.Env,
|
||||
namespace: a.Namespace,
|
||||
logger: a.Logger,
|
||||
abs: a.abs,
|
||||
|
||||
Reverse: a.Reverse,
|
||||
KubeContext: a.KubeContext,
|
||||
|
|
@ -316,6 +319,18 @@ func fileExistsAt(path string) bool {
|
|||
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 {
|
||||
fileInfo, err := os.Stat(path)
|
||||
return err == nil && fileInfo.Mode().IsDir()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func injectFs(app *App, fs *state.TestFs) *App {
|
|||
app.getwd = fs.Getwd
|
||||
app.chdir = fs.Chdir
|
||||
app.fileExistsAt = fs.FileExistsAt
|
||||
app.fileExists = fs.FileExists
|
||||
app.directoryExistsAt = fs.DirectoryExistsAt
|
||||
return app
|
||||
}
|
||||
|
|
@ -155,12 +156,66 @@ releases:
|
|||
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 {
|
||||
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
|
||||
func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) {
|
||||
files := map[string]string{
|
||||
|
|
@ -746,16 +801,18 @@ helmDefaults:
|
|||
`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
fileExistsAt: testFs.FileExistsAt,
|
||||
fileExists: testFs.FileExists,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(yamlFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
|
||||
|
|
@ -825,11 +882,12 @@ helmDefaults:
|
|||
`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
readFile: testFs.ReadFile,
|
||||
fileExists: testFs.FileExists,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(yamlFile)
|
||||
if err != nil {
|
||||
|
|
@ -900,11 +958,12 @@ foo: FOO
|
|||
`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
readFile: testFs.ReadFile,
|
||||
fileExists: testFs.FileExists,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(yamlFile)
|
||||
if err != nil {
|
||||
|
|
@ -978,11 +1037,12 @@ helmDefaults:
|
|||
`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "test",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
readFile: testFs.ReadFile,
|
||||
fileExists: testFs.FileExists,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "test",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(yamlFile)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ type desiredStateLoader struct {
|
|||
env string
|
||||
namespace string
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
readFile func(string) ([]byte, error)
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
|
@ -96,7 +97,7 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba
|
|||
}
|
||||
|
||||
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
|
||||
return c
|
||||
}
|
||||
|
|
@ -109,10 +110,13 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas
|
|||
|
||||
helmfiles := []state.SubHelmfileSpec{}
|
||||
for _, hf := range st.Helmfiles {
|
||||
matches, err := st.ExpandPaths([]string{hf.Path}, a.glob)
|
||||
matches, err := st.ExpandPaths(hf.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("no file matching %s found", hf.Path)
|
||||
}
|
||||
for _, match := range matches {
|
||||
newHelmfile := hf
|
||||
newHelmfile.Path = match
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ import (
|
|||
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) {
|
||||
testfs := state.NewTestFs(files)
|
||||
return &desiredStateLoader{
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
logger: helmexec.NewLogger(os.Stdout, "debug"),
|
||||
readFile: testfs.ReadFile,
|
||||
abs: testfs.Abs,
|
||||
glob: testfs.Glob,
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
logger: helmexec.NewLogger(os.Stdout, "debug"),
|
||||
readFile: testfs.ReadFile,
|
||||
fileExists: testfs.FileExists,
|
||||
abs: testfs.Abs,
|
||||
glob: testfs.Glob,
|
||||
}, testfs
|
||||
}
|
||||
|
||||
|
|
|
|||
100
state/create.go
100
state/create.go
|
|
@ -35,23 +35,25 @@ func (e *UndefinedEnvError) Error() string {
|
|||
}
|
||||
|
||||
type StateCreator struct {
|
||||
logger *zap.SugaredLogger
|
||||
readFile func(string) ([]byte, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
logger *zap.SugaredLogger
|
||||
readFile func(string) ([]byte, error)
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
|
||||
Strict bool
|
||||
|
||||
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{
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
fileExists: fileExists,
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,17 +104,8 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
|
|||
|
||||
state.readFile = c.readFile
|
||||
state.removeFile = os.Remove
|
||||
state.fileExists = func(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
state.fileExists = c.fileExists
|
||||
state.glob = c.glob
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
|
@ -171,28 +164,17 @@ func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmSta
|
|||
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{}
|
||||
for _, globPattern := range patterns {
|
||||
var absPathPattern string
|
||||
if filepath.IsAbs(globPattern) {
|
||||
absPathPattern = globPattern
|
||||
} else {
|
||||
absPathPattern = st.JoinBase(globPattern)
|
||||
}
|
||||
matches, err := glob(absPathPattern)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
result = append(result, matches...)
|
||||
absPathPattern := st.normalizePath(globPattern)
|
||||
matches, err := st.glob(absPathPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
|
||||
}
|
||||
|
||||
sort.Strings(matches)
|
||||
|
||||
result = append(result, matches...)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
|
@ -200,12 +182,20 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
|||
envVals := map[string]interface{}{}
|
||||
envSpec, ok := st.Environments[name]
|
||||
if ok {
|
||||
valuesFiles, err := st.ExpandPaths(envSpec.Values, glob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var envValuesFiles []string
|
||||
for _, urlOrPath := range envSpec.Values {
|
||||
resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
envValuesFiles = append(envValuesFiles, resolved...)
|
||||
}
|
||||
|
||||
for _, envvalFullPath := range valuesFiles {
|
||||
for _, envvalFullPath := range envValuesFiles {
|
||||
tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
|
||||
r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData)
|
||||
bytes, err := r.RenderToBytes(envvalFullPath)
|
||||
|
|
@ -222,16 +212,22 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
|||
}
|
||||
|
||||
if len(envSpec.Secrets) > 0 {
|
||||
secretsFiles, err := st.ExpandPaths(envSpec.Secrets, glob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helm := helmexec.New(st.logger, "")
|
||||
for _, path := range secretsFiles {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
|
||||
var envSecretFiles []string
|
||||
for _, urlOrPath := range envSpec.Secrets {
|
||||
resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
envSecretFiles = append(envSecretFiles, resolved...)
|
||||
}
|
||||
|
||||
for _, path := range envSecretFiles {
|
||||
// Work-around to allow decrypting environment secrets
|
||||
//
|
||||
// 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"
|
||||
|
||||
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 {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,13 @@ package state
|
|||
type EnvironmentSpec struct {
|
||||
Values []string `yaml:"values"`
|
||||
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"`
|
||||
}
|
||||
|
|
|
|||
103
state/state.go
103
state/state.go
|
|
@ -27,9 +27,10 @@ import (
|
|||
|
||||
// HelmState structure for the helmfile
|
||||
type HelmState struct {
|
||||
basePath string
|
||||
Environments map[string]EnvironmentSpec
|
||||
FilePath string
|
||||
basePath string
|
||||
FilePath string
|
||||
|
||||
Environments map[string]EnvironmentSpec `yaml:"environments"`
|
||||
|
||||
Bases []string `yaml:"bases"`
|
||||
HelmDefaults HelmSpec `yaml:"helmDefaults"`
|
||||
|
|
@ -51,6 +52,7 @@ type HelmState struct {
|
|||
|
||||
removeFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
glob func(string) ([]string, error)
|
||||
tempDir func(string, string) (string, error)
|
||||
|
||||
runner helmexec.Runner
|
||||
|
|
@ -171,6 +173,11 @@ type AffectedReleases struct {
|
|||
|
||||
const DefaultEnv = "default"
|
||||
|
||||
const MissingFileHandlerError = "Error"
|
||||
const MissingFileHandlerInfo = "Info"
|
||||
const MissingFileHandlerWarn = "Warn"
|
||||
const MissingFileHandlerDebug = "Debug"
|
||||
|
||||
func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
|
||||
if st.Namespace != "" {
|
||||
spec.Namespace = st.Namespace
|
||||
|
|
@ -1252,27 +1259,19 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
|
|||
for _, value := range values {
|
||||
switch typedValue := value.(type) {
|
||||
case string:
|
||||
path := st.normalizePath(typedValue)
|
||||
|
||||
ok, err := st.fileExists(path)
|
||||
paths, skip, err := st.resolveFile(missingFileHandler, "values", typedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
if skip {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
|
||||
|
|
@ -1337,26 +1336,19 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
|
|||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
|
||||
for _, value := range release.Secrets {
|
||||
path := st.normalizePath(release.ValuesPathPrefix + value)
|
||||
ok, err := st.fileExists(path)
|
||||
paths, skip, err := st.resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
if skip {
|
||||
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)
|
||||
valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
|
||||
if err != nil {
|
||||
|
|
@ -1413,6 +1405,49 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
|
|||
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
|
||||
func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
|
||||
if ar.Upgraded != nil {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
var logger = helmexec.NewLogger(os.Stdout, "warn")
|
||||
|
||||
func injectFs(st *HelmState, fs *TestFs) *HelmState {
|
||||
st.glob = fs.Glob
|
||||
st.readFile = fs.ReadFile
|
||||
st.fileExists = fs.FileExists
|
||||
return st
|
||||
|
|
@ -1000,7 +1001,7 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.
|
|||
Values: []interface{}{"noexistent.values.yaml"},
|
||||
},
|
||||
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",
|
||||
|
|
@ -1011,7 +1012,7 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.
|
|||
},
|
||||
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`,
|
||||
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",
|
||||
|
|
@ -1427,22 +1428,15 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
|
|||
state := &HelmState{
|
||||
Releases: tt.releases,
|
||||
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 {
|
||||
numRemovedFiles += 1
|
||||
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 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
|
@ -1517,22 +1511,16 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
|
|||
state := &HelmState{
|
||||
Releases: tt.releases,
|
||||
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 {
|
||||
numRemovedFiles += 1
|
||||
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 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue