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"
"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,

View File

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

View File

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

View File

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

View File

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

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