initial helmfile impl

This commit is contained in:
rob boll 2016-11-22 12:36:49 -05:00
commit e5e834f9f0
4 changed files with 410 additions and 0 deletions

79
helmexec/exec.go Normal file
View File

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

11
helmexec/helmexec.go Normal file
View File

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

164
main.go Normal file
View File

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

156
state/state.go Normal file
View File

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