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:
parent
b894012c80
commit
c6236a15bb
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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...)...)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
673
main.go
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultHelmfile = "helmfile.yaml"
|
||||||
|
DeprecatedHelmfile = "charts.yaml"
|
||||||
|
DefaultHelmfileDirectory = "helmfile.d"
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue