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:
KUOKA Yusuke 2019-05-28 15:33:45 +09:00 committed by GitHub
parent 4a5996d083
commit a896f801ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 303 additions and 151 deletions

View File

@ -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

View File

@ -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
} }
@ -113,11 +115,12 @@ 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,
env: a.Env, fileExists: a.fileExists,
namespace: a.Namespace, env: a.Env,
logger: a.Logger, namespace: a.Namespace,
abs: a.abs, logger: a.Logger,
abs: a.abs,
Reverse: a.Reverse, Reverse: a.Reverse,
KubeContext: a.KubeContext, KubeContext: a.KubeContext,
@ -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()

View File

@ -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{
@ -746,16 +801,18 @@ helmDefaults:
`, `,
}) })
app := &App{ app := &App{
readFile: testFs.ReadFile, readFile: testFs.ReadFile,
glob: testFs.Glob, glob: testFs.Glob,
abs: testFs.Abs, abs: testFs.Abs,
KubeContext: "default", fileExistsAt: testFs.FileExistsAt,
Env: "default", fileExists: testFs.FileExists,
Logger: helmexec.NewLogger(os.Stderr, "debug"), KubeContext: "default",
Env: "default",
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" {
@ -825,11 +882,12 @@ helmDefaults:
`, `,
}) })
app := &App{ app := &App{
readFile: testFs.ReadFile, readFile: testFs.ReadFile,
glob: testFs.Glob, fileExists: testFs.FileExists,
abs: testFs.Abs, glob: testFs.Glob,
Env: "default", abs: testFs.Abs,
Logger: helmexec.NewLogger(os.Stderr, "debug"), Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
st, err := app.loadDesiredStateFromYaml(yamlFile) st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil { if err != nil {
@ -900,11 +958,12 @@ foo: FOO
`, `,
}) })
app := &App{ app := &App{
readFile: testFs.ReadFile, readFile: testFs.ReadFile,
glob: testFs.Glob, fileExists: testFs.FileExists,
abs: testFs.Abs, glob: testFs.Glob,
Env: "default", abs: testFs.Abs,
Logger: helmexec.NewLogger(os.Stderr, "debug"), Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
st, err := app.loadDesiredStateFromYaml(yamlFile) st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil { if err != nil {
@ -978,11 +1037,12 @@ helmDefaults:
`, `,
}) })
app := &App{ app := &App{
readFile: testFs.ReadFile, readFile: testFs.ReadFile,
glob: testFs.Glob, fileExists: testFs.FileExists,
abs: testFs.Abs, glob: testFs.Glob,
Env: "test", abs: testFs.Abs,
Logger: helmexec.NewLogger(os.Stderr, "debug"), Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
st, err := app.loadDesiredStateFromYaml(yamlFile) st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil { if err != nil {

View File

@ -19,9 +19,10 @@ type desiredStateLoader struct {
env string env string
namespace string namespace string
readFile func(string) ([]byte, error) readFile func(string) ([]byte, error)
abs func(string) (string, error) fileExists func(string) (bool, error)
glob func(string) ([]string, error) abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger logger *zap.SugaredLogger
} }
@ -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

View File

@ -13,12 +13,13 @@ import (
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) { func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) {
testfs := state.NewTestFs(files) testfs := state.NewTestFs(files)
return &desiredStateLoader{ return &desiredStateLoader{
env: env, env: env,
namespace: "namespace", namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"), logger: helmexec.NewLogger(os.Stdout, "debug"),
readFile: testfs.ReadFile, readFile: testfs.ReadFile,
abs: testfs.Abs, fileExists: testfs.FileExists,
glob: testfs.Glob, abs: testfs.Abs,
glob: testfs.Glob,
}, testfs }, testfs
} }

View File

@ -35,23 +35,25 @@ 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)
abs func(string) (string, error) fileExists func(string) (bool, error)
glob func(string) ([]string, error) abs func(string) (string, error)
glob func(string) ([]string, error)
Strict bool Strict bool
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,
abs: abs, fileExists: fileExists,
glob: glob, abs: abs,
Strict: true, glob: glob,
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) { if err != nil {
absPathPattern = globPattern return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
} 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...)
} }
sort.Strings(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
if err != nil { for _, urlOrPath := range envSpec.Values {
return nil, err 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: ""} 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)
if err != nil {
return nil, err
}
helm := helmexec.New(st.logger, "") 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 return nil, err
} }
if skipped {
continue
}
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

View File

@ -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)
} }

View File

@ -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"`
} }

View File

@ -27,9 +27,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"`
@ -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,27 +1259,19 @@ 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" { continue
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 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 {
return nil, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err) 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...) 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" { continue
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 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...)
if err != nil { if err != nil {
@ -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 {

View File

@ -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)
} }