feat: Opt-out dep-builds and repo-updates (#463)

* Improve code organization

To make sure it is still readable after upcoming changes to helmfile

* feat: `helmfile deps` to update dependencies of all the local charts

Resolves #450

* feat: helmfile updates repos and build deps by default

But not update deps. Use `helmfile deps` to update deps, and provide `--skip-deps` to skip updating repos and builds deps in sync/diff/apply/template

Resolves #415

* Improve integration test coverage
This commit is contained in:
KUOKA Yusuke 2019-02-04 12:07:25 +09:00 committed by GitHub
parent b894012c80
commit c6236a15bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 832 additions and 653 deletions

100
cmd/cmd.go Normal file
View File

@ -0,0 +1,100 @@
package cmd
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"os/exec"
"strings"
"syscall"
)
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
a, fileOrDir, err := InitAppEntry(c, false)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStates(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
app := app.Init(&app.App{
KubeContext: kubeContext,
Logger: logger,
Reverse: reverse,
Env: env,
Namespace: namespace,
Selectors: selectors,
})
return app, fileOrDir, nil
}
func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
a, fileOrDir, err := InitAppEntry(c, reverse)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
return cli.NewExitError(e.Error(), status.ExitStatus())
case *state.DiffError:
return cli.NewExitError(e.Error(), e.Code)
default:
return cli.NewExitError(e.Error(), 1)
}
}
return err
}

37
cmd/deps.go Normal file
View File

@ -0,0 +1,37 @@
package cmd
import (
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
)
func Deps(a *app.App) cli.Command {
return cli.Command{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
errs := state.UpdateDeps(helm)
ok := len(errs) == 0
return ok, errs
})
},
}
}

View File

@ -89,6 +89,13 @@ func (helm *execer) UpdateDeps(chart string) error {
return err return err
} }
func (helm *execer) BuildDeps(chart string) error {
helm.logger.Infof("Building dependency %v", chart)
out, err := helm.exec("dependency", "build", 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 {
helm.logger.Infof("Upgrading %v", chart) helm.logger.Infof("Upgrading %v", chart)
out, err := helm.exec(append([]string{"upgrade", "--install", "--reset-values", name, chart}, flags...)...) out, err := helm.exec(append([]string{"upgrade", "--install", "--reset-values", name, chart}, flags...)...)

View File

@ -159,6 +159,29 @@ exec: helm dependency update ./chart/foo --verify --kube-context dev
} }
} }
func Test_BuildDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.BuildDeps("./chart/foo")
expected := `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.BuildDeps("./chart/foo")
expected = `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --verify --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\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
logger := NewLogger(&buffer, "debug") logger := NewLogger(&buffer, "debug")

View File

