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
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
100
state/create.go
100
state/create.go
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
state/state.go
103
state/state.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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