feat: expand glob pattern in environment values file path (#610)

This enhances helmfile's internal environment values files loader to expand glob patterns (#606)

Fixes the existing bug that helmfile was unable to load environment values file from absolute path (#549)

Resolves #606
Fixes #549
This commit is contained in:
KUOKA Yusuke 2019-05-21 16:49:57 +09:00 committed by GitHub
parent 4c9c42d3c5
commit 90390492a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 410 additions and 275 deletions

View File

@ -5,7 +5,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/helmexec"
@ -13,119 +12,153 @@ import (
"gotest.tools/env" "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 { func appWithFs(app *App, files map[string]string) *App {
fs := newTestFs(files) fs := state.NewTestFs(files)
return injectFs(app, fs) return injectFs(app, fs)
} }
func injectFs(app *App, fs *testFs) *App { func injectFs(app *App, fs *state.TestFs) *App {
app.readFile = fs.readFile app.readFile = fs.ReadFile
app.glob = fs.glob app.glob = fs.Glob
app.abs = fs.abs app.abs = fs.Abs
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.directoryExistsAt = fs.directoryExistsAt app.directoryExistsAt = fs.DirectoryExistsAt
return app return app
} }
func newTestFs(files map[string]string) *testFs { func TestVisitDesiredStatesWithReleasesFiltered_ReleaseOrder(t *testing.T) {
dirs := map[string]bool{} files := map[string]string{
for abs, _ := range files { "/path/to/helmfile.yaml": `
d := filepath.Dir(abs) helmfiles:
dirs[d] = true - 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{ fs := state.NewTestFs(files)
wd: "/path/to", fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
dirs: dirs, app := &App{
files: files, 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 { func TestVisitDesiredStatesWithReleasesFiltered_EnvValuesFileOrder(t *testing.T) {
var ok bool files := map[string]string{
if strings.Contains(path, "/") { "/path/to/helmfile.yaml": `
_, ok = f.files[path] environments:
} else { default:
_, ok = f.files[filepath.Join(f.wd, path)] 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 { func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFile(t *testing.T) {
var ok bool files := map[string]string{
if strings.Contains(path, "/") { "/path/to/helmfile.yaml": `
_, ok = f.dirs[path] environments:
} else { default:
_, ok = f.dirs[filepath.Join(f.wd, path)] values:
- env.*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
} }
return ok fs := state.NewTestFs(files)
} app := &App{
KubeContext: "default",
func (f *testFs) readFile(filename string) ([]byte, error) { Logger: helmexec.NewLogger(os.Stderr, "debug"),
var str string Namespace: "",
var ok bool Env: "default",
if strings.Contains(filename, "/") {
str, ok = f.files[filename]
} else {
str, ok = f.files[filepath.Join(f.wd, filename)]
} }
if !ok { app = injectFs(app, fs)
return []byte(nil), fmt.Errorf("no file found: %s", filename) noop := func(st *state.HelmState, helm helmexec.Interface) []error {
} return []error{}
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)
} }
matches := []string{} err := app.VisitDesiredStatesWithReleasesFiltered(
for name, _ := range f.files { "helmfile.yaml", noop,
matched, err := filepath.Match(pattern, name) )
if err != nil { if err == nil {
return nil, err t.Fatal("expected error did not occur")
}
if matched {
matches = append(matches, name)
}
} }
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) { expected := "in ./helmfile.yaml: failed to read helmfile.yaml: no file matching env.*.yaml found"
var p string if err.Error() != expected {
if path[0] == '/' { t.Errorf("unexpected error: expected=%s, got=%v", expected, err)
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 // See https://github.com/roboll/helmfile/issues/193
@ -152,10 +185,6 @@ releases:
chart: stable/grafana chart: stable/grafana
`, `,
} }
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
testcases := []struct { testcases := []struct {
name string name string
expectErr bool expectErr bool
@ -167,13 +196,20 @@ releases:
} }
for _, testcase := range testcases { 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", KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)}, Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "", Namespace: "",
Env: "default", Env: "default",
}, files) }
app = injectFs(app, fs)
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered( err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop, "helmfile.yaml", noop,
) )
@ -668,7 +704,7 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
func TestLoadDesiredStateFromYaml_Bases(t *testing.T) { func TestLoadDesiredStateFromYaml_Bases(t *testing.T) {
yamlFile := "/path/to/yaml/file" yamlFile := "/path/to/yaml/file"
yamlContent := []byte(`bases: yamlContent := `bases:
- ../base.yaml - ../base.yaml
- ../base.gotmpl - ../base.gotmpl
@ -685,41 +721,34 @@ releases:
labels: labels:
stage: post stage: post
<<: *default <<: *default
`) `
files := map[string][]byte{ testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent, yamlFile: yamlContent,
"/path/to/base.yaml": []byte(`environments: "/path/to/base.yaml": `environments:
default: default:
values: values:
- environments/default/1.yaml - environments/default/1.yaml
`), `,
"/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), "/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": []byte(`environments: "/path/to/base.gotmpl": `environments:
default: default:
values: values:
- environments/default/2.yaml - environments/default/2.yaml
helmDefaults: helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }} tillerNamespace: {{ .Environment.Values.tillerNs }}
`), `,
"/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": []byte(`templates: "/path/to/yaml/templates.yaml": `templates:
default: &default default: &default
missingFileHandler: Warn missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] 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{ app := &App{
readFile: readFile, readFile: testFs.ReadFile,
glob: filepath.Glob, glob: testFs.Glob,
abs: filepath.Abs, abs: testFs.Abs,
KubeContext: "default", KubeContext: "default",
Env: "default", Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
@ -744,7 +773,7 @@ helmDefaults:
func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) { func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) {
yamlFile := "/path/to/yaml/file" yamlFile := "/path/to/yaml/file"
yamlContent := []byte(`bases: yamlContent := `bases:
- ../base.yaml - ../base.yaml
--- ---
bases: bases:
@ -771,41 +800,34 @@ releases:
labels: labels:
stage: post stage: post
<<: *default <<: *default
`) `
files := map[string][]byte{ testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent, yamlFile: yamlContent,
"/path/to/base.yaml": []byte(`environments: "/path/to/base.yaml": `environments:
default: default:
values: values:
- environments/default/1.yaml - environments/default/1.yaml
`), `,
"/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), "/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": []byte(`environments: "/path/to/base.gotmpl": `environments:
default: default:
values: values:
- environments/default/2.yaml - environments/default/2.yaml
helmDefaults: helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }} tillerNamespace: {{ .Environment.Values.tillerNs }}
`), `,
"/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": []byte(`templates: "/path/to/yaml/templates.yaml": `templates:
default: &default default: &default
missingFileHandler: Warn missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] 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{ app := &App{
readFile: readFile, readFile: testFs.ReadFile,
glob: filepath.Glob, glob: testFs.Glob,
abs: filepath.Abs, abs: testFs.Abs,
Env: "default", Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
@ -845,7 +867,7 @@ helmDefaults:
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) { func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) {
yamlFile := "/path/to/yaml/file" yamlFile := "/path/to/yaml/file"
yamlContent := []byte(`bases: yamlContent := `bases:
- ../base.yaml - ../base.yaml
--- ---
bases: bases:
@ -872,41 +894,34 @@ releases:
labels: labels:
stage: post stage: post
<<: *default <<: *default
`) `
files := map[string][]byte{ testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent, yamlFile: yamlContent,
"/path/to/base.yaml": []byte(`environments: "/path/to/base.yaml": `environments:
test: test:
values: values:
- environments/default/1.yaml - environments/default/1.yaml
`), `,
"/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), "/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": []byte(`environments: "/path/to/base.gotmpl": `environments:
test: test:
values: values:
- environments/default/2.yaml - environments/default/2.yaml
helmDefaults: helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }} tillerNamespace: {{ .Environment.Values.tillerNs }}
`), `,
"/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), "/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": []byte(`templates: "/path/to/yaml/templates.yaml": `templates:
default: &default default: &default
missingFileHandler: Warn missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] 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{ app := &App{
readFile: readFile, readFile: testFs.ReadFile,
glob: filepath.Glob, glob: testFs.Glob,
abs: filepath.Abs, abs: testFs.Abs,
Env: "test", Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
@ -946,7 +961,7 @@ helmDefaults:
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithReverse(t *testing.T) { func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithReverse(t *testing.T) {
yamlFile := "/path/to/yaml/file" yamlFile := "/path/to/yaml/file"
yamlContent := []byte(` yamlContent := `
{{ readFile "templates.yaml" }} {{ readFile "templates.yaml" }}
releases: releases:
@ -965,26 +980,19 @@ releases:
- name: myrelease3 - name: myrelease3
chart: mychart3 chart: mychart3
<<: *default <<: *default
`) `
files := map[string][]byte{ testFs := state.NewTestFs(map[string]string{
yamlFile: yamlContent, yamlFile: yamlContent,
"/path/to/yaml/templates.yaml": []byte(`templates: "/path/to/yaml/templates.yaml": `templates:
default: &default default: &default
missingFileHandler: Warn missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] 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{ app := &App{
readFile: readFile, readFile: testFs.ReadFile,
glob: filepath.Glob, glob: testFs.Glob,
abs: filepath.Abs, abs: testFs.Abs,
Env: "default", Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: true, Reverse: true,

View File

@ -95,7 +95,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool)
} }
func (a *desiredStateLoader) underlying() *state.StateCreator { 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 c.LoadFile = a.loadFile
return c return c
} }
@ -108,18 +108,10 @@ 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 {
globPattern := hf.Path matches, err := st.ExpandPaths([]string{hf.Path}, a.glob)
var absPathPattern string
if filepath.IsAbs(globPattern) {
absPathPattern = globPattern
} else {
absPathPattern = st.JoinBase(globPattern)
}
matches, err := a.glob(absPathPattern)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) return nil, err
} }
sort.Strings(matches)
for _, match := range matches { for _, match := range matches {
newHelmfile := hf newHelmfile := hf
newHelmfile.Path = match newHelmfile.Path = match

View File

@ -1,9 +1,7 @@
package app package app
import ( import (
"fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -12,14 +10,16 @@ import (
"gopkg.in/yaml.v2" "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{ return &desiredStateLoader{
readFile: readFile,
env: env, env: env,
namespace: "namespace", namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"), 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) { func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
@ -36,30 +36,21 @@ releases:
chart: mychart1 chart: mychart1
`) `)
fileReaderCalls := 0 files := map[string]string{
// make a reader that returns a simulated context "/path/to/default/values.yaml": ``,
fileReader := func(filename string) ([]byte, error) { "/path/to/other/default/values.yaml": `SecondPass`,
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
} }
r := makeLoader(fileReader, "staging") r, testfs := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
var state state.HelmState var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state) err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if fileReaderCalls > 2 { if testfs.FileReaderCalls() > 2 {
t.Error("reader should be called only twice") t.Error("reader should be called only twice")
} }
@ -70,10 +61,10 @@ releases:
func TestReadFromYaml_RenderTemplate(t *testing.T) { func TestReadFromYaml_RenderTemplate(t *testing.T) {
defaultValuesYaml := []byte(` defaultValuesYaml := `
releaseName: "hello" releaseName: "hello"
conditionalReleaseTag: "yes" conditionalReleaseTag: "yes"
`) `
yamlContent := []byte(` yamlContent := []byte(`
environments: environments:
@ -92,16 +83,11 @@ releases:
`) `)
// make a reader that returns a simulated context files := map[string]string{
fileReader := func(filename string) ([]byte, error) { "/path/to/default/values.yaml": defaultValuesYaml,
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
} }
r := makeLoader(fileReader, "staging") r, _ := makeLoader(files, "staging")
// test the double rendering // test the double rendering
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil { if err != nil {
@ -129,7 +115,7 @@ releases:
} }
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) { func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
defaultValuesYaml := []byte("") defaultValuesYaml := ``
yamlContent := []byte(` yamlContent := []byte(`
environments: environments:
@ -145,12 +131,11 @@ releases:
{{ end }} {{ end }}
`) `)
// make a reader that returns a simulated context files := map[string]string{
fileReader := func(filename string) ([]byte, error) { "/path/to/default/values.yaml": defaultValuesYaml,
return defaultValuesYaml, nil
} }
r := makeLoader(fileReader, "staging") r, _ := makeLoader(files, "staging")
// test the double rendering // test the double rendering
_, err := r.renderTemplatesToYaml("", "", yamlContent) _, err := r.renderTemplatesToYaml("", "", yamlContent)
@ -164,9 +149,9 @@ releases:
// This does not apply to .gotmpl files, which is a nice side-effect. // This does not apply to .gotmpl files, which is a nice side-effect.
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) { func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
defaultValuesYamlGotmpl := []byte(` defaultValuesYamlGotmpl := `
releaseName: {{ readFile "nonIgnoredFile" }} releaseName: {{ readFile "nonIgnoredFile" }}
`) `
yamlContent := []byte(` yamlContent := []byte(`
environments: environments:
@ -182,14 +167,12 @@ releases:
{{ end }} {{ end }}
`) `)
fileReader := func(filename string) ([]byte, error) { files := map[string]string{
if strings.HasSuffix(filename, "nonIgnoredFile") { "/path/to/nonIgnoredFile": `release-a`,
return []byte("release-a"), nil "/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl,
}
return defaultValuesYamlGotmpl, nil
} }
r := makeLoader(fileReader, "staging") r, _ := makeLoader(files, "staging")
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent) rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
var state state.HelmState var state state.HelmState
@ -205,18 +188,14 @@ releases:
} }
func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) { func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
defaultValuesYaml := []byte(``)
yamlContent := []byte(`releases: yamlContent := []byte(`releases:
- name: {{ .Namespace }}-myrelease - name: {{ .Namespace }}-myrelease
chart: mychart chart: mychart
`) `)
// make a reader that returns a simulated context files := map[string]string{}
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
}
r := makeLoader(fileReader, "staging") r, _ := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent) yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
@ -243,11 +222,8 @@ releases:
{{ end }} {{ end }}
chart: mychart 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) _, err := r.renderTemplatesToYaml("", "", yamlContent)
if err == nil { if err == nil {
t.Fatalf("wanted error, none returned") t.Fatalf("wanted error, none returned")

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/environment"
@ -37,17 +38,19 @@ type StateCreator struct {
logger *zap.SugaredLogger logger *zap.SugaredLogger
readFile func(string) ([]byte, error) readFile func(string) ([]byte, error)
abs func(string) (string, error) abs func(string) (string, error)
glob func(string) ([]string, error)
Strict bool Strict bool
LoadFile func(baseDir, file string, evaluateBases bool) (*HelmState, error) 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{ return &StateCreator{
logger: logger, logger: logger,
readFile: readFile, readFile: readFile,
abs: abs, abs: abs,
glob: glob,
Strict: true, 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) { func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment) (*HelmState, error) {
state := *target state := *target
e, err := state.loadEnvValues(env, ctxEnv, c.readFile) e, err := state.loadEnvValues(env, ctxEnv, c.readFile, c.glob)
if err != nil { if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err} 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 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{}{} envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name] envSpec, ok := st.Environments[name]
if ok { if ok {
for _, envvalFile := range envSpec.Values { valuesFiles, err := st.ExpandPaths(envSpec.Values, glob)
envvalFullPath := filepath.Join(st.basePath, envvalFile) if err != nil {
return nil, err
}
for _, envvalFullPath := range valuesFiles {
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)
if err != nil { 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{}{} m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil { 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 { 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 { 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 _, secFile := range envSpec.Secrets { for _, path := range secretsFiles {
path := filepath.Join(st.basePath, secFile)
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
} }
@ -212,14 +248,14 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
} }
bytes, err := readFile(decFile) bytes, err := readFile(decFile)
if err != nil { 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{}{} m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil { 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 { 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)
} }
} }
} }

View File

@ -1,7 +1,6 @@
package state package state
import ( import (
"fmt"
"go.uber.org/zap" "go.uber.org/zap"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
@ -99,23 +98,17 @@ bar: {{ readFile "bar.txt" }}
expectedValues := `env: production` expectedValues := `env: production`
readFile := func(filename string) ([]byte, error) { testFs := NewTestFs(map[string]string{
switch filename { fooYamlFile: string(fooYamlContent),
case fooYamlFile: barYamlFile: string(barYamlContent),
return fooYamlContent, nil barTextFile: string(barTextContent),
case barYamlFile: valuesFile: string(valuesContent),
return barYamlContent, nil })
case barTextFile: testFs.Cwd = "/example/path/to"
return barTextContent, nil
case valuesFile:
return valuesContent, nil
}
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
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 { if err != nil {
t.Errorf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
actual := state.Env.Values actual := state.Env.Values

130
state/testfs.go Normal file
View File

@ -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)
}