diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 2dff049c..3f90be0c 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "reflect" - "strings" "testing" "github.com/roboll/helmfile/helmexec" @@ -13,119 +12,153 @@ import ( "gotest.tools/env" ) -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) + fs := state.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 +func injectFs(app *App, fs *state.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 +func TestVisitDesiredStatesWithReleasesFiltered_ReleaseOrder(t *testing.T) { + files := map[string]string{ + "/path/to/helmfile.yaml": ` +helmfiles: +- helmfile.d/a*.yaml +- helmfile.d/b*.yaml +`, + "/path/to/helmfile.d/a1.yaml": ` +releases: +- name: zipkin + chart: stable/zipkin +`, + "/path/to/helmfile.d/a2.yaml": ` +releases: +- name: prometheus + chart: stable/prometheus +`, + "/path/to/helmfile.d/b.yaml": ` +releases: +- name: grafana + chart: stable/grafana +`, } - return &testFs{ - wd: "/path/to", - dirs: dirs, - files: files, + fs := state.NewTestFs(files) + fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"} + app := &App{ + KubeContext: "default", + Logger: helmexec.NewLogger(os.Stderr, "debug"), + Namespace: "", + Env: "default", + } + app = injectFs(app, fs) + actualOrder := []string{} + noop := func(st *state.HelmState, helm helmexec.Interface) []error { + actualOrder = append(actualOrder, st.FilePath) + return []error{} + } + + err := app.VisitDesiredStatesWithReleasesFiltered( + "helmfile.yaml", noop, + ) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedOrder := []string{"a1.yaml", "a2.yaml", "b.yaml", "helmfile.yaml"} + if !reflect.DeepEqual(actualOrder, expectedOrder) { + t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder) } } -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)] +func TestVisitDesiredStatesWithReleasesFiltered_EnvValuesFileOrder(t *testing.T) { + files := map[string]string{ + "/path/to/helmfile.yaml": ` +environments: + default: + values: + - env.*.yaml +releases: +- name: zipkin + chart: stable/zipkin +`, + "/path/to/env.1.yaml": `FOO: 1 +BAR: 2 +`, + "/path/to/env.2.yaml": `BAR: 3 +BAZ: 4 +`, + } + fs := state.NewTestFs(files) + fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"} + 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 err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedOrder := []string{"helmfile.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml"} + actualOrder := fs.SuccessfulReads() + if !reflect.DeepEqual(actualOrder, expectedOrder) { + t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder) } - 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)] +func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFile(t *testing.T) { + files := map[string]string{ + "/path/to/helmfile.yaml": ` +environments: + default: + values: + - env.*.yaml +releases: +- name: zipkin + chart: stable/zipkin +`, } - 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)] + fs := state.NewTestFs(files) + app := &App{ + KubeContext: "default", + Logger: helmexec.NewLogger(os.Stderr, "debug"), + Namespace: "", + Env: "default", } - if !ok { - return []byte(nil), fmt.Errorf("no file found: %s", filename) - } - return []byte(str), nil -} - -func (f *testFs) glob(relPattern string) ([]string, error) { - var pattern string - if relPattern[0] == '/' { - pattern = relPattern - } else { - pattern = filepath.Join(f.wd, relPattern) + app = injectFs(app, fs) + noop := func(st *state.HelmState, helm helmexec.Interface) []error { + return []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) - } + err := app.VisitDesiredStatesWithReleasesFiltered( + "helmfile.yaml", noop, + ) + if err == nil { + t.Fatal("expected error did not occur") } - if len(matches) == 0 { - return []string(nil), fmt.Errorf("no file matched %s for files: %v", pattern, f.files) - } - 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) + expected := "in ./helmfile.yaml: failed to read helmfile.yaml: no file matching env.*.yaml found" + if err.Error() != expected { + t.Errorf("unexpected error: expected=%s, got=%v", expected, err) } - 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 @@ -152,10 +185,6 @@ releases: chart: stable/grafana `, } - noop := func(st *state.HelmState, helm helmexec.Interface) []error { - return []error{} - } - testcases := []struct { name string expectErr bool @@ -167,13 +196,20 @@ releases: } for _, testcase := range testcases { - app := appWithFs(&App{ + fs := state.NewTestFs(files) + fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"} + app := &App{ KubeContext: "default", Logger: helmexec.NewLogger(os.Stderr, "debug"), Selectors: []string{fmt.Sprintf("name=%s", testcase.name)}, Namespace: "", Env: "default", - }, files) + } + app = injectFs(app, fs) + noop := func(st *state.HelmState, helm helmexec.Interface) []error { + return []error{} + } + err := app.VisitDesiredStatesWithReleasesFiltered( "helmfile.yaml", noop, ) @@ -668,7 +704,7 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) { func TestLoadDesiredStateFromYaml_Bases(t *testing.T) { yamlFile := "/path/to/yaml/file" - yamlContent := []byte(`bases: + yamlContent := `bases: - ../base.yaml - ../base.gotmpl @@ -685,41 +721,34 @@ releases: labels: stage: post <<: *default -`) - files := map[string][]byte{ +` + testFs := state.NewTestFs(map[string]string{ yamlFile: yamlContent, - "/path/to/base.yaml": []byte(`environments: + "/path/to/base.yaml": `environments: default: values: - environments/default/1.yaml -`), - "/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), - "/path/to/base.gotmpl": []byte(`environments: +`, + "/path/to/yaml/environments/default/1.yaml": `foo: FOO`, + "/path/to/base.gotmpl": `environments: default: values: - environments/default/2.yaml helmDefaults: tillerNamespace: {{ .Environment.Values.tillerNs }} -`), - "/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), - "/path/to/yaml/templates.yaml": []byte(`templates: +`, + "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`, + "/path/to/yaml/templates.yaml": `templates: default: &default missingFileHandler: Warn values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] -`), - } - readFile := func(filename string) ([]byte, error) { - content, ok := files[filename] - if !ok { - return nil, fmt.Errorf("unexpected filename: %s", filename) - } - return content, nil - } +`, + }) app := &App{ - readFile: readFile, - glob: filepath.Glob, - abs: filepath.Abs, + readFile: testFs.ReadFile, + glob: testFs.Glob, + abs: testFs.Abs, KubeContext: "default", Env: "default", Logger: helmexec.NewLogger(os.Stderr, "debug"), @@ -744,7 +773,7 @@ helmDefaults: func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) { yamlFile := "/path/to/yaml/file" - yamlContent := []byte(`bases: + yamlContent := `bases: - ../base.yaml --- bases: @@ -771,41 +800,34 @@ releases: labels: stage: post <<: *default -`) - files := map[string][]byte{ +` + testFs := state.NewTestFs(map[string]string{ yamlFile: yamlContent, - "/path/to/base.yaml": []byte(`environments: + "/path/to/base.yaml": `environments: default: values: - environments/default/1.yaml -`), - "/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), - "/path/to/base.gotmpl": []byte(`environments: +`, + "/path/to/yaml/environments/default/1.yaml": `foo: FOO`, + "/path/to/base.gotmpl": `environments: default: values: - environments/default/2.yaml helmDefaults: tillerNamespace: {{ .Environment.Values.tillerNs }} -`), - "/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), - "/path/to/yaml/templates.yaml": []byte(`templates: +`, + "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`, + "/path/to/yaml/templates.yaml": `templates: default: &default missingFileHandler: Warn values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] -`), - } - readFile := func(filename string) ([]byte, error) { - content, ok := files[filename] - if !ok { - return nil, fmt.Errorf("unexpected filename: %s", filename) - } - return content, nil - } +`, + }) app := &App{ - readFile: readFile, - glob: filepath.Glob, - abs: filepath.Abs, + readFile: testFs.ReadFile, + glob: testFs.Glob, + abs: testFs.Abs, Env: "default", Logger: helmexec.NewLogger(os.Stderr, "debug"), } @@ -845,7 +867,7 @@ helmDefaults: func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) { yamlFile := "/path/to/yaml/file" - yamlContent := []byte(`bases: + yamlContent := `bases: - ../base.yaml --- bases: @@ -872,41 +894,34 @@ releases: labels: stage: post <<: *default -`) - files := map[string][]byte{ +` + testFs := state.NewTestFs(map[string]string{ yamlFile: yamlContent, - "/path/to/base.yaml": []byte(`environments: + "/path/to/base.yaml": `environments: test: values: - environments/default/1.yaml -`), - "/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), - "/path/to/base.gotmpl": []byte(`environments: +`, + "/path/to/yaml/environments/default/1.yaml": `foo: FOO`, + "/path/to/base.gotmpl": `environments: test: values: - environments/default/2.yaml helmDefaults: tillerNamespace: {{ .Environment.Values.tillerNs }} -`), - "/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), - "/path/to/yaml/templates.yaml": []byte(`templates: +`, + "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`, + "/path/to/yaml/templates.yaml": `templates: default: &default missingFileHandler: Warn values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] -`), - } - readFile := func(filename string) ([]byte, error) { - content, ok := files[filename] - if !ok { - return nil, fmt.Errorf("unexpected filename: %s", filename) - } - return content, nil - } +`, + }) app := &App{ - readFile: readFile, - glob: filepath.Glob, - abs: filepath.Abs, + readFile: testFs.ReadFile, + glob: testFs.Glob, + abs: testFs.Abs, Env: "test", Logger: helmexec.NewLogger(os.Stderr, "debug"), } @@ -946,7 +961,7 @@ helmDefaults: func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithReverse(t *testing.T) { yamlFile := "/path/to/yaml/file" - yamlContent := []byte(` + yamlContent := ` {{ readFile "templates.yaml" }} releases: @@ -965,26 +980,19 @@ releases: - name: myrelease3 chart: mychart3 <<: *default -`) - files := map[string][]byte{ +` + testFs := state.NewTestFs(map[string]string{ yamlFile: yamlContent, - "/path/to/yaml/templates.yaml": []byte(`templates: + "/path/to/yaml/templates.yaml": `templates: default: &default missingFileHandler: Warn values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] -`), - } - readFile := func(filename string) ([]byte, error) { - content, ok := files[filename] - if !ok { - return nil, fmt.Errorf("unexpected filename: %s", filename) - } - return content, nil - } +`, + }) app := &App{ - readFile: readFile, - glob: filepath.Glob, - abs: filepath.Abs, + readFile: testFs.ReadFile, + glob: testFs.Glob, + abs: testFs.Abs, Env: "default", Logger: helmexec.NewLogger(os.Stderr, "debug"), Reverse: true, diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index 5f3c5e01..e101aba7 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -95,7 +95,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool) } func (a *desiredStateLoader) underlying() *state.StateCreator { - c := state.NewCreator(a.logger, a.readFile, a.abs) + c := state.NewCreator(a.logger, a.readFile, a.abs, a.glob) c.LoadFile = a.loadFile return c } @@ -108,18 +108,10 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas helmfiles := []state.SubHelmfileSpec{} for _, hf := range st.Helmfiles { - globPattern := hf.Path - var absPathPattern string - if filepath.IsAbs(globPattern) { - absPathPattern = globPattern - } else { - absPathPattern = st.JoinBase(globPattern) - } - matches, err := a.glob(absPathPattern) + matches, err := st.ExpandPaths([]string{hf.Path}, a.glob) if err != nil { - return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) + return nil, err } - sort.Strings(matches) 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 e042c1c4..bf9656d8 100644 --- a/pkg/app/two_pass_renderer_test.go +++ b/pkg/app/two_pass_renderer_test.go @@ -1,9 +1,7 @@ package app import ( - "fmt" "os" - "path/filepath" "strings" "testing" @@ -12,14 +10,16 @@ import ( "gopkg.in/yaml.v2" ) -func makeLoader(readFile func(string) ([]byte, error), env string) *desiredStateLoader { +func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) { + testfs := state.NewTestFs(files) return &desiredStateLoader{ - readFile: readFile, env: env, namespace: "namespace", logger: helmexec.NewLogger(os.Stdout, "debug"), - abs: filepath.Abs, - } + readFile: testfs.ReadFile, + abs: testfs.Abs, + glob: testfs.Glob, + }, testfs } func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) { @@ -36,30 +36,21 @@ releases: chart: mychart1 `) - fileReaderCalls := 0 - // make a reader that returns a simulated context - fileReader := func(filename string) ([]byte, error) { - expectedFilename := filepath.Clean("default/values.yaml") - if !strings.HasSuffix(filename, expectedFilename) { - return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename) - } - fileReaderCalls++ - if fileReaderCalls == 2 { - return []byte("SecondPass"), nil - } - return []byte(""), nil + files := map[string]string{ + "/path/to/default/values.yaml": ``, + "/path/to/other/default/values.yaml": `SecondPass`, } - r := makeLoader(fileReader, "staging") + r, testfs := makeLoader(files, "staging") yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } var state state.HelmState err = yaml.Unmarshal(yamlBuf.Bytes(), &state) - if fileReaderCalls > 2 { + if testfs.FileReaderCalls() > 2 { t.Error("reader should be called only twice") } @@ -70,10 +61,10 @@ releases: func TestReadFromYaml_RenderTemplate(t *testing.T) { - defaultValuesYaml := []byte(` + defaultValuesYaml := ` releaseName: "hello" conditionalReleaseTag: "yes" -`) +` yamlContent := []byte(` environments: @@ -92,16 +83,11 @@ releases: `) - // make a reader that returns a simulated context - fileReader := func(filename string) ([]byte, error) { - expectedFilename := filepath.Clean("default/values.yaml") - if !strings.HasSuffix(filename, expectedFilename) { - return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename) - } - return defaultValuesYaml, nil + files := map[string]string{ + "/path/to/default/values.yaml": defaultValuesYaml, } - r := makeLoader(fileReader, "staging") + r, _ := makeLoader(files, "staging") // test the double rendering yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) if err != nil { @@ -129,7 +115,7 @@ releases: } func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) { - defaultValuesYaml := []byte("") + defaultValuesYaml := `` yamlContent := []byte(` environments: @@ -145,12 +131,11 @@ releases: {{ end }} `) - // make a reader that returns a simulated context - fileReader := func(filename string) ([]byte, error) { - return defaultValuesYaml, nil + files := map[string]string{ + "/path/to/default/values.yaml": defaultValuesYaml, } - r := makeLoader(fileReader, "staging") + r, _ := makeLoader(files, "staging") // test the double rendering _, err := r.renderTemplatesToYaml("", "", yamlContent) @@ -164,9 +149,9 @@ releases: // This does not apply to .gotmpl files, which is a nice side-effect. func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) { - defaultValuesYamlGotmpl := []byte(` + defaultValuesYamlGotmpl := ` releaseName: {{ readFile "nonIgnoredFile" }} -`) +` yamlContent := []byte(` environments: @@ -182,14 +167,12 @@ releases: {{ end }} `) - fileReader := func(filename string) ([]byte, error) { - if strings.HasSuffix(filename, "nonIgnoredFile") { - return []byte("release-a"), nil - } - return defaultValuesYamlGotmpl, nil + files := map[string]string{ + "/path/to/nonIgnoredFile": `release-a`, + "/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl, } - r := makeLoader(fileReader, "staging") + r, _ := makeLoader(files, "staging") rendered, _ := r.renderTemplatesToYaml("", "", yamlContent) var state state.HelmState @@ -205,18 +188,14 @@ releases: } func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) { - defaultValuesYaml := []byte(``) yamlContent := []byte(`releases: - name: {{ .Namespace }}-myrelease chart: mychart `) - // make a reader that returns a simulated context - fileReader := func(filename string) ([]byte, error) { - return defaultValuesYaml, nil - } + files := map[string]string{} - r := makeLoader(fileReader, "staging") + r, _ := makeLoader(files, "staging") yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -243,11 +222,8 @@ releases: {{ end }} chart: mychart `) - fileReader := func(filename string) ([]byte, error) { - return yamlContent, nil - } - r := makeLoader(fileReader, "staging") + r, _ := makeLoader(map[string]string{}, "staging") _, err := r.renderTemplatesToYaml("", "", yamlContent) if err == nil { t.Fatalf("wanted error, none returned") diff --git a/state/create.go b/state/create.go index 3f122522..d5ef68a7 100644 --- a/state/create.go +++ b/state/create.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "sort" "github.com/imdario/mergo" "github.com/roboll/helmfile/environment" @@ -37,17 +38,19 @@ type StateCreator struct { logger *zap.SugaredLogger readFile func(string) ([]byte, error) abs func(string) (string, error) + glob func(string) ([]string, error) Strict bool LoadFile func(baseDir, file string, evaluateBases bool) (*HelmState, error) } -func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error)) *StateCreator { +func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator { return &StateCreator{ logger: logger, readFile: readFile, abs: abs, + glob: glob, Strict: true, } } @@ -118,7 +121,7 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment) (*HelmState, error) { state := *target - e, err := state.loadEnvValues(env, ctxEnv, c.readFile) + e, err := state.loadEnvValues(env, ctxEnv, c.readFile, c.glob) if err != nil { return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err} } @@ -168,31 +171,64 @@ func (c *StateCreator) loadBases(st *HelmState, baseDir string) (*HelmState, err return layers[0], nil } -func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error)) (*environment.Environment, error) { +func (st *HelmState) ExpandPaths(patterns []string, glob func(string) ([]string, error)) ([]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...) + } + return result, nil +} + +func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) { envVals := map[string]interface{}{} envSpec, ok := st.Environments[name] if ok { - for _, envvalFile := range envSpec.Values { - envvalFullPath := filepath.Join(st.basePath, envvalFile) + valuesFiles, err := st.ExpandPaths(envSpec.Values, glob) + if err != nil { + return nil, err + } + + for _, envvalFullPath := range valuesFiles { tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""} r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData) bytes, err := r.RenderToBytes(envvalFullPath) if err != nil { - return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err) + return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err) } m := map[string]interface{}{} if err := yaml.Unmarshal(bytes, &m); err != nil { - return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err) + return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err) } if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil { - return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFile, err) + return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFullPath, err) } } if len(envSpec.Secrets) > 0 { + secretsFiles, err := st.ExpandPaths(envSpec.Secrets, glob) + if err != nil { + return nil, err + } + helm := helmexec.New(st.logger, "") - for _, secFile := range envSpec.Secrets { - path := filepath.Join(st.basePath, secFile) + for _, path := range secretsFiles { if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } @@ -212,14 +248,14 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, } bytes, err := readFile(decFile) if err != nil { - return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secFile, err) + return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err) } m := map[string]interface{}{} if err := yaml.Unmarshal(bytes, &m); err != nil { - return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secFile, err) + return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err) } if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil { - return nil, fmt.Errorf("failed to load \"%s\": %v", secFile, err) + return nil, fmt.Errorf("failed to load \"%s\": %v", path, err) } } } diff --git a/state/create_test.go b/state/create_test.go index bb934d2a..3850a42d 100644 --- a/state/create_test.go +++ b/state/create_test.go @@ -1,7 +1,6 @@ package state import ( - "fmt" "go.uber.org/zap" "io/ioutil" "path/filepath" @@ -99,23 +98,17 @@ bar: {{ readFile "bar.txt" }} expectedValues := `env: production` - readFile := func(filename string) ([]byte, error) { - switch filename { - case fooYamlFile: - return fooYamlContent, nil - case barYamlFile: - return barYamlContent, nil - case barTextFile: - return barTextContent, nil - case valuesFile: - return valuesContent, nil - } - return nil, fmt.Errorf("unexpected filename: %s", filename) - } + testFs := NewTestFs(map[string]string{ + fooYamlFile: string(fooYamlContent), + barYamlFile: string(barYamlContent), + barTextFile: string(barTextContent), + valuesFile: string(valuesContent), + }) + testFs.Cwd = "/example/path/to" - state, err := NewCreator(logger, readFile, filepath.Abs).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil) + state, err := NewCreator(logger, testFs.ReadFile, testFs.Abs, testFs.Glob).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } actual := state.Env.Values diff --git a/state/testfs.go b/state/testfs.go new file mode 100644 index 00000000..31e55aca --- /dev/null +++ b/state/testfs.go @@ -0,0 +1,130 @@ +package state + +import ( + "fmt" + "path/filepath" + "strings" +) + +type TestFs struct { + Cwd string + dirs map[string]bool + files map[string]string + + GlobFixtures map[string][]string + + fileReaderCalls int + successfulReads []string +} + +func NewTestFs(files map[string]string) *TestFs { + dirs := map[string]bool{} + for abs, _ := range files { + d := filepath.Dir(abs) + dirs[d] = true + } + return &TestFs{ + Cwd: "/path/to", + dirs: dirs, + files: files, + + successfulReads: []string{}, + + GlobFixtures: map[string][]string{}, + } +} + +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.Cwd, 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.Cwd, path)] + } + return ok +} + +func (f *TestFs) ReadFile(filename string) ([]byte, error) { + var str string + var ok bool + if filename[0] == '/' { + str, ok = f.files[filename] + } else { + str, ok = f.files[filepath.Join(f.Cwd, filename)] + } + if !ok { + return []byte(nil), fmt.Errorf("no registered file found: %s", filename) + } + + f.fileReaderCalls += 1 + + f.successfulReads = append(f.successfulReads, filename) + + return []byte(str), nil +} + +func (f *TestFs) SuccessfulReads() []string { + return f.successfulReads +} + +func (f *TestFs) FileReaderCalls() int { + return f.fileReaderCalls +} + +func (f *TestFs) Glob(relPattern string) ([]string, error) { + var pattern string + if relPattern[0] == '/' { + pattern = relPattern + } else { + pattern = filepath.Join(f.Cwd, relPattern) + } + + fixtures, ok := f.GlobFixtures[pattern] + if ok { + return fixtures, nil + } + + 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) + } + } + return matches, nil +} + +func (f *TestFs) Abs(path string) (string, error) { + var p string + if path[0] == '/' { + p = path + } else { + p = filepath.Join(f.Cwd, path) + } + return filepath.Clean(p), nil +} + +func (f *TestFs) Getwd() (string, error) { + return f.Cwd, nil +} + +func (f *TestFs) Chdir(dir string) error { + if _, ok := f.dirs[dir]; ok { + f.Cwd = dir + return nil + } + return fmt.Errorf("unexpected chdir \"%s\"", dir) +}