running update dependencies for local charts

This commit is contained in:
Cedric Meury 2018-03-30 10:27:43 +02:00
parent cef2d26c06
commit 1b302db7f8
7 changed files with 246 additions and 12 deletions

View File

@ -101,7 +101,7 @@ COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update) repos sync repositories from state file (helm repo add && helm repo update)
charts sync charts from state file (helm upgrade --install) charts sync charts from state file (helm upgrade --install)
diff diff charts from state file against env (helm diff) 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) delete delete charts from state file (helm delete)
GLOBAL OPTIONS: GLOBAL OPTIONS:
@ -119,9 +119,10 @@ GLOBAL OPTIONS:
### sync ### 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

View File

@ -46,6 +46,12 @@ func (helm *execer) UpdateRepo() error {
return err 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 { func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
out, err := helm.exec(append([]string{"upgrade", "--install", name, chart}, flags...)...) out, err := helm.exec(append([]string{"upgrade", "--install", name, chart}, flags...)...)
helm.write(out) helm.write(out)
@ -86,4 +92,4 @@ func (helm *execer) write(out []byte) {
if helm.writer != nil { if helm.writer != nil {
helm.writer.Write(out) helm.writer.Write(out)
} }
} }

View File

@ -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) { func Test_DecryptSecret(t *testing.T) {
var buffer bytes.Buffer var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev") helm := MockExecer(&buffer, "dev")

View File

@ -5,7 +5,7 @@ type Interface interface {
AddRepo(name, repository, certfile, keyfile string) error AddRepo(name, repository, certfile, keyfile string) error
UpdateRepo() error UpdateRepo() error
UpdateDeps(chart string) error
SyncRelease(name, chart string, flags ...string) error SyncRelease(name, chart string, flags ...string) error
DiffRelease(name, chart string, flags ...string) error DiffRelease(name, chart string, flags ...string) error
DeleteRelease(name string) error DeleteRelease(name string) error

15
main.go
View File

@ -163,7 +163,7 @@ func main() {
}, },
{ {
Name: "sync", 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{ Flags: []cli.Flag{
cli.StringSliceFlag{ cli.StringSliceFlag{
Name: "values", Name: "values",
@ -193,14 +193,21 @@ func main() {
os.Exit(1) os.Exit(1)
} }
values := c.StringSlice("values")
workers := c.Int("concurrency")
args := c.String("args") args := c.String("args")
if len(args) > 0 { if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...) 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) errs := state.SyncReleases(helm, values, workers)
return clean(state, errs) return clean(state, errs)
}, },

View File

@ -184,7 +184,8 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
continue 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 errQueue <- err
} }
doneQueue <- true doneQueue <- true
@ -331,18 +332,38 @@ func (state *HelmState) FilterReleases(labels []string) error {
return nil 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. // 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 // - Any single (or double character) followed by a `/` will be considered a local file reference and
// be constructed relative to the `base path`. // be constructed relative to the `base path`.
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference. // - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
func normalizeChart(basePath, chart string) string { func normalizeChart(basePath, chart string) string {
regex, _ := regexp.Compile("^[.]?./") if !isLocalChart(chart) {
if !regex.MatchString(chart) {
return chart return chart
} }
return filepath.Join(basePath, 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) { func flagsForRelease(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
flags := []string{} flags := []string{}
if release.Version != "" { if release.Version != "" {

View File

@ -3,6 +3,10 @@ package state
import ( import (
"reflect" "reflect"
"testing" "testing"
"errors"
"fmt"
"strings"
) )
func TestReadFromYaml(t *testing.T) { 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))
}
}