diff --git a/README.md b/README.md index e401650f..2d3946c4 100644 --- a/README.md +++ b/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 diff --git a/pkg/app/app.go b/pkg/app/app.go index ad5196fd..776c3094 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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() diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 97e48bc4..7224b033 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -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 { diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index b82310fb..40eac2ec 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -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 diff --git a/pkg/app/two_pass_renderer_test.go b/pkg/app/two_pass_renderer_test.go index bf9656d8..832d01bb 100644 --- a/pkg/app/two_pass_renderer_test.go +++ b/pkg/app/two_pass_renderer_test.go @@ -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 } diff --git a/state/create.go b/state/create.go index 7fec9f09..bf697d05 100644 --- a/state/create.go +++ b/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 diff --git a/state/create_test.go b/state/create_test.go index 3850a42d..9331b24b 100644 --- a/state/create_test.go +++ b/state/create_test.go @@ -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) } diff --git a/state/environment.go b/state/environment.go index 6833c9f9..3bc2c451 100644 --- a/state/environment.go +++ b/state/environment.go @@ -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"` } diff --git a/state/state.go b/state/state.go index b95ddd4a..ba1ce682 100644 --- a/state/state.go +++ b/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 { diff --git a/state/state_test.go b/state/state_test.go index 8715202b..8faa348d 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -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) }