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
10 changed files with 303 additions and 151 deletions
+48 -52
View File
@@ -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
+1 -1
View File
@@ -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)
}
+9
View File
@@ -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"`
}
+69 -34
View File
@@ -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 {
+12 -24
View File
@@ -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)
}