From e5e834f9f08de07aa36c995cd8fbf949e4aaab8f Mon Sep 17 00:00:00 2001 From: rob boll Date: Tue, 22 Nov 2016 12:36:49 -0500 Subject: [PATCH] initial helmfile impl --- helmexec/exec.go | 79 +++++++++++++++++++++ helmexec/helmexec.go | 11 +++ main.go | 164 +++++++++++++++++++++++++++++++++++++++++++ state/state.go | 156 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 helmexec/exec.go create mode 100644 helmexec/helmexec.go create mode 100644 main.go create mode 100644 state/state.go diff --git a/helmexec/exec.go b/helmexec/exec.go new file mode 100644 index 00000000..c55c41ba --- /dev/null +++ b/helmexec/exec.go @@ -0,0 +1,79 @@ +package helmexec + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +const ( + command = "helm" +) + +type execer struct { + writer io.Writer + extra []string +} + +func NewHelmExec(writer io.Writer) Interface { + return &execer{writer: writer} +} + +func (helm *execer) SetExtraArgs(args ...string) { + helm.extra = args +} + +func (helm *execer) AddRepo(name, repository string) error { + out, err := helm.exec("repo", "add", name, repository) + if helm.writer != nil { + helm.writer.Write(out) + } + return err +} + +func (helm *execer) UpdateRepo() error { + out, err := helm.exec("repo", "update") + if helm.writer != nil { + helm.writer.Write(out) + } + return err +} + +func (helm *execer) SyncChart(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) + } + return err +} + +func (helm *execer) DeleteChart(name string) error { + out, err := helm.exec("delete", name) + if helm.writer != nil { + helm.writer.Write(out) + } + return err +} + +func (helm *execer) exec(args ...string) ([]byte, error) { + dir, err := ioutil.TempDir("", "helmfile-exec") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + + cmdargs := args + if len(helm.extra) > 0 { + cmdargs = append(cmdargs, helm.extra...) + } + if helm.writer != nil { + helm.writer.Write([]byte(fmt.Sprintf("exec: helm %s\n", strings.Join(cmdargs, " ")))) + } + + cmd := exec.Command(command, args...) + cmd.Dir = dir + return cmd.CombinedOutput() +} diff --git a/helmexec/helmexec.go b/helmexec/helmexec.go new file mode 100644 index 00000000..72150663 --- /dev/null +++ b/helmexec/helmexec.go @@ -0,0 +1,11 @@ +package helmexec + +type Interface interface { + SetExtraArgs(args ...string) + + AddRepo(name, repository string) error + UpdateRepo() error + + SyncChart(name, chart string, flags ...string) error + DeleteChart(name string) error +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..c9a4a1f9 --- /dev/null +++ b/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/roboll/helmfile/helmexec" + "github.com/roboll/helmfile/state" + "github.com/urfave/cli" +) + +const ( + helmfile = "charts.yaml" +) + +func main() { + + app := cli.NewApp() + app.Name = "helmfile" + app.Usage = "" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Value: helmfile, + Usage: "load config from `FILE`", + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "silence output", + }, + } + + app.Commands = []cli.Command{ + { + Name: "repos", + Usage: "sync repositories from state file", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm exec", + }, + }, + Action: func(c *cli.Context) error { + state, helm, err := before(c) + if err != nil { + return err + } + + args := c.String("args") + if len(args) > 0 { + helm.SetExtraArgs(strings.Split(args, " ")...) + } + + if errs := state.SyncRepos(helm); err != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s", err.Error()) + } + os.Exit(1) + } + return nil + }, + }, + { + Name: "charts", + Usage: "sync charts from state file", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm exec", + }, + }, + Action: func(c *cli.Context) error { + state, helm, err := before(c) + if err != nil { + return err + } + + args := c.String("args") + if len(args) > 0 { + helm.SetExtraArgs(strings.Split(args, " ")...) + } + + if errs := state.SyncCharts(helm); err != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s", err.Error()) + } + os.Exit(1) + } + return nil + }, + }, + { + Name: "sync", + Usage: "sync all resources from state file", + Action: func(c *cli.Context) error { + state, helm, err := before(c) + if err != nil { + return err + } + + if errs := state.SyncRepos(helm); err != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s", err.Error()) + } + os.Exit(1) + } + + if errs := state.SyncCharts(helm); err != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s", err.Error()) + } + os.Exit(1) + } + return nil + }, + }, + { + Name: "delete", + Usage: "delete charts from state file", + Action: func(c *cli.Context) error { + state, helm, err := before(c) + if err != nil { + return err + } + + if errs := state.DeleteCharts(helm); err != nil && len(errs) > 0 { + for _, err := range errs { + fmt.Printf("err: %s", err.Error()) + } + os.Exit(1) + } + return nil + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Printf("err: %s", err.Error()) + os.Exit(1) + } +} + +func before(c *cli.Context) (*state.HelmState, helmexec.Interface, error) { + file := c.GlobalString("file") + quiet := c.GlobalBool("quiet") + + state, err := state.ReadFromFile(file) + if err != nil { + return nil, nil, err + } + + var writer io.Writer + if !quiet { + writer = os.Stdout + } + + return state, helmexec.NewHelmExec(writer), nil +} diff --git a/state/state.go b/state/state.go new file mode 100644 index 00000000..b3b2be6a --- /dev/null +++ b/state/state.go @@ -0,0 +1,156 @@ +package state + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/roboll/helmfile/helmexec" + + yaml "gopkg.in/yaml.v1" +) + +type HelmState struct { + Repositories []RepositorySpec `yaml:"repositories"` + Charts []ChartSpec `yaml:"charts"` +} + +type RepositorySpec struct { + Name string `yaml:"name"` + URL string `yaml:"url"` +} + +type ChartSpec struct { + Chart string `yaml:"chart"` + Version string `yaml:"version"` + Verify bool `yaml:"verify"` + + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + Values []string `yaml:"values"` + SetValues []SetValue `yaml:"set"` +} + +type SetValue struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +func ReadFromFile(file string) (*HelmState, error) { + content, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + var state HelmState + if err := yaml.Unmarshal(content, &state); err != nil { + return nil, err + } + return &state, nil +} + +func (state *HelmState) SyncRepos(helm helmexec.Interface) []error { + var wg sync.WaitGroup + errs := []error{} + + for _, repo := range state.Repositories { + wg.Add(1) + go func(wg *sync.WaitGroup) { + if err := helm.AddRepo(repo.Name, repo.URL); err != nil { + errs = append(errs, err) + } + wg.Done() + }(&wg) + } + wg.Wait() + + if len(errs) != 0 { + return errs + } + + if err := helm.UpdateRepo(); err != nil { + return []error{err} + } + return nil +} + +func (state *HelmState) SyncCharts(helm helmexec.Interface) []error { + var wg sync.WaitGroup + errs := []error{} + + for _, chart := range state.Charts { + wg.Add(1) + go func(wg *sync.WaitGroup, chart ChartSpec) { + flags, err := flagsForChart(&chart) + if err != nil { + errs = append(errs, err) + } else { + if err := helm.SyncChart(chart.Name, chart.Chart, flags...); err != nil { + errs = append(errs, err) + } + } + wg.Done() + }(&wg, chart) + } + wg.Wait() + + if len(errs) != 0 { + return errs + } + + return nil +} + +func (state *HelmState) DeleteCharts(helm helmexec.Interface) []error { + var wg sync.WaitGroup + errs := []error{} + + for _, chart := range state.Charts { + wg.Add(1) + go func(wg *sync.WaitGroup, chart ChartSpec) { + if err := helm.DeleteChart(chart.Name); err != nil { + errs = append(errs, err) + } + wg.Done() + }(&wg, chart) + } + wg.Wait() + + if len(errs) != 0 { + return errs + } + + return nil +} + +func flagsForChart(chart *ChartSpec) ([]string, error) { + flags := []string{} + if chart.Version != "" { + flags = append(flags, "--version", chart.Version) + } + if chart.Verify { + flags = append(flags, "--verify") + } + if chart.Namespace != "" { + flags = append(flags, "--namespace", chart.Namespace) + } + for _, value := range chart.Values { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + valfile := filepath.Join(wd, value) + flags = append(flags, "--values", valfile) + } + if len(chart.SetValues) > 0 { + val := []string{} + for _, set := range chart.SetValues { + val = append(val, fmt.Sprintf("%s=%s", set.Name, set.Value)) + } + flags = append(flags, "--set", strings.Join(val, ",")) + } + return flags, nil +}