feat: `helmfile repos` updates all repos regardless of selector and releases (#353)

Resolves #338
This commit is contained in:
KUOKA Yusuke 2018-09-27 01:44:05 +09:00 committed by GitHub
parent 6cba77d4f2
commit f2b610afdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 231 additions and 144 deletions

View File

@ -4,13 +4,15 @@ import (
"fmt" "fmt"
"github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state" "github.com/roboll/helmfile/state"
"io/ioutil"
"os" "os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
) )
// See https://github.com/roboll/helmfile/issues/193 // See https://github.com/roboll/helmfile/issues/193
func TestFindAndIterateOverDesiredStates(t *testing.T) { func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) {
absPaths := map[string]string{ absPaths := map[string]string{
".": "/path/to", ".": "/path/to",
"/path/to/helmfile.d": "/path/to/helmfile.d", "/path/to/helmfile.d": "/path/to/helmfile.d",
@ -73,15 +75,6 @@ releases:
} }
return a, nil return a, nil
} }
app := &app{
readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error { noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{} return []error{}
} }
@ -97,8 +90,20 @@ releases:
} }
for _, testcase := range testcases { for _, testcase := range testcases {
err := app.FindAndIterateOverDesiredStates( app := &app{
"helmfile.yaml", noop, "", []string{fmt.Sprintf("name=%s", testcase.name)}, "default", readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
namespace: "",
env: "default",
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
) )
if testcase.expectErr && err == nil { if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for name=%s", testcase.name) t.Errorf("error expected but not happened for name=%s", testcase.name)
@ -109,7 +114,7 @@ releases:
} }
// See https://github.com/roboll/helmfile/issues/320 // See https://github.com/roboll/helmfile/issues/320
func TestFindAndIterateOverDesiredStates_UndefinedEnv(t *testing.T) { func TestVisitDesiredStatesWithReleasesFiltered_UndefinedEnv(t *testing.T) {
absPaths := map[string]string{ absPaths := map[string]string{
".": "/path/to", ".": "/path/to",
"/path/to/helmfile.d": "/path/to/helmfile.d", "/path/to/helmfile.d": "/path/to/helmfile.d",
@ -166,15 +171,6 @@ releases:
} }
return a, nil return a, nil
} }
app := &app{
readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error { noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{} return []error{}
} }
@ -189,8 +185,20 @@ releases:
} }
for _, testcase := range testcases { for _, testcase := range testcases {
err := app.FindAndIterateOverDesiredStates( app := &app{
"helmfile.yaml", noop, "", []string{}, testcase.name, readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
namespace: "",
selectors: []string{},
env: testcase.name,
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
) )
if testcase.expectErr && err == nil { if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for environment=%s", testcase.name) t.Errorf("error expected but not happened for environment=%s", testcase.name)
@ -201,7 +209,7 @@ releases:
} }
// See https://github.com/roboll/helmfile/issues/322 // See https://github.com/roboll/helmfile/issues/322
func TestFindAndIterateOverDesiredStates_Selectors(t *testing.T) { func TestVisitDesiredStatesWithReleasesFiltered_Selectors(t *testing.T) {
absPaths := map[string]string{ absPaths := map[string]string{
".": "/path/to", ".": "/path/to",
"/path/to/helmfile.d": "/path/to/helmfile.d", "/path/to/helmfile.d": "/path/to/helmfile.d",
@ -229,6 +237,14 @@ releases:
releases: releases:
- name: grafana - name: grafana
chart: stable/grafana chart: stable/grafana
- name: foo
chart: charts/foo
labels:
duplicated: yes
- name: foo
chart: charts/foo
labels:
duplicated: yes
`, `,
} }
globMatches := map[string][]string{ globMatches := map[string][]string{
@ -264,48 +280,64 @@ releases:
} }
return a, nil return a, nil
} }
app := &app{
readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
}
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
testcases := []struct { testcases := []struct {
label string label string
expectErr bool expectedCount int
errMsg string expectErr bool
errMsg string
}{ }{
{label: "name=prometheus", expectErr: false}, {label: "name=prometheus", expectedCount: 1, expectErr: false},
{label: "name=", expectErr: true, errMsg: "Malformed label: name=. Expected label in form k=v or k!=v"}, {label: "name=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
{label: "name!=", expectErr: true, errMsg: "Malformed label: name!=. Expected label in form k=v or k!=v"}, {label: "name!=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
{label: "name", expectErr: true, errMsg: "Malformed label: name. Expected label in form k=v or k!=v"}, {label: "name", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
// See https://github.com/roboll/helmfile/issues/193
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found: there were 2 releases named \"foo\" matching specified selector"},
} }
for _, testcase := range testcases { for _, testcase := range testcases {
err := app.FindAndIterateOverDesiredStates( actual := []string{}
"helmfile.yaml", noop, "", []string{testcase.label}, "default",
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
for _, r := range st.Releases {
actual = append(actual, r.Name)
}
return []error{}
}
app := &app{
readFile: readFile,
glob: glob,
abs: abs,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"),
namespace: "",
selectors: []string{testcase.label},
env: "default",
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases,
) )
if testcase.expectErr { if testcase.expectErr {
if err == nil { if err == nil {
t.Errorf("error expected but not happened for name=%s", testcase.label) t.Errorf("error expected but not happened for selector %s", testcase.label)
} else if err.Error() != testcase.errMsg { } else if err.Error() != testcase.errMsg {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error()) t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
} }
} else if !testcase.expectErr && err != nil { } else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for name=%s: %v", testcase.label, err) t.Errorf("unexpected error for selector %s: %v", testcase.label, err)
}
if len(actual) != testcase.expectedCount {
t.Errorf("unexpected release count for selector %s: expected=%d, actual=%d", testcase.label, testcase.expectedCount, len(actual))
} }
} }
} }
// See https://github.com/roboll/helmfile/issues/312 // See https://github.com/roboll/helmfile/issues/312
func TestFindAndIterateOverDesiredStates_ReverseOrder(t *testing.T) { func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) {
absPaths := map[string]string{ absPaths := map[string]string{
".": "/path/to", ".": "/path/to",
"/path/to/helmfile.d": "/path/to/helmfile.d", "/path/to/helmfile.d": "/path/to/helmfile.d",
@ -399,9 +431,12 @@ releases:
kubeContext: "default", kubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"), logger: helmexec.NewLogger(os.Stderr, "debug"),
reverse: testcase.reverse, reverse: testcase.reverse,
namespace: "",
selectors: []string{},
env: "default",
} }
err := app.FindAndIterateOverDesiredStates( err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases, "", []string{}, "default", "helmfile.yaml", collectReleases,
) )
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
@ -411,3 +446,29 @@ releases:
} }
} }
} }
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
`)
app := &app{
readFile: ioutil.ReadFile,
glob: filepath.Glob,
abs: filepath.Abs,
kubeContext: "default",
logger: logger,
}
_, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default")
if err != nil {
t.Error("unexpected error")
}
}

175
main.go
View File

@ -10,8 +10,6 @@ import (
"sort" "sort"
"syscall" "syscall"
"os/exec"
"io/ioutil" "io/ioutil"
"strings" "strings"
@ -24,6 +22,7 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"os/exec"
) )
const ( const (
@ -115,13 +114,17 @@ func main() {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error { return visitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface) (bool, []error) {
args := args.GetArgs(c.String("args"), state) args := args.GetArgs(c.String("args"), state)
if len(args) > 0 { if len(args) > 0 {
helm.SetExtraArgs(args...) helm.SetExtraArgs(args...)
} }
return state.SyncRepos(helm) errs := state.SyncRepos(helm)
ok := len(state.Repositories) > 0 && len(errs) == 0
return ok, errs
}) })
}, },
}, },
@ -566,16 +569,19 @@ type app struct {
fileExistsAt func(string) bool fileExistsAt func(string) bool
directoryExistsAt func(string) bool directoryExistsAt func(string) bool
reverse bool reverse bool
env string
namespace string
selectors []string
} }
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error { func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
return findAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge) return findAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge)
} }
func findAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface) []error) error { func initAppEntry(c *cli.Context, reverse bool) (*app, string, error) {
if c.NArg() > 0 { if c.NArg() > 0 {
cli.ShowAppHelp(c) cli.ShowAppHelp(c)
return fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
} }
fileOrDir := c.GlobalString("file") fileOrDir := c.GlobalString("file")
@ -598,21 +604,66 @@ func findAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, revers
kubeContext: kubeContext, kubeContext: kubeContext,
logger: logger, logger: logger,
reverse: reverse, reverse: reverse,
env: env,
namespace: namespace,
selectors: selectors,
} }
return app, fileOrDir, nil
}
func visitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
app, fileOrDir, err := initAppEntry(c, false)
if err != nil {
return err
}
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm)
}
err = app.VisitDesiredStates(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
switch e := err.(type) {
case *noMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
return cli.NewExitError(e.Error(), status.ExitStatus())
case *state.DiffError:
return cli.NewExitError(e.Error(), e.Code)
default:
return cli.NewExitError(e.Error(), 1)
}
}
return err
}
func findAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface) []error) error {
app, fileOrDir, err := initAppEntry(c, reverse)
if err != nil {
return err
}
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error { convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" { if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary")) helm.SetHelmBinary(c.GlobalString("helm-binary"))
} }
return converge(st, helm) return converge(st, helm)
} }
if err := app.FindAndIterateOverDesiredStates(fileOrDir, convergeWithHelmBinary, namespace, selectors, env); err != nil {
switch e := err.(type) { err = app.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
case *noMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2) return toCliError(err)
}
return err
}
return nil
} }
type noMatchingHelmfileError struct { type noMatchingHelmfileError struct {
@ -690,11 +741,12 @@ func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error)
return yamlBuf, nil return yamlBuf, nil
} }
func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, namespace string, selectors []string, env string) error { func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir) desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil { if err != nil {
return err return err
} }
noMatchInHelmfiles := true noMatchInHelmfiles := true
for _, f := range desiredStateFiles { for _, f := range desiredStateFiles {
a.logger.Debugf("Processing %s", f) a.logger.Debugf("Processing %s", f)
@ -706,8 +758,8 @@ func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*s
// render template, in two runs // render template, in two runs
r := &twoPassRenderer{ r := &twoPassRenderer{
reader: a.readFile, reader: a.readFile,
env: env, env: a.env,
namespace: namespace, namespace: a.namespace,
filename: f, filename: f,
logger: a.logger, logger: a.logger,
abs: a.abs, abs: a.abs,
@ -717,13 +769,13 @@ func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*s
return fmt.Errorf("error during %s parsing: %v", f, err) return fmt.Errorf("error during %s parsing: %v", f, err)
} }
st, noMatchInThisHelmfile, err := a.loadDesiredStateFromYaml( st, err := a.loadDesiredStateFromYaml(
yamlBuf.Bytes(), yamlBuf.Bytes(),
f, f,
namespace, a.namespace,
selectors, a.env,
env,
) )
helm := helmexec.New(a.logger, a.kubeContext) helm := helmexec.New(a.logger, a.kubeContext)
if err != nil { if err != nil {
@ -746,7 +798,7 @@ func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*s
if len(st.Helmfiles) > 0 { if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles { for _, m := range st.Helmfiles {
if err := a.FindAndIterateOverDesiredStates(m, converge, namespace, selectors, env); err != nil { if err := a.VisitDesiredStates(m, converge); err != nil {
switch err.(type) { switch err.(type) {
case *noMatchingHelmfileError: case *noMatchingHelmfileError:
@ -759,11 +811,9 @@ func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*s
} }
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
} else { } else {
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInThisHelmfile var processed bool
if noMatchInThisHelmfile { processed, errs = converge(st, helm)
continue noMatchInHelmfiles = noMatchInHelmfiles && !processed
}
errs = converge(st, helm)
} }
if err := clean(st, errs); err != nil { if err := clean(st, errs); err != nil {
@ -771,7 +821,40 @@ func (a *app) FindAndIterateOverDesiredStates(fileOrDir string, converge func(*s
} }
} }
if noMatchInHelmfiles { if noMatchInHelmfiles {
return &noMatchingHelmfileError{selectors, env} return &noMatchingHelmfileError{selectors: a.selectors, env: a.env}
}
return nil
}
func (a *app) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
selectors := a.selectors
err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(selectors) > 0 {
err := st.FilterReleases(selectors)
if err != nil {
return false, []error{err}
}
}
releaseNameCounts := map[string]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)}
}
}
errs := converge(st, helm)
processed := len(st.Releases) != 0 && len(errs) == 0
return processed, errs
})
if err != nil {
return err
} }
return nil return nil
} }
@ -833,11 +916,11 @@ func directoryExistsAt(path string) bool {
return err == nil && fileInfo.Mode().IsDir() return err == nil && fileInfo.Mode().IsDir()
} }
func (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, labels []string, env string) (*state.HelmState, bool, error) { func (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) {
c := state.NewCreator(a.logger, a.readFile, a.abs) c := state.NewCreator(a.logger, a.readFile, a.abs)
st, err := c.CreateFromYaml(yaml, file, env) st, err := c.CreateFromYaml(yaml, file, env)
if err != nil { if err != nil {
return nil, false, err return nil, err
} }
helmfiles := []string{} helmfiles := []string{}
@ -845,7 +928,7 @@ func (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
helmfileRelativePattern := st.JoinBase(globPattern) helmfileRelativePattern := st.JoinBase(globPattern)
matches, err := a.glob(helmfileRelativePattern) matches, err := a.glob(helmfileRelativePattern)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("failed processing %s: %v", globPattern, err) return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
} }
sort.Strings(matches) sort.Strings(matches)
@ -876,23 +959,6 @@ func (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
st.Namespace = namespace st.Namespace = namespace
} }
if len(labels) > 0 {
err = st.FilterReleases(labels)
if err != nil {
return nil, false, err
}
}
releaseNameCounts := map[string]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return nil, false, fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)
}
}
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
@ -902,7 +968,7 @@ func (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin
clean(st, errs) clean(st, errs)
}() }()
return st, len(st.Releases) == 0, nil return st, nil
} }
func clean(st *state.HelmState, errs []error) error { func clean(st *state.HelmState, errs []error) error {
@ -924,16 +990,7 @@ func clean(st *state.HelmState, errs []error) error {
fmt.Printf("err: %v\n", e) fmt.Printf("err: %v\n", e)
} }
} }
switch e := errs[0].(type) { return errs[0]
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
os.Exit(status.ExitStatus())
case *state.DiffError:
os.Exit(e.Code)
default:
os.Exit(1)
}
} }
return nil return nil
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -11,36 +10,6 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// See https://github.com/roboll/helmfile/issues/193
func TestReadFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
`)
app := &app{
readFile: ioutil.ReadFile,
glob: filepath.Glob,
abs: filepath.Abs,
kubeContext: "default",
logger: logger,
}
_, _, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", []string{}, "default")
if err == nil {
t.Error("error expected but not happened")
}
if err.Error() != "duplicate release \"myrelease1\" found: there were 2 releases named \"myrelease1\" matching specified selector" {
t.Errorf("unexpected error happened: %v", err)
}
}
func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer { func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer {
return &twoPassRenderer{ return &twoPassRenderer{
reader: readFile, reader: readFile,

View File

@ -788,7 +788,7 @@ func (state *HelmState) Clean() []error {
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile. // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
func (state *HelmState) FilterReleases(labels []string) error { func (state *HelmState) FilterReleases(labels []string) error {
var filteredReleases []ReleaseSpec var filteredReleases []ReleaseSpec
releaseSet := map[string]ReleaseSpec{} releaseSet := map[string][]ReleaseSpec{}
filters := []ReleaseFilter{} filters := []ReleaseFilter{}
for _, label := range labels { for _, label := range labels {
f, err := ParseLabels(label) f, err := ParseLabels(label)
@ -808,13 +808,13 @@ func (state *HelmState) FilterReleases(labels []string) error {
r.Labels = map[string]string{} r.Labels = map[string]string{}
} }
if f.Match(r) { if f.Match(r) {
releaseSet[r.Name] = r releaseSet[r.Name] = append(releaseSet[r.Name], r)
continue continue
} }
} }
} }
for _, r := range releaseSet { for _, r := range releaseSet {
filteredReleases = append(filteredReleases, r) filteredReleases = append(filteredReleases, r...)
} }
state.Releases = filteredReleases state.Releases = filteredReleases
numFound := len(filteredReleases) numFound := len(filteredReleases)