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:
parent
4c9c42d3c5
commit
90390492a3
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
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(relPattern string) ([]string, error) {
|
||||
var pattern string
|
||||
if relPattern[0] == '/' {
|
||||
pattern = relPattern
|
||||
} else {
|
||||
pattern = filepath.Join(f.wd, relPattern)
|
||||
app = injectFs(app, fs)
|
||||
actualOrder := []string{}
|
||||
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
actualOrder = append(actualOrder, st.FilePath)
|
||||
return []error{}
|
||||
}
|
||||
|
||||
matches := []string{}
|
||||
for name, _ := range f.files {
|
||||
matched, err := filepath.Match(pattern, name)
|
||||
err := app.VisitDesiredStatesWithReleasesFiltered(
|
||||
"helmfile.yaml", noop,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if matched {
|
||||
matches = append(matches, name)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
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 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
|
||||
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
|
||||
`,
|
||||
}
|
||||
fs := state.NewTestFs(files)
|
||||
app := &App{
|
||||
KubeContext: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Namespace: "",
|
||||
Env: "default",
|
||||
}
|
||||
app = injectFs(app, fs)
|
||||
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
return []error{}
|
||||
}
|
||||
|
||||
err := app.VisitDesiredStatesWithReleasesFiltered(
|
||||
"helmfile.yaml", noop,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected error did not occur")
|
||||
}
|
||||
|
||||
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 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue