helmfile/main.go

584 lines
15 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
)
var Version string
func configure_logging(c *cli.Context) error {
// Valid levels:
// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
logLevel := c.GlobalString("log-level")
if c.GlobalBool("quiet") {
logLevel = "warn"
}
var level zapcore.Level
err := level.Set(logLevel)
if err != nil {
return err
}
logger := helmexec.NewLogger(os.Stdout, logLevel)
if c.App.Metadata == nil {
// Auto-initialised in 1.19.0
// https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19
c.App.Metadata = make(map[string]interface{})
}
c.App.Metadata["logger"] = logger
return nil
}
func main() {
app := cli.NewApp()
app.Name = "helmfile"
app.Usage = ""
app.Version = Version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "helm-binary, b",
Usage: "path to helm binary",
},
cli.StringFlag{
Name: "file, f",
Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "Silence output. Equivalent to log-level warn",
},
cli.StringFlag{
Name: "kube-context",
Usage: "Set kubectl context. Uses current context by default",
},
cli.StringFlag{
Name: "log-level",
Usage: "Set log level, default info",
},
cli.StringFlag{
Name: "namespace, n",
Usage: "Set namespace. Uses the namespace set in the context by default",
},
cli.StringSliceFlag{
Name: "selector, l",
Usage: `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease`,
},
}
app.Before = configure_logging
app.Commands = []cli.Command{
{
Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.SyncRepos(helm)
})
},
},
{
Name: "charts",
Usage: "sync releases from state file (helm upgrade --install)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
})
},
},
{
Name: "diff",
Usage: "diff releases from state file against env (helm diff)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.BoolFlag{
Name: "sync-repos",
Usage: "enable a repo sync prior to diffing",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
if c.Bool("sync-repos") {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.DiffReleases(helm, values, workers)
})
},
},
{
Name: "lint",
Usage: "lint charts from state file (helm lint)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.LintReleases(helm, values, workers)
})
},
},
{
Name: "sync",
Usage: "sync all resources from state file (repos, releases and chart deps)",
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
})
},
},
{
Name: "status",
Usage: "retrieve status of releases in state file",
Flags: []cli.Flag{
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
workers := c.Int("concurrency")
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.ReleaseStatuses(helm, workers)
})
},
},
{
Name: "delete",
Usage: "delete releases from state file (helm delete)",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "purge",
Usage: "purge releases i.e. free release names and histories",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
purge := c.Bool("purge")
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.DeleteReleases(helm, purge)
})
},
},
{
Name: "test",
Usage: "test releases from state file (helm test)",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "cleanup",
Usage: "delete test pods upon completion",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass additional args to helm exec",
},
cli.IntFlag{
Name: "timeout",
Value: 300,
Usage: "maximum time for tests to run before being considered failed",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
cleanup := c.Bool("cleanup")
timeout := c.Int("timeout")
args := getArgs(c, state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return state.TestReleases(helm, cleanup, timeout)
})
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Printf("err: %s", err.Error())
os.Exit(1)
}
}
func eachDesiredStateDo(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
fileOrDirPath := c.GlobalString("file")
desiredStateFiles, err := findDesiredStateFiles(fileOrDirPath)
if err != nil {
return err
}
allSelectorNotMatched := true
for _, f := range desiredStateFiles {
state, helm, empty, err := loadDesiredStateFromFile(c, f)
if err != nil {
return err
}
allSelectorNotMatched = allSelectorNotMatched && empty
if empty {
continue
}
errs := converge(state, helm)
if err := clean(state, errs); err != nil {
return err
}
}
if allSelectorNotMatched {
return fmt.Errorf("specified selector did not match any releases in any helmfile")
}
return nil
}
func findDesiredStateFiles(specifiedPath string) ([]string, error) {
var helmfileDir string
if specifiedPath != "" {
if fileExistsAt(specifiedPath) {
return []string{specifiedPath}, nil
} else if directoryExistsAt(specifiedPath) {
helmfileDir = specifiedPath
} else {
return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath)
}
} else {
var defaultFile string
if fileExistsAt(DefaultHelmfile) {
defaultFile = DefaultHelmfile
} else if fileExistsAt(DeprecatedHelmfile) {
log.Printf(
"warn: %s is being loaded: %s is deprecated in favor of %s. See https://github.com/roboll/helmfile/issues/25 for more information",
DeprecatedHelmfile,
DeprecatedHelmfile,
DefaultHelmfile,
)
defaultFile = DeprecatedHelmfile
}
if directoryExistsAt(DefaultHelmfileDirectory) {
if defaultFile != "" {
return []string{}, fmt.Errorf("configuration conlict error: you can have either %s or %s, but not both", defaultFile, DefaultHelmfileDirectory)
}
helmfileDir = DefaultHelmfileDirectory
} else if defaultFile != "" {
return []string{defaultFile}, nil
} else {
return []string{}, fmt.Errorf("no state file found. It must be named %s/*.yaml, %s, or %s, or otherwise specified with the --file flag", DefaultHelmfileDirectory, DefaultHelmfile, DeprecatedHelmfile)
}
}
files, err := filepath.Glob(filepath.Join(helmfileDir, "*.yaml"))
if err != nil {
return []string{}, err
}
sort.Slice(files, func(i, j int) bool {
return files[i] < files[j]
})
return files, nil
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
}
func directoryExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsDir()
}
func loadDesiredStateFromFile(c *cli.Context, file string) (*state.HelmState, helmexec.Interface, bool, error) {
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
labels := c.GlobalStringSlice("selector")
st, err := state.ReadFromFile(file)
if err != nil {
return nil, nil, false, fmt.Errorf("failed to read %s: %v", file, err)
}
if st.Context != "" {
if kubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute context.")
os.Exit(1)
}
kubeContext = st.Context
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
if len(labels) > 0 {
err = st.FilterReleases(labels)
if err != nil {
log.Print(err)
return nil, nil, true, nil
}
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
clean(st, errs)
}()
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
return st, helmexec.New(logger, kubeContext), false, nil
}
func clean(state *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := state.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
if errs != nil && len(errs) > 0 {
for _, err := range errs {
fmt.Printf("err: %s\n", err.Error())
}
os.Exit(1)
}
return nil
}
func getArgs(c *cli.Context, state *state.HelmState) []string {
args := c.String("args")
argsMap := map[string]string{}
if len(args) > 0 {
argsVals := strings.Split(args, " ")
for _, arg := range argsVals {
argVal := strings.SplitN(arg, "=", 2)
if len(argVal) > 1 {
arg := argVal[0]
value := argVal[1]
argsMap[arg] = value
} else {
arg := argVal[0]
argsMap[arg] = ""
}
}
}
if len(state.HelmDefaults.Args) > 0 {
for _, arg := range state.HelmDefaults.Args {
argVal := strings.SplitN(arg, "=", 2)
arg := argVal[0]
if _, exists := argsMap[arg]; !exists {
if len(argVal) > 1 {
argsMap[arg] = argVal[1]
} else {
argsMap[arg] = ""
}
}
}
}
if state.HelmDefaults.TillerNamespace != "" {
setDefaultValue(argsMap, "--tiller-namespace", state.HelmDefaults.TillerNamespace)
}
if state.HelmDefaults.KubeContext != "" {
setDefaultValue(argsMap, "--kube-context", state.HelmDefaults.KubeContext)
}
var argArr []string
for key, val := range argsMap {
if val != "" {
argArr = append(argArr, fmt.Sprintf("%s=%s", key, val))
} else {
argArr = append(argArr, fmt.Sprintf("%s", key))
}
}
state.HelmDefaults.Args = argArr
return state.HelmDefaults.Args
}
func setDefaultValue(argsMap map[string]string, flag string, value string) {
if _, exists := argsMap[flag]; !exists {
argsMap[flag] = value
}
}