diff --git a/app_test.go b/app_test.go index 1e6c44a2..3869ab80 100644 --- a/app_test.go +++ b/app_test.go @@ -8,20 +8,122 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" ) +type testFs struct { + wd string + dirs map[string]bool + files map[string]string +} + +func appWithFs(app *app, files map[string]string) *app { + fs := newTestFs(files) + return injectFs(app, fs) +} + +func injectFs(app *app, fs *testFs) *app { + app.readFile = fs.readFile + app.glob = fs.glob + app.abs = fs.abs + app.getwd = fs.getwd + app.chdir = fs.chdir + app.fileExistsAt = fs.fileExistsAt + app.directoryExistsAt = fs.directoryExistsAt + return app +} + +func newTestFs(files map[string]string) *testFs { + dirs := map[string]bool{} + for abs, _ := range files { + d := filepath.Dir(abs) + dirs[d] = true + } + return &testFs{ + wd: "/path/to", + dirs: dirs, + files: files, + } +} + +func (f *testFs) fileExistsAt(path string) bool { + var ok bool + if strings.Contains(path, "/") { + _, ok = f.files[path] + } else { + _, ok = f.files[filepath.Join(f.wd, path)] + } + return ok +} + +func (f *testFs) directoryExistsAt(path string) bool { + var ok bool + if strings.Contains(path, "/") { + _, ok = f.dirs[path] + } else { + _, ok = f.dirs[filepath.Join(f.wd, path)] + } + return ok +} + +func (f *testFs) readFile(filename string) ([]byte, error) { + var str string + var ok bool + if strings.Contains(filename, "/") { + str, ok = f.files[filename] + } else { + str, ok = f.files[filepath.Join(f.wd, filename)] + } + if !ok { + return []byte(nil), fmt.Errorf("no file found: %s", filename) + } + return []byte(str), nil +} + +func (f *testFs) glob(pattern string) ([]string, error) { + matches := []string{} + for name, _ := range f.files { + matched, err := filepath.Match(pattern, name) + if err != nil { + return nil, err + } + if matched { + matches = append(matches, name) + } + } + if len(matches) == 0 { + return []string(nil), fmt.Errorf("no file matched: %s", pattern) + } + return matches, nil +} + +func (f *testFs) abs(path string) (string, error) { + var p string + if path[0] == '/' { + p = path + } else { + p = filepath.Join(f.wd, path) + } + return filepath.Clean(p), nil +} + +func (f *testFs) getwd() (string, error) { + return f.wd, nil +} + +func (f *testFs) chdir(dir string) error { + if dir == "/path/to" || dir == "/path/to/helmfile.d" { + f.wd = dir + return nil + } + return fmt.Errorf("unexpected chdir \"%s\"", dir) +} + // See https://github.com/roboll/helmfile/issues/193 func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) { - absPaths := map[string]string{ - ".": "/path/to", - "/path/to/helmfile.d": "/path/to/helmfile.d", - } - dirs := map[string]bool{ - "helmfile.d": true, - } files := map[string]string{ - "helmfile.yaml": ` + "/path/to/helmfile.yaml": ` helmfiles: - helmfile.d/a*.yaml - helmfile.d/b*.yaml @@ -42,39 +144,6 @@ releases: chart: stable/grafana `, } - globMatches := map[string][]string{ - "/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"}, - "/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"}, - } - fileExistsAt := func(path string) bool { - _, ok := files[path] - return ok - } - directoryExistsAt := func(path string) bool { - _, ok := dirs[path] - return ok - } - readFile := func(filename string) ([]byte, error) { - str, ok := files[filename] - if !ok { - return []byte(nil), fmt.Errorf("no file found: %s", filename) - } - return []byte(str), nil - } - glob := func(pattern string) ([]string, error) { - matches, ok := globMatches[pattern] - if !ok { - return []string(nil), fmt.Errorf("no file matched: %s", pattern) - } - return matches, nil - } - abs := func(path string) (string, error) { - a, ok := absPaths[path] - if !ok { - return "", fmt.Errorf("abs: unexpected path: %s", path) - } - return a, nil - } noop := func(st *state.HelmState, helm helmexec.Interface) []error { return []error{} } @@ -90,18 +159,13 @@ releases: } for _, testcase := range testcases { - app := &app{ - readFile: readFile, - glob: glob, - abs: abs, - fileExistsAt: fileExistsAt, - directoryExistsAt: directoryExistsAt, - kubeContext: "default", - logger: helmexec.NewLogger(os.Stderr, "debug"), - selectors: []string{fmt.Sprintf("name=%s", testcase.name)}, - namespace: "", - env: "default", - } + app := appWithFs(&app{ + kubeContext: "default", + logger: helmexec.NewLogger(os.Stderr, "debug"), + selectors: []string{fmt.Sprintf("name=%s", testcase.name)}, + namespace: "", + env: "default", + }, files) err := app.VisitDesiredStatesWithReleasesFiltered( "helmfile.yaml", noop, ) @@ -115,15 +179,8 @@ releases: // See https://github.com/roboll/helmfile/issues/320 func TestVisitDesiredStatesWithReleasesFiltered_UndefinedEnv(t *testing.T) { - absPaths := map[string]string{ - ".": "/path/to", - "/path/to/helmfile.d": "/path/to/helmfile.d", - } - dirs := map[string]bool{ - "helmfile.d": true, - } files := map[string]string{ - "helmfile.yaml": ` + "/path/to/helmfile.yaml": ` environments: prod: @@ -139,38 +196,6 @@ releases: chart: stable/zipkin `, } - globMatches := map[string][]string{ - "/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml"}, - } - fileExistsAt := func(path string) bool { - _, ok := files[path] - return ok - } - directoryExistsAt := func(path string) bool { - _, ok := dirs[path] - return ok - } - readFile := func(filename string) ([]byte, error) { - str, ok := files[filename] - if !ok { - return []byte(nil), fmt.Errorf("no file found: %s", filename) - } - return []byte(str), nil - } - glob := func(pattern string) ([]string, error) { - matches, ok := globMatches[pattern] - if !ok { - return []string(nil), fmt.Errorf("no file matched: %s", pattern) - } - return matches, nil - } - abs := func(path string) (string, error) { - a, ok := absPaths[path] - if !ok { - return "", fmt.Errorf("abs: unexpected path: %s", path) - } - return a, nil - } noop := func(st *state.HelmState, helm helmexec.Interface) []error { return []error{} } @@ -185,18 +210,13 @@ releases: } for _, testcase := range testcases { - app := &app{ - readFile: readFile, - glob: glob, - abs: abs, - fileExistsAt: fileExistsAt, - directoryExistsAt: directoryExistsAt, - kubeContext: "default", - logger: helmexec.NewLogger(os.Stderr, "debug"), - namespace: "", - selectors: []string{}, - env: testcase.name, - } + app := appWithFs(&app{ + kubeContext: "default", + logger: helmexec.NewLogger(os.Stderr, "debug"), + namespace: "", + selectors: []string{}, + env: testcase.name, + }, files) err := app.VisitDesiredStatesWithReleasesFiltered( "helmfile.yaml", noop, ) @@ -210,15 +230,8 @@ releases: // See https://github.com/roboll/helmfile/issues/322 func TestVisitDesiredStatesWithReleasesFiltered_Selectors(t *testing.T) { - absPaths := map[string]string{ - ".": "/path/to", - "/path/to/helmfile.d": "/path/to/helmfile.d", - } - dirs := map[string]bool{ - "helmfile.d": true, - } files := map[string]string{ - "helmfile.yaml": ` + "/path/to/helmfile.yaml": ` helmfiles: - helmfile.d/a*.yaml - helmfile.d/b*.yaml @@ -247,39 +260,6 @@ releases: duplicated: yes `, } - globMatches := map[string][]string{ - "/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"}, - "/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"}, - } - fileExistsAt := func(path string) bool { - _, ok := files[path] - return ok - } - directoryExistsAt := func(path string) bool { - _, ok := dirs[path] - return ok - } - readFile := func(filename string) ([]byte, error) { - str, ok := files[filename] - if !ok { - return []byte(nil), fmt.Errorf("no file found: %s", filename) - } - return []byte(str), nil - } - glob := func(pattern string) ([]string, error) { - matches, ok := globMatches[pattern] - if !ok { - return []string(nil), fmt.Errorf("no file matched: %s", pattern) - } - return matches, nil - } - abs := func(path string) (string, error) { - a, ok := absPaths[path] - if !ok { - return "", fmt.Errorf("abs: unexpected path: %s", path) - } - return a, nil - } testcases := []struct { label string @@ -305,18 +285,13 @@ releases: return []error{} } - app := &app{ - readFile: readFile, - glob: glob, - abs: abs, - fileExistsAt: fileExistsAt, - directoryExistsAt: directoryExistsAt, - kubeContext: "default", - logger: helmexec.NewLogger(os.Stderr, "debug"), - namespace: "", - selectors: []string{testcase.label}, - env: "default", - } + app := appWithFs(&app{ + kubeContext: "default", + logger: helmexec.NewLogger(os.Stderr, "debug"), + namespace: "", + selectors: []string{testcase.label}, + env: "default", + }, files) err := app.VisitDesiredStatesWithReleasesFiltered( "helmfile.yaml", collectReleases, @@ -338,15 +313,8 @@ releases: // See https://github.com/roboll/helmfile/issues/312 func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) { - absPaths := map[string]string{ - ".": "/path/to", - "/path/to/helmfile.d": "/path/to/helmfile.d", - } - dirs := map[string]bool{ - "helmfile.d": true, - } files := map[string]string{ - "helmfile.yaml": ` + "/path/to/helmfile.yaml": ` helmfiles: - helmfile.d/a*.yaml - helmfile.d/b*.yaml @@ -369,39 +337,6 @@ releases: chart: stable/grafana `, } - globMatches := map[string][]string{ - "/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"}, - "/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"}, - } - fileExistsAt := func(path string) bool { - _, ok := files[path] - return ok - } - directoryExistsAt := func(path string) bool { - _, ok := dirs[path] - return ok - } - readFile := func(filename string) ([]byte, error) { - str, ok := files[filename] - if !ok { - return []byte(nil), fmt.Errorf("no file found: %s", filename) - } - return []byte(str), nil - } - glob := func(pattern string) ([]string, error) { - matches, ok := globMatches[pattern] - if !ok { - return []string(nil), fmt.Errorf("no file matched: %s", pattern) - } - return matches, nil - } - abs := func(path string) (string, error) { - a, ok := absPaths[path] - if !ok { - return "", fmt.Errorf("abs: unexpected path: %s", path) - } - return a, nil - } expected := []string{"grafana", "elasticsearch", "prometheus", "zipkin"} @@ -421,20 +356,14 @@ releases: } return []error{} } - - app := &app{ - readFile: readFile, - glob: glob, - abs: abs, - fileExistsAt: fileExistsAt, - directoryExistsAt: directoryExistsAt, - kubeContext: "default", - logger: helmexec.NewLogger(os.Stderr, "debug"), - reverse: testcase.reverse, - namespace: "", - selectors: []string{}, - env: "default", - } + app := appWithFs(&app{ + kubeContext: "default", + logger: helmexec.NewLogger(os.Stderr, "debug"), + reverse: testcase.reverse, + namespace: "", + selectors: []string{}, + env: "default", + }, files) err := app.VisitDesiredStatesWithReleasesFiltered( "helmfile.yaml", collectReleases, ) diff --git a/main.go b/main.go index cb987e31..76a9180d 100644 --- a/main.go +++ b/main.go @@ -571,6 +571,9 @@ type app struct { env string namespace string selectors []string + + getwd func() (string, error) + chdir func(string) error } func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, context) []error) error { @@ -598,6 +601,8 @@ func initAppEntry(c *cli.Context, reverse bool) (*app, string, error) { readFile: ioutil.ReadFile, glob: filepath.Glob, abs: filepath.Abs, + getwd: os.Getwd, + chdir: os.Chdir, fileExistsAt: fileExistsAt, directoryExistsAt: directoryExistsAt, kubeContext: kubeContext, @@ -778,16 +783,69 @@ func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) return yamlBuf, nil } -func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { +func (a *app) within(dir string, do func() error) error { + prev, err := a.getwd() + if err != nil { + return fmt.Errorf("failed getting current working direcotyr: %v", err) + } + + absDir, err := a.abs(dir) + if err != nil { + return err + } + + a.logger.Debugf("changing working directory to \"%s\"", absDir) + + if err := a.chdir(absDir); err != nil { + return fmt.Errorf("failed changing working directory to \"%s\": %v", absDir, err) + } + + appErr := do() + + a.logger.Debugf("changing working directory back to \"%s\"", prev) + + if chdirBackErr := a.chdir(prev); chdirBackErr != nil { + if appErr != nil { + a.logger.Warnf("%v", appErr) + } + return fmt.Errorf("failed chaging working directory back to \"%s\": %v", prev, chdirBackErr) + } + + return appErr +} + +func (a *app) visitStateFiles(fileOrDir string, do func(string) error) error { desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir) if err != nil { return err } - noMatchInHelmfiles := true - for _, f := range desiredStateFiles { - a.logger.Debugf("Processing %s", f) + for _, relPath := range desiredStateFiles { + a.logger.Debugf("Processing %s", relPath) + var file string + var dir string + if a.directoryExistsAt(fileOrDir) { + file = fileOrDir + dir = fileOrDir + } else { + file = filepath.Base(fileOrDir) + dir = filepath.Dir(fileOrDir) + } + err := a.within(dir, func() error { + return do(file) + }) + if err != nil { + return err + } + } + + return nil +} + +func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { + noMatchInHelmfiles := true + err := a.visitStateFiles(fileOrDir, func(f string) error { content, err := a.readFile(f) if err != nil { return err @@ -821,7 +879,7 @@ func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat case *state.StateLoadError: switch stateLoadErr.Cause.(type) { case *state.UndefinedEnvError: - continue + return nil default: return err } @@ -859,9 +917,10 @@ func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat noMatchInHelmfiles = noMatchInHelmfiles && !processed } - if err := clean(st, errs); err != nil { - return err - } + return clean(st, errs) + }) + if err != nil { + return err } if noMatchInHelmfiles { return &noMatchingHelmfileError{selectors: a.selectors, env: a.env} @@ -902,6 +961,22 @@ func (a *app) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge return nil } +func (a *app) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) { + rels, err := a.findDesiredStateFiles(specifiedPath) + if err != nil { + return rels, err + } + + files := make([]string, len(rels)) + for i := range rels { + files[i], err = filepath.Abs(rels[i]) + if err != nil { + return []string{}, err + } + } + return files, nil +} + func (a *app) findDesiredStateFiles(specifiedPath string) ([]string, error) { var helmfileDir string if specifiedPath != "" {