@ -7,6 +7,7 @@ type Interface interface {
AddRepo(name, repository, certfile, keyfile, username, password string) error AddRepo(name, repository, certfile, keyfile, username, password string) error
UpdateRepo() error UpdateRepo() error
BuildDeps(chart string) error
UpdateDeps(chart string) 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

673
main.go
View File

@ -1,32 +1,17 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"github.com/roboll/helmfile/args" "github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/cmd"
"github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state" "github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"github.com/urfave/cli" "github.com/urfave/cli"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) "os"
"strings"
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
) )
var Version string var Version string
@ -57,11 +42,11 @@ func configureLogging(c *cli.Context) error {
func main() { func main() {
app := cli.NewApp() cliApp := cli.NewApp()
app.Name = "helmfile" cliApp.Name = "helmfile"
app.Usage = "" cliApp.Usage = ""
app.Version = Version cliApp.Version = Version
app.Flags = []cli.Flag{ cliApp.Flags = []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "helm-binary, b", Name: "helm-binary, b",
Usage: "path to helm binary", Usage: "path to helm binary",
@ -103,8 +88,8 @@ func main() {
}, },
} }
app.Before = configureLogging cliApp.Before = configureLogging
app.Commands = []cli.Command{ cliApp.Commands = []cli.Command{
{ {
Name: "repos", Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)", Usage: "sync repositories from state file (helm repo add && helm repo update)",
@ -116,7 +101,7 @@ func main() {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return visitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx context) (bool, []error) { return cmd.VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state) args := args.GetArgs(c.String("args"), state)
if len(args) > 0 { if len(args) > 0 {
helm.SetExtraArgs(args...) helm.SetExtraArgs(args...)
@ -150,7 +135,7 @@ func main() {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
return executeSyncCommand(c, state, helm) return executeSyncCommand(c, state, helm)
}) })
}, },
@ -173,8 +158,8 @@ func main() {
Usage: "DEPRECATED", Usage: "DEPRECATED",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "skip-repo-update", Name: "skip-deps",
Usage: "skip running `helm repo update` on repositories declared in helmfile", Usage: "skip running `helm repo update` and `helm dependency build`",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "detailed-exitcode", Name: "detailed-exitcode",
@ -191,14 +176,17 @@ func main() {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-repo-update") { if !c.Bool("skip-deps") {
if c.Bool("sync-repos") { if c.Bool("sync-repos") {
logger.Warnf("--sync-repos has been removed and `helmfile diff` updates repositories by default. Provide `--skip-repo-update` to opt-out.") logger.Warnf("--sync-repos has been removed and `helmfile diff` updates repositories by default. Provide `--skip-repo-update` to opt-out.")
} }
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
} }
if errs := state.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 { if errs := state.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 {
return errs return errs
@ -227,20 +215,24 @@ func main() {
Value: 0, Value: 0,
Usage: "maximum number of concurrent downloads of release charts", Usage: "maximum number of concurrent downloads of release charts",
}, },
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if errs := state.PrepareRelease(helm, "template"); errs != nil && len(errs) > 0 { if !c.Bool("skip-deps") {
return errs
}
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { return errs
}
}
if errs := state.PrepareRelease(helm, "template"); errs != nil && len(errs) > 0 {
return errs return errs
} }
return executeTemplateCommand(c, state, helm) return executeTemplateCommand(c, state, helm)
}) })
}, },
@ -265,7 +257,7 @@ func main() {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
values := c.StringSlice("values") values := c.StringSlice("values")
args := args.GetArgs(c.String("args"), state) args := args.GetArgs(c.String("args"), state)
workers := c.Int("concurrency") workers := c.Int("concurrency")
@ -297,16 +289,22 @@ func main() {
Value: "", Value: "",
Usage: "pass args to helm exec", Usage: "pass args to helm exec",
}, },
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := state.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 { if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { }
if errs := state.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 {
return errs return errs
} }
return executeSyncCommand(c, state, helm) return executeSyncCommand(c, state, helm)
@ -339,20 +337,27 @@ func main() {
Name: "skip-repo-update", Name: "skip-repo-update",
Usage: "skip running `helm repo update` on repositories declared in helmfile", Usage: "skip running `helm repo update` on repositories declared in helmfile",
}, },
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") || !c.Bool("skip-repo-update") {
if !c.Bool("skip-repo-update") { if !c.Bool("skip-repo-update") {
logger.Warn("--skip-repo-update has been deprecated. Provide --skip-deps instead.")
}
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
} }
if errs := st.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 { if errs := st.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 {
return errs return errs
} }
if errs := st.UpdateDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets")) releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets"))
@ -430,7 +435,7 @@ Do you really want to apply?
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
workers := c.Int("concurrency") workers := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state) args := args.GetArgs(c.String("args"), state)
@ -457,7 +462,7 @@ Do you really want to apply?
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ context) []error { return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
purge := c.Bool("purge") purge := c.Bool("purge")
args := args.GetArgs(c.String("args"), state) args := args.GetArgs(c.String("args"), state)
@ -510,7 +515,7 @@ Do you really want to delete?
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ context) []error { return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
cleanup := c.Bool("cleanup") cleanup := c.Bool("cleanup")
timeout := c.Int("timeout") timeout := c.Int("timeout")
concurrency := c.Int("concurrency") concurrency := c.Int("concurrency")
@ -526,7 +531,7 @@ Do you really want to delete?
}, },
} }
err := app.Run(os.Args) err := cliApp.Run(os.Args)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(3) os.Exit(3)
@ -566,568 +571,6 @@ func executeDiffCommand(c *cli.Context, st *state.HelmState, helm helmexec.Inter
return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets, triggerCleanupEvents) return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets, triggerCleanupEvents)
} }
type app struct { func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
kubeContext string return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge)
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
glob func(string) ([]string, error)
abs func(string) (string, error)
fileExistsAt func(string) bool
directoryExistsAt func(string) bool
reverse bool
env string
namespace string
selectors []string
getwd func() (string, error)
chdir func(string) error
}
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, context) []error) error {
return findAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge)
}
func initAppEntry(c *cli.Context, reverse bool) (*app, string, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
app := &app{
readFile: ioutil.ReadFile,
glob: filepath.Glob,
abs: filepath.Abs,
getwd: os.Getwd,
chdir: os.Chdir,
fileExistsAt: fileExistsAt,
directoryExistsAt: directoryExistsAt,
kubeContext: kubeContext,
logger: logger,
reverse: reverse,
env: env,
namespace: namespace,
selectors: selectors,
}
return app, fileOrDir, nil
}
type context struct {
updatedRepos map[string]struct{}
}
func (ctx context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
var errs []error
allUpdated := true
for _, r := range st.Repositories {
_, exists := ctx.updatedRepos[r.Name]
allUpdated = allUpdated && exists
}
if !allUpdated {
errs = st.SyncRepos(helm)
for _, r := range st.Repositories {
ctx.updatedRepos[r.Name] = struct{}{}
}
}
return errs
}
func visitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, context) (bool, []error)) error {
app, fileOrDir, err := initAppEntry(c, false)
if err != nil {
return err
}
ctx := context{
updatedRepos: map[string]struct{}{},
}
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = app.VisitDesiredStates(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
switch e := err.(type) {
case *noMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
return cli.NewExitError(e.Error(), status.ExitStatus())
case *state.DiffError:
return cli.NewExitError(e.Error(), e.Code)
default:
return cli.NewExitError(e.Error(), 1)
}
}
return err
}
func findAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, context) []error) error {
app, fileOrDir, err := initAppEntry(c, reverse)
if err != nil {
return err
}
ctx := context{
updatedRepos: map[string]struct{}{},
}
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = app.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
type noMatchingHelmfileError struct {
selectors []string
env string
}
func (e *noMatchingHelmfileError) Error() string {
return fmt.Sprintf(
"err: no releases found that matches specified selector(%s) and environment(%s), in any helmfile",
strings.Join(e.selectors, ", "),
e.env,
)
}
func prependLineNumbers(text string) string {
buf := bytes.NewBufferString("")
lines := strings.Split(text, "\n")
for i, line := range lines {
buf.WriteString(fmt.Sprintf("%2d: %s\n", i, line))
}
return buf.String()
}
type twoPassRenderer struct {
reader func(string) ([]byte, error)
env string
namespace string
filename string
logger *zap.SugaredLogger
abs func(string) (string, error)
}
func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environment {
firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), tmplData)
// parse as much as we can, tolerate errors, this is a preparse
yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil && r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
if yamlBuf == nil { // we have a template syntax error, let the second parse report
r.logger.Debugf("template syntax error: %v", err)
return firstPassEnv
}
}
c := state.NewCreator(r.logger, r.reader, r.abs)
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
}
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
}
if prestate != nil {
firstPassEnv = prestate.Env
}
return firstPassEnv
}
func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content)
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData)
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil {
if r.logger != nil {
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}
func (a *app) within(dir string, do func() error) error {
if dir == "." {
return do()
}
prev, err := a.getwd()
if err != nil {
return fmt.Errorf("failed getting current working direcotyr: %v", err)
}
absDir, err := a.abs(dir)
if err != nil {
return err
}
a.logger.Debugf("changing working directory to \"%s\"", absDir)
if err := a.chdir(absDir); err != nil {
return fmt.Errorf("failed changing working directory to \"%s\": %v", absDir, err)
}
appErr := do()
a.logger.Debugf("changing working directory back to \"%s\"", prev)
if chdirBackErr := a.chdir(prev); chdirBackErr != nil {
if appErr != nil {
a.logger.Warnf("%v", appErr)
}
return fmt.Errorf("failed chaging working directory back to \"%s\": %v", prev, chdirBackErr)
}
return appErr
}
func (a *app) visitStateFiles(fileOrDir string, do func(string) error) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
for _, relPath := range desiredStateFiles {
var file string
var dir string
if a.directoryExistsAt(relPath) {
file = relPath
dir = relPath
} else {
file = filepath.Base(relPath)
dir = filepath.Dir(relPath)
}
a.logger.Debugf("processing file \"%s\" in directory \"%s\"", file, dir)
err := a.within(dir, func() error {
return do(file)
})
if err != nil {
return err
}
}
return nil
}
func (a *app) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f string) error {
content, err := a.readFile(f)
if err != nil {
return err
}
// render template, in two runs
r := &twoPassRenderer{
reader: a.readFile,
env: a.env,
namespace: a.namespace,
filename: f,
logger: a.logger,
abs: a.abs,
}
yamlBuf, err := r.renderTemplate(content)
if err != nil {
return fmt.Errorf("error during %s parsing: %v", f, err)
}
st, err := a.loadDesiredStateFromYaml(
yamlBuf.Bytes(),
f,
a.namespace,
a.env,
)
helm := helmexec.New(a.logger, a.kubeContext)
if err != nil {
switch stateLoadErr := err.(type) {
// Addresses https://github.com/roboll/helmfile/issues/279
case *state.StateLoadError:
switch stateLoadErr.Cause.(type) {
case *state.UndefinedEnvError:
return nil
default:
return err
}
default:
return err
}
}
errs := []error{}
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles {
if err := a.VisitDesiredStates(m, converge); err != nil {
switch err.(type) {
case *noMatchingHelmfileError:
default:
return fmt.Errorf("failed processing %s: %v", m, err)
}
} else {
noMatchInSubHelmfiles = false
}
}
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
} else {
var err error
st, err = st.ExecuteTemplates()
if err != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, err)
}
var processed bool
processed, errs = converge(st, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
}
return clean(st, errs)
})
if err != nil {
return err
}
if noMatchInHelmfiles {
return &noMatchingHelmfileError{selectors: a.selectors, env: a.env}
}
return nil
}
func (a *app) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
selectors := a.selectors
err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(selectors) > 0 {
err := st.FilterReleases(selectors)
if err != nil {
return false, []error{err}
}
}
releaseNameCounts := map[string]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)}
}
}
errs := converge(st, helm)
processed := len(st.Releases) != 0 && len(errs) == 0
return processed, errs
})
if err != nil {
return err
}
return nil
}
func (a *app) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) {
rels, err := a.findDesiredStateFiles(specifiedPath)
if err != nil {
return rels, err
}
files := make([]string, len(rels))
for i := range rels {
files[i], err = filepath.Abs(rels[i])
if err != nil {
return []string{}, err
}
}
return files, nil
}
func (a *app) findDesiredStateFiles(specifiedPath string) ([]string, error) {
var helmfileDir string
if specifiedPath != "" {
if a.fileExistsAt(specifiedPath) {
return []string{specifiedPath}, nil
} else if a.directoryExistsAt(specifiedPath) {
helmfileDir = specifiedPath
} else {
return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath)
}
} else {
var defaultFile string
if a.fileExistsAt(DefaultHelmfile) {
defaultFile = DefaultHelmfile
} else if a.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 a.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 := a.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 (a *app) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) {
c := state.NewCreator(a.logger, a.readFile, a.abs)
st, err := c.CreateFromYaml(yaml, file, env)
if err != nil {
return nil, err
}
helmfiles := []string{}
for _, globPattern := range st.Helmfiles {
helmfileRelativePattern := st.JoinBase(globPattern)
matches, err := a.glob(helmfileRelativePattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
helmfiles = append(helmfiles, matches...)
}
st.Helmfiles = helmfiles
if a.reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
}
if a.kubeContext != "" {
if st.HelmDefaults.KubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
os.Exit(1)
}
st.HelmDefaults.KubeContext = a.kubeContext
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
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)
// See http://tldp.org/LDP/abs/html/exitcodes.html
switch sig {
case syscall.SIGINT:
os.Exit(130)
case syscall.SIGTERM:
os.Exit(143)
}
}()
return st, nil
}
func clean(st *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := st.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
if errs != nil && len(errs) > 0 {
for _, err := range errs {
switch e := err.(type) {
case *state.ReleaseError:
fmt.Printf("err: release \"%s\" in \"%s\" failed: %v\n", e.Name, st.FilePath, e)
default:
fmt.Printf("err: %v\n", e)
}
}
return errs[0]
}
return nil
} }

