Merge pull request #77 from cmeury/sync-update-deps

Update dependencies of local charts when running 'sync'
This commit is contained in:
KUOKA Yusuke 2018-04-02 18:29:11 +09:00 committed by GitHub
commit 1af8743bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 32 deletions

View File

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

View File

@ -36,49 +36,43 @@ func (helm *execer) AddRepo(name, repository, certfile, keyfile string) error {
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
}
out, err := helm.exec(args...)
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
return err
}
func (helm *execer) UpdateRepo() error {
out, err := helm.exec("repo", "update")
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
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...)...)
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
return err
}
func (helm *execer) DecryptSecret(name string) (string, error) {
out, err := helm.exec(append([]string{"secrets", "dec", name})...)
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
return name + ".dec", err
}
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
out, err := helm.exec(append([]string{"diff", name, chart}, flags...)...)
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
return err
}
func (helm *execer) DeleteRelease(name string) error {
out, err := helm.exec("delete", "--purge", name)
if helm.writer != nil {
helm.writer.Write(out)
}
helm.write(out)
return err
}
@ -90,8 +84,12 @@ func (helm *execer) exec(args ...string) ([]byte, error) {
if helm.kubeContext != "" {
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
}
if helm.writer != nil {
helm.writer.Write([]byte(fmt.Sprintf("exec: helm %s\n", strings.Join(cmdargs, " "))))
}
helm.write([]byte(fmt.Sprintf("exec: helm %s\n", strings.Join(cmdargs, " "))))
return helm.runner.Execute(command, cmdargs)
}
func (helm *execer) write(out []byte) {
if helm.writer != nil {
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) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")

View File

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

15
main.go
View File

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

View File

@ -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 <repository>/<chart> 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 != "" {

View File

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