From 1b302db7f81029ef6629c3070e9c765043f0f64e Mon Sep 17 00:00:00 2001 From: Cedric Meury Date: Fri, 30 Mar 2018 10:27:43 +0200 Subject: [PATCH] running update dependencies for local charts --- README.md | 7 +- helmexec/exec.go | 8 +- helmexec/exec_test.go | 18 +++++ helmexec/helmexec.go | 2 +- main.go | 15 +++- state/state.go | 27 ++++++- state/state_test.go | 181 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 246 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4f8b71f6..7521c295 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ COMMANDS: repos sync repositories from state file (helm repo add && helm repo update) charts sync charts from state file (helm upgrade --install) diff diff charts from state file against env (helm diff) - sync sync all resources from state file (repos && charts) + sync sync all resources from state file (repos, charts and local chart deps) delete delete charts from state file (helm delete) GLOBAL OPTIONS: @@ -119,9 +119,10 @@ GLOBAL OPTIONS: ### sync -The `helmfile sync` sub-command sync your cluster state as desired in your `helmfile`. The default helmfile is `helmfile.yaml`, but any yaml file can be passed by specifying a `--file path/to/your/yaml/file` flag. +The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any yaml file can be passed by specifying a `--file path/to/your/yaml/file` flag. -Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. +Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. It also updates specified chart repositories and updates the +dependencies of any referenced local charts. ### diff diff --git a/helmexec/exec.go b/helmexec/exec.go index a73c44bf..f268cd61 100644 --- a/helmexec/exec.go +++ b/helmexec/exec.go @@ -46,6 +46,12 @@ func (helm *execer) UpdateRepo() error { return err } +func (helm *execer) UpdateDeps(chart string) error { + out, err := helm.exec("dependency", "update", chart) + helm.write(out) + return err +} + func (helm *execer) SyncRelease(name, chart string, flags ...string) error { out, err := helm.exec(append([]string{"upgrade", "--install", name, chart}, flags...)...) helm.write(out) @@ -86,4 +92,4 @@ func (helm *execer) write(out []byte) { if helm.writer != nil { helm.writer.Write(out) } -} \ No newline at end of file +} diff --git a/helmexec/exec_test.go b/helmexec/exec_test.go index c7d3f13a..45476834 100644 --- a/helmexec/exec_test.go +++ b/helmexec/exec_test.go @@ -100,6 +100,24 @@ func Test_SyncRelease(t *testing.T) { } } +func Test_UpdateDeps(t *testing.T) { + var buffer bytes.Buffer + helm := MockExecer(&buffer, "dev") + helm.UpdateDeps("./chart/foo") + expected := "exec: helm dependency update ./chart/foo --kube-context dev\n" + if buffer.String() != expected { + t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected) + } + + buffer.Reset() + helm.SetExtraArgs("--verify") + helm.UpdateDeps("./chart/foo") + expected = "exec: helm dependency update ./chart/foo --verify --kube-context dev\n" + if buffer.String() != expected { + t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected) + } +} + func Test_DecryptSecret(t *testing.T) { var buffer bytes.Buffer helm := MockExecer(&buffer, "dev") diff --git a/helmexec/helmexec.go b/helmexec/helmexec.go index 2786c67b..1de1d726 100644 --- a/helmexec/helmexec.go +++ b/helmexec/helmexec.go @@ -5,7 +5,7 @@ type Interface interface { AddRepo(name, repository, certfile, keyfile string) error UpdateRepo() error - + UpdateDeps(chart string) error SyncRelease(name, chart string, flags ...string) error DiffRelease(name, chart string, flags ...string) error DeleteRelease(name string) error diff --git a/main.go b/main.go index 5d036d0d..a3ec5c26 100644 --- a/main.go +++ b/main.go @@ -163,7 +163,7 @@ func main() { }, { Name: "sync", - Usage: "sync all resources from state file (repos && charts)", + Usage: "sync all resources from state file (repos, charts and local chart deps)", Flags: []cli.Flag{ cli.StringSliceFlag{ Name: "values", @@ -193,14 +193,21 @@ func main() { os.Exit(1) } - values := c.StringSlice("values") - workers := c.Int("concurrency") - args := c.String("args") if len(args) > 0 { helm.SetExtraArgs(strings.Split(args, " ")...) } + if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s\n", err.Error()) + } + os.Exit(1) + } + + values := c.StringSlice("values") + workers := c.Int("concurrency") + errs := state.SyncReleases(helm, values, workers) return clean(state, errs) }, diff --git a/state/state.go b/state/state.go index 2aea6181..8e07f535 100644 --- a/state/state.go +++ b/state/state.go @@ -184,7 +184,8 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ continue } - if err := helm.SyncRelease(release.Name, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil { + chart := normalizeChart(state.BaseChartPath, release.Chart) + if err := helm.SyncRelease(release.Name, chart, flags...); err != nil { errQueue <- err } doneQueue <- true @@ -331,18 +332,38 @@ func (state *HelmState) FilterReleases(labels []string) error { return nil } +func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error { + errs := []error{} + + for _, release := range state.Releases { + if isLocalChart(release.Chart) { + if err := helm.UpdateDeps(normalizeChart(state.BaseChartPath, release.Chart)); err != nil { + errs = append(errs, err) + } + } + } + if len(errs) != 0 { + return errs + } + return nil +} + // normalizeChart allows for the distinction between a file path reference and repository references. // - Any single (or double character) followed by a `/` will be considered a local file reference and // be constructed relative to the `base path`. // - Everything else is assumed to be an absolute path or an actual / reference. func normalizeChart(basePath, chart string) string { - regex, _ := regexp.Compile("^[.]?./") - if !regex.MatchString(chart) { + if !isLocalChart(chart) { return chart } return filepath.Join(basePath, chart) } +func isLocalChart(chart string) bool { + regex, _ := regexp.Compile("^[.]?./") + return regex.MatchString(chart) +} + func flagsForRelease(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) { flags := []string{} if release.Version != "" { diff --git a/state/state_test.go b/state/state_test.go index 5238a52d..a44b1965 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -3,6 +3,10 @@ package state import ( "reflect" "testing" + + "errors" + "fmt" + "strings" ) func TestReadFromYaml(t *testing.T) { @@ -218,3 +222,180 @@ func TestHelmState_applyDefaultsTo(t *testing.T) { }) } } + +func Test_isLocalChart(t *testing.T) { + type args struct { + chart string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "local chart", + args: args{ + chart: "./charts/nonstop", + }, + want: true, + }, + { + name: "repo chart", + args: args{ + chart: "stable/genius", + }, + want: false, + }, + { + name: "empty", + args: args{ + chart: "", + }, + want: false, + }, + { + name: "parent local path", + args: args{ + chart: "../../dotty", + }, + want: true, + }, + { + name: "parent-parent local path", + args: args{ + chart: "../../dotty", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isLocalChart(tt.args.chart); got != tt.want { + t.Errorf("isLocalChart() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_normalizeChart(t *testing.T) { + type args struct { + basePath string + chart string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "construct local chart path", + args: args{ + basePath: "/Users/jane/code/deploy/charts", + chart: "./app", + }, + want: "/Users/jane/code/deploy/charts/app", + }, + { + name: "repo path", + args: args{ + basePath: "/Users/jane/code/deploy/charts", + chart: "remote/app", + }, + want: "remote/app", + }, + { + name: "construct local chart path, parent dir", + args: args{ + basePath: "/Users/jane/code/deploy/charts", + chart: "../app", + }, + want: "/Users/jane/code/deploy/app", + }, + { + name: "too much parent levels", + args: args{ + basePath: "/src", + chart: "../../app", + }, + want: "/app", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normalizeChart(tt.args.basePath, tt.args.chart); got != tt.want { + t.Errorf("normalizeChart() = %v, want %v", got, tt.want) + } + }) + } +} + +// mocking helmexec.Interface + +type mockHelmExec struct { + charts []string +} + +func (helm *mockHelmExec) UpdateDeps(chart string) error { + fmt.Println(chart) + if strings.Contains(chart, "error") { + return errors.New("error") + } + helm.charts = append(helm.charts, chart) + return nil +} +func (helm *mockHelmExec) SetExtraArgs(args ...string) { + return +} +func (helm *mockHelmExec) AddRepo(name, repository, certfile, keyfile string) error { + return nil +} +func (helm *mockHelmExec) UpdateRepo() error { + return nil +} +func (helm *mockHelmExec) SyncRelease(name, chart string, flags ...string) error { + return nil +} +func (helm *mockHelmExec) DiffRelease(name, chart string, flags ...string) error { + return nil +} +func (helm *mockHelmExec) DeleteRelease(name string) error { + return nil +} +func (helm *mockHelmExec) DecryptSecret(name string) (string, error) { + return "", nil +} + +func TestHelmState_UpdateDeps(t *testing.T) { + state := &HelmState{ + BaseChartPath: "/src", + Releases: []ReleaseSpec{ + { + Chart: "./local", + }, + { + Chart: "../local", + }, + { + Chart: "../../local", + }, + { + Chart: "published", + }, + { + Chart: "published/deeper", + }, + { + Chart: "./error", + }, + }, + } + want := []string{"/src/local", "/local", "/local"} + helm := &mockHelmExec{} + errs := state.UpdateDeps(helm) + if !reflect.DeepEqual(helm.charts, want) { + t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.charts, want) + } + if len(errs) != 1 { + t.Errorf("HelmState.UpdateDeps() - expected an error, but got: %v", len(errs)) + } +}