386
pkg/app/app.go Normal file
View File

@ -0,0 +1,386 @@
package app
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"io/ioutil"
"log"
"os"
"os/signal"
"path/filepath"
"sort"
"syscall"
)
type App struct {
KubeContext string
Logger *zap.SugaredLogger
Reverse bool
Env string
Namespace string
Selectors []string
readFile func(string) ([]byte, error)
glob func(string) ([]string, error)
abs func(string) (string, error)
fileExistsAt func(string) bool
directoryExistsAt func(string) bool
getwd func() (string, error)
chdir func(string) error
}
func Init(app *App) *App {
app.readFile = ioutil.ReadFile
app.glob = filepath.Glob
app.abs = filepath.Abs
app.getwd = os.Getwd
app.chdir = os.Chdir
app.fileExistsAt = fileExistsAt
app.directoryExistsAt = directoryExistsAt
return app
}
func (a *App) within(dir string, do func() error) error {
if dir == "." {
return do()
}
prev, err := a.getwd()
if err != nil {
return fmt.Errorf("failed getting current working direcotyr: %v", err)
}
absDir, err := a.abs(dir)
if err != nil {
return err
}
a.Logger.Debugf("changing working directory to \"%s\"", absDir)
if err := a.chdir(absDir); err != nil {
return fmt.Errorf("failed changing working directory to \"%s\": %v", absDir, err)
}
appErr := do()
a.Logger.Debugf("changing working directory back to \"%s\"", prev)
if chdirBackErr := a.chdir(prev); chdirBackErr != nil {
if appErr != nil {
a.Logger.Warnf("%v", appErr)
}
return fmt.Errorf("failed chaging working directory back to \"%s\": %v", prev, chdirBackErr)
}
return appErr
}
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
for _, relPath := range desiredStateFiles {
var file string
var dir string
if a.directoryExistsAt(relPath) {
file = relPath
dir = relPath
} else {
file = filepath.Base(relPath)
dir = filepath.Dir(relPath)
}
a.Logger.Debugf("processing file \"%s\" in directory \"%s\"", file, dir)
err := a.within(dir, func() error {
return do(file)
})
if err != nil {
return err
}
}
return nil
}
func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f string) error {
content, err := a.readFile(f)
if err != nil {
return err
}
// render template, in two runs
r := &twoPassRenderer{
reader: a.readFile,
env: a.Env,
namespace: a.Namespace,
filename: f,
logger: a.Logger,
abs: a.abs,
}
yamlBuf, err := r.renderTemplate(content)
if err != nil {
return fmt.Errorf("error during %s parsing: %v", f, err)
}
st, err := a.loadDesiredStateFromYaml(
yamlBuf.Bytes(),
f,
a.Namespace,
a.Env,
)
helm := helmexec.New(a.Logger, a.KubeContext)
if err != nil {
switch stateLoadErr := err.(type) {
// Addresses https://github.com/roboll/helmfile/issues/279
case *state.StateLoadError:
switch stateLoadErr.Cause.(type) {
case *state.UndefinedEnvError:
return nil
default:
return err
}
default:
return err
}
}
errs := []error{}
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles {
if err := a.VisitDesiredStates(m, converge); err != nil {
switch err.(type) {
case *NoMatchingHelmfileError:
default:
return fmt.Errorf("failed processing %s: %v", m, err)
}
} else {
noMatchInSubHelmfiles = false
}
}
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
} else {
var err error
st, err = st.ExecuteTemplates()
if err != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, err)
}
var processed bool
processed, errs = converge(st, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
}
return clean(st, errs)
})
if err != nil {
return err
}
if noMatchInHelmfiles {
return &NoMatchingHelmfileError{selectors: a.Selectors, env: a.Env}
}
return nil
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
selectors := a.Selectors
err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(selectors) > 0 {
err := st.FilterReleases(selectors)
if err != nil {
return false, []error{err}
}
}
releaseNameCounts := map[string]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)}
}
}
errs := converge(st, helm)
processed := len(st.Releases) != 0 && len(errs) == 0
return processed, errs
})
if err != nil {
return err
}
return nil
}
func (a *App) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) {
rels, err := a.findDesiredStateFiles(specifiedPath)
if err != nil {
return rels, err
}
files := make([]string, len(rels))
for i := range rels {
files[i], err = filepath.Abs(rels[i])
if err != nil {
return []string{}, err
}
}
return files, nil
}
func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
var helmfileDir string
if specifiedPath != "" {
if a.fileExistsAt(specifiedPath) {
return []string{specifiedPath}, nil
} else if a.directoryExistsAt(specifiedPath) {
helmfileDir = specifiedPath
} else {
return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath)
}
} else {
var defaultFile string
if a.fileExistsAt(DefaultHelmfile) {
defaultFile = DefaultHelmfile
} else if a.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 a.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 := a.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 (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) {
c := state.NewCreator(a.Logger, a.readFile, a.abs)
st, err := c.CreateFromYaml(yaml, file, env)
if err != nil {
return nil, err
}
helmfiles := []string{}
for _, globPattern := range st.Helmfiles {
helmfileRelativePattern := st.JoinBase(globPattern)
matches, err := a.glob(helmfileRelativePattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
helmfiles = append(helmfiles, matches...)
}
st.Helmfiles = helmfiles
if a.Reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
}
if a.KubeContext != "" {
if st.HelmDefaults.KubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
os.Exit(1)
}
st.HelmDefaults.KubeContext = a.KubeContext
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
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)
// See http://tldp.org/LDP/abs/html/exitcodes.html
switch sig {
case syscall.SIGINT:
os.Exit(130)
case syscall.SIGTERM:
os.Exit(143)
}
}()
return st, nil
}
func clean(st *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := st.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
if errs != nil && len(errs) > 0 {
for _, err := range errs {
switch e := err.(type) {
case *state.ReleaseError:
fmt.Printf("err: release \"%s\" in \"%s\" failed: %v\n", e.Name, st.FilePath, e)
default:
fmt.Printf("err: %v\n", e)
}
}
return errs[0]
}
return nil
}

View File

@ -1,4 +1,4 @@
package main package app
import ( import (
"fmt" "fmt"
@ -18,12 +18,12 @@ type testFs struct {
files map[string]string files map[string]string
} }
func appWithFs(app *app, files map[string]string) *app { func appWithFs(app *App, files map[string]string) *App {
fs := newTestFs(files) fs := newTestFs(files)
return injectFs(app, fs) return injectFs(app, fs)
} }
func injectFs(app *app, fs *testFs) *app { func injectFs(app *App, fs *testFs) *App {
app.readFile = fs.readFile app.readFile = fs.readFile
app.glob = fs.glob app.glob = fs.glob
app.abs = fs.abs app.abs = fs.abs
@ -159,12 +159,12 @@ releases:
} }
for _, testcase := range testcases { for _, testcase := range testcases {
app := appWithFs(&app{ app := appWithFs(&App{
kubeContext: "default", KubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
selectors: []string{fmt.Sprintf("name=%s", testcase.name)}, Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
namespace: "", Namespace: "",
env: "default", Env: "default",
}, files) }, files)
err := app.VisitDesiredStatesWithReleasesFiltered( err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop, "helmfile.yaml", noop,
@ -210,12 +210,12 @@ releases:
} }
for _, testcase := range testcases { for _, testcase := range testcases {
app := appWithFs(&app{ app := appWithFs(&App{
kubeContext: "default", KubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
namespace: "", Namespace: "",
selectors: []string{}, Selectors: []string{},
env: testcase.name, Env: testcase.name,
}, files) }, files)
err := app.VisitDesiredStatesWithReleasesFiltered( err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop, "helmfile.yaml", noop,
@ -285,12 +285,12 @@ releases:
return []error{} return []error{}
} }
app := appWithFs(&app{ app := appWithFs(&App{
kubeContext: "default", KubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
namespace: "", Namespace: "",
selectors: []string{testcase.label}, Selectors: []string{testcase.label},
env: "default", Env: "default",
}, files) }, files)
err := app.VisitDesiredStatesWithReleasesFiltered( err := app.VisitDesiredStatesWithReleasesFiltered(
@ -356,13 +356,13 @@ releases:
} }
return []error{} return []error{}
} }
app := appWithFs(&app{ app := appWithFs(&App{
kubeContext: "default", KubeContext: "default",
logger: helmexec.NewLogger(os.Stderr, "debug"), Logger: helmexec.NewLogger(os.Stderr, "debug"),
reverse: testcase.reverse, Reverse: testcase.reverse,
namespace: "", Namespace: "",
selectors: []string{}, Selectors: []string{},
env: "default", Env: "default",
}, files) }, files)
err := app.VisitDesiredStatesWithReleasesFiltered( err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases, "helmfile.yaml", collectReleases,
@ -389,12 +389,12 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
labels: labels:
stage: post stage: post
`) `)
app := &app{ app := &App{
readFile: ioutil.ReadFile, readFile: ioutil.ReadFile,
glob: filepath.Glob, glob: filepath.Glob,
abs: filepath.Abs, abs: filepath.Abs,
kubeContext: "default", KubeContext: "default",
logger: logger, Logger: helmexec.NewLogger(os.Stderr, "debug"),
} }
_, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default") _, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default")
if err != nil { if err != nil {

7
pkg/app/constants.go Normal file
View File

@ -0,0 +1,7 @@
package app
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
)

33
pkg/app/context.go Normal file
View File

@ -0,0 +1,33 @@
package app
import "github.com/roboll/helmfile/state"
type Context struct {
updatedRepos map[string]struct{}
}
func NewContext() Context {
return Context{
updatedRepos: map[string]struct{}{},
}
}
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
var errs []error
allUpdated := true
for _, r := range st.Repositories {
_, exists := ctx.updatedRepos[r.Name]
allUpdated = allUpdated && exists
}
if !allUpdated {
errs = st.SyncRepos(helm)
for _, r := range st.Repositories {
ctx.updatedRepos[r.Name] = struct{}{}
}
}
return errs
}

19
pkg/app/errors.go Normal file
View File

@ -0,0 +1,19 @@
package app
import (
"fmt"
"strings"
)
type NoMatchingHelmfileError struct {
selectors []string
env string
}
func (e *NoMatchingHelmfileError) Error() string {
return fmt.Sprintf(
"err: no releases found that matches specified selector(%s) and environment(%s), in any helmfile",
strings.Join(e.selectors, ", "),
e.env,
)
}

View File

@ -0,0 +1,80 @@
package app
import (
"bytes"
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"path/filepath"
"strings"
)
func prependLineNumbers(text string) string {
buf := bytes.NewBufferString("")
lines := strings.Split(text, "\n")
for i, line := range lines {
buf.WriteString(fmt.Sprintf("%2d: %s\n", i, line))
}
return buf.String()
}
type twoPassRenderer struct {
reader func(string) ([]byte, error)
env string
namespace string
filename string
logger *zap.SugaredLogger
abs func(string) (string, error)
}
func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environment {
firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), tmplData)
// parse as much as we can, tolerate errors, this is a preparse
yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil && r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
if yamlBuf == nil { // we have a template syntax error, let the second parse report
r.logger.Debugf("template syntax error: %v", err)
return firstPassEnv
}
}
c := state.NewCreator(r.logger, r.reader, r.abs)
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
}
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
}
if prestate != nil {
firstPassEnv = prestate.Env
}
return firstPassEnv
}
func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content)
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData)
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil {
if r.logger != nil {
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}

View File

@ -1,4 +1,4 @@
package main package app
import ( import (
"fmt" "fmt"

View File

@ -830,6 +830,23 @@ func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
return nil return nil
} }
// BuildDeps wrapper for building dependencies on the releases
func (st *HelmState) BuildDeps(helm helmexec.Interface) []error {
errs := []error{}
for _, release := range st.Releases {
if isLocalChart(release.Chart) {
if err := helm.BuildDeps(normalizeChart(st.basePath, release.Chart)); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}
// JoinBase returns an absolute path in the form basePath/relative // JoinBase returns an absolute path in the form basePath/relative
func (st *HelmState) JoinBase(relPath string) string { func (st *HelmState) JoinBase(relPath string) string {
return filepath.Join(st.basePath, relPath) return filepath.Join(st.basePath, relPath)

View File

@ -524,6 +524,14 @@ func (helm *mockHelmExec) UpdateDeps(chart string) error {
return nil return nil
} }
func (helm *mockHelmExec) BuildDeps(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) { func (helm *mockHelmExec) SetExtraArgs(args ...string) {
return return
} }

View File

@ -55,14 +55,32 @@ trap "{ $kubectl delete namespace ${test_ns}; }" EXIT # remove namespace wheneve
# TEST CASES---------------------------------------------------------------------------------------------------------- # TEST CASES----------------------------------------------------------------------------------------------------------
test_start "happypath - simple rollout of httpbin chart" test_start "happypath - simple rollout of httpbin chart"
info "Diffing ${dir}/happypath.yaml"
helmfile -f ${dir}/happypath.yaml diff --detailed-exitcode
code=$?
[ ${code} -eq 2 ] || fail "unexpected exit code returned by helmfile diff: ${code}"
info "Templating ${dir}/happypath.yaml"
helmfile -f ${dir}/happypath.yaml template
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}"
info "Syncing ${dir}/happypath.yaml" info "Syncing ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml sync ${helmfile} -f ${dir}/happypath.yaml sync
wait_deploy_ready httpbin-httpbin wait_deploy_ready httpbin-httpbin
retry 5 "curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200" retry 5 "curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200"
[ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK" [ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK"
info "Applying ${dir}/happypath.yaml"
helmfile -f ${dir}/happypath.yaml apply
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
info "Deleting release" info "Deleting release"
${helmfile} -f ${dir}/happypath.yaml delete ${helmfile} -f ${dir}/happypath.yaml delete
${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release should not exist anymore after a delete" ${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release should not exist anymore after a delete"
test_pass "happypath" test_pass "happypath"