feat: helmfile as a go library (#639)
* feat: helmfile as a go library This removes almost all the dependencies from the helmfile core logic to urfave/cli. `main.go` is now a thin wrapper around the core logic implemented in `pkg/app`.
This commit is contained in:
		
							parent
							
								
									f6057a1cca
								
							
						
					
					
						commit
						e2d6dc4afa
					
				
							
								
								
									
										98
									
								
								cmd/cmd.go
								
								
								
								
							
							
						
						
									
										98
									
								
								cmd/cmd.go
								
								
								
								
							|  | @ -1,98 +0,0 @@ | ||||||
| package cmd |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/app" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/helmexec" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/state" |  | ||||||
| 	"github.com/urfave/cli" |  | ||||||
| 	"go.uber.org/zap" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| 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, app.LoadOpts{Selectors: a.Selectors}, convergeWithHelmBinary) |  | ||||||
| 
 |  | ||||||
| 	return toCliError(c, 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(c, err) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func toCliError(c *cli.Context, err error) error { |  | ||||||
| 	if err != nil { |  | ||||||
| 		switch e := err.(type) { |  | ||||||
| 		case *app.NoMatchingHelmfileError: |  | ||||||
| 			noMatchingExitCode := 3 |  | ||||||
| 			if c.GlobalBool("allow-no-matching-release") { |  | ||||||
| 				noMatchingExitCode = 0 |  | ||||||
| 			} |  | ||||||
| 			return cli.NewExitError(e.Error(), noMatchingExitCode) |  | ||||||
| 		case *app.Error: |  | ||||||
| 			return cli.NewExitError(e.Error(), e.Code()) |  | ||||||
| 		default: |  | ||||||
| 			panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
							
								
								
									
										37
									
								
								cmd/deps.go
								
								
								
								
							
							
						
						
									
										37
									
								
								cmd/deps.go
								
								
								
								
							|  | @ -1,37 +0,0 @@ | ||||||
| package cmd |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/roboll/helmfile/pkg/app" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/argparser" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/helmexec" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/state" |  | ||||||
| 	"github.com/urfave/cli" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func Deps() 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 := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 				if len(args) > 0 { |  | ||||||
| 					helm.SetExtraArgs(args...) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				errs := state.UpdateDeps(helm) |  | ||||||
| 
 |  | ||||||
| 				ok := len(errs) == 0 |  | ||||||
| 
 |  | ||||||
| 				return ok, errs |  | ||||||
| 			}) |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										454
									
								
								main.go
								
								
								
								
							
							
						
						
									
										454
									
								
								main.go
								
								
								
								
							|  | @ -2,9 +2,7 @@ package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/roboll/helmfile/cmd" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/app" | 	"github.com/roboll/helmfile/pkg/app" | ||||||
| 	"github.com/roboll/helmfile/pkg/argparser" |  | ||||||
| 	"github.com/roboll/helmfile/pkg/helmexec" | 	"github.com/roboll/helmfile/pkg/helmexec" | ||||||
| 	"github.com/roboll/helmfile/pkg/state" | 	"github.com/roboll/helmfile/pkg/state" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
|  | @ -94,7 +92,20 @@ func main() { | ||||||
| 
 | 
 | ||||||
| 	cliApp.Before = configureLogging | 	cliApp.Before = configureLogging | ||||||
| 	cliApp.Commands = []cli.Command{ | 	cliApp.Commands = []cli.Command{ | ||||||
| 		cmd.Deps(), | 		{ | ||||||
|  | 			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: action(func(run *app.App, c configImpl) error { | ||||||
|  | 				return run.Deps(c) | ||||||
|  | 			}), | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			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)", | ||||||
|  | @ -105,20 +116,9 @@ func main() { | ||||||
| 					Usage: "pass args to helm exec", | 					Usage: "pass args to helm exec", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return cmd.VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) { | 				return run.Repos(c) | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) | 			}), | ||||||
| 					if len(args) > 0 { |  | ||||||
| 						helm.SetExtraArgs(args...) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					errs := ctx.SyncReposOnce(state, helm) |  | ||||||
| 
 |  | ||||||
| 					ok := len(state.Repositories) > 0 && len(errs) == 0 |  | ||||||
| 
 |  | ||||||
| 					return ok, errs |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "charts", | 			Name:  "charts", | ||||||
|  | @ -139,14 +139,9 @@ func main() { | ||||||
| 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				affectedReleases := state.AffectedReleases{} | 				return run.DeprecatedSyncCharts(c) | ||||||
| 				errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, _ app.Context) []error { | 			}), | ||||||
| 					return executeSyncCommand(c, &affectedReleases, st, helm) |  | ||||||
| 				}) |  | ||||||
| 				affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) |  | ||||||
| 				return errs |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "diff", | 			Name:  "diff", | ||||||
|  | @ -179,24 +174,9 @@ func main() { | ||||||
| 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { | 				return run.Diff(c) | ||||||
| 					if !c.Bool("skip-deps") { | 			}), | ||||||
| 						if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 						if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if errs := state.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 { |  | ||||||
| 						return errs |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					_, errs := ExecuteDiffCommand(NewUrfaveCliConfigImpl(c), state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets")) |  | ||||||
| 					return errs |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "template", | 			Name:  "template", | ||||||
|  | @ -221,22 +201,9 @@ func main() { | ||||||
| 					Usage: "skip running `helm repo update` and `helm dependency build`", | 					Usage: "skip running `helm repo update` and `helm dependency build`", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { | 				return run.Template(c) | ||||||
| 					if !c.Bool("skip-deps") { | 			}), | ||||||
| 						if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 						if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 { |  | ||||||
| 						return errs |  | ||||||
| 					} |  | ||||||
| 					return executeTemplateCommand(c, state, helm) |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "lint", | 			Name:  "lint", | ||||||
|  | @ -261,25 +228,9 @@ func main() { | ||||||
| 					Usage: "skip running `helm repo update` and `helm dependency build`", | 					Usage: "skip running `helm repo update` and `helm dependency build`", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { | 				return run.Lint(c) | ||||||
| 					values := c.StringSlice("values") | 			}), | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 					workers := c.Int("concurrency") |  | ||||||
| 					if !c.Bool("skip-deps") { |  | ||||||
| 						if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 						if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 { |  | ||||||
| 						return errs |  | ||||||
| 					} |  | ||||||
| 					return state.LintReleases(helm, values, args, workers) |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "sync", | 			Name:  "sync", | ||||||
|  | @ -304,25 +255,9 @@ func main() { | ||||||
| 					Usage: "skip running `helm repo update` and `helm dependency build`", | 					Usage: "skip running `helm repo update` and `helm dependency build`", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				affectedReleases := state.AffectedReleases{} | 				return run.Sync(c) | ||||||
| 				errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { | 			}), | ||||||
| 					if !c.Bool("skip-deps") { |  | ||||||
| 						if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 						if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 { |  | ||||||
| 						return errs |  | ||||||
| 					} |  | ||||||
| 					return executeSyncCommand(c, &affectedReleases, st, helm) |  | ||||||
| 				}) |  | ||||||
| 				affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) |  | ||||||
| 				return errs |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "apply", | 			Name:  "apply", | ||||||
|  | @ -351,88 +286,9 @@ func main() { | ||||||
| 					Usage: "skip running `helm repo update` and `helm dependency build`", | 					Usage: "skip running `helm repo update` and `helm dependency build`", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				affectedReleases := state.AffectedReleases{} | 				return run.Apply(c) | ||||||
| 				errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { | 			}), | ||||||
| 					if !c.Bool("skip-deps") { |  | ||||||
| 						if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 						if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { |  | ||||||
| 							return errs |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 { |  | ||||||
| 						return errs |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					releases, errs := ExecuteDiffCommand(NewUrfaveCliConfigImpl(c), st, helm, true, c.Bool("suppress-secrets")) |  | ||||||
| 
 |  | ||||||
| 					releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm) |  | ||||||
| 					if err != nil { |  | ||||||
| 						errs = append(errs, err) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					fatalErrs := []error{} |  | ||||||
| 
 |  | ||||||
| 					noError := true |  | ||||||
| 					for _, e := range errs { |  | ||||||
| 						switch err := e.(type) { |  | ||||||
| 						case *state.ReleaseError: |  | ||||||
| 							if err.Code != 2 { |  | ||||||
| 								noError = false |  | ||||||
| 								fatalErrs = append(fatalErrs, e) |  | ||||||
| 							} |  | ||||||
| 						default: |  | ||||||
| 							noError = false |  | ||||||
| 							fatalErrs = append(fatalErrs, e) |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					// sync only when there are changes
 |  | ||||||
| 					if noError { |  | ||||||
| 						if len(releases) == 0 && len(releasesToBeDeleted) == 0 { |  | ||||||
| 							// TODO better way to get the logger
 |  | ||||||
| 							logger := c.App.Metadata["logger"].(*zap.SugaredLogger) |  | ||||||
| 							logger.Infof("") |  | ||||||
| 							logger.Infof("No affected releases") |  | ||||||
| 						} else { |  | ||||||
| 							names := []string{} |  | ||||||
| 							for _, r := range releases { |  | ||||||
| 								names = append(names, fmt.Sprintf("  %s (%s) UPDATED", r.Name, r.Chart)) |  | ||||||
| 							} |  | ||||||
| 							for _, r := range releasesToBeDeleted { |  | ||||||
| 								names = append(names, fmt.Sprintf("  %s (%s) DELETED", r.Name, r.Chart)) |  | ||||||
| 							} |  | ||||||
| 
 |  | ||||||
| 							msg := fmt.Sprintf(`Affected releases are: |  | ||||||
| %s |  | ||||||
| 
 |  | ||||||
| Do you really want to apply? |  | ||||||
|   Helmfile will apply all your changes, as shown above. |  | ||||||
| 
 |  | ||||||
| `, strings.Join(names, "\n")) |  | ||||||
| 							interactive := c.GlobalBool("interactive") |  | ||||||
| 							if !interactive || interactive && askForConfirmation(msg) { |  | ||||||
| 								rs := []state.ReleaseSpec{} |  | ||||||
| 								for _, r := range releases { |  | ||||||
| 									rs = append(rs, *r) |  | ||||||
| 								} |  | ||||||
| 								for _, r := range releasesToBeDeleted { |  | ||||||
| 									rs = append(rs, *r) |  | ||||||
| 								} |  | ||||||
| 
 |  | ||||||
| 								st.Releases = rs |  | ||||||
| 								return executeSyncCommand(c, &affectedReleases, st, helm) |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					return fatalErrs |  | ||||||
| 				}) |  | ||||||
| 				affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) |  | ||||||
| 				return errs |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "status", | 			Name:  "status", | ||||||
|  | @ -449,18 +305,9 @@ Do you really want to apply? | ||||||
| 					Usage: "pass args to helm exec", | 					Usage: "pass args to helm exec", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { | 				return run.Status(c) | ||||||
| 					workers := c.Int("concurrency") | 			}), | ||||||
| 
 |  | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 					if len(args) > 0 { |  | ||||||
| 						helm.SetExtraArgs(args...) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					return state.ReleaseStatuses(helm, workers) |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "delete", | 			Name:  "delete", | ||||||
|  | @ -476,37 +323,9 @@ Do you really want to apply? | ||||||
| 					Usage: "purge releases i.e. free release names and histories", | 					Usage: "purge releases i.e. free release names and histories", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				affectedReleases := state.AffectedReleases{} | 				return run.Delete(c) | ||||||
| 				errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { | 			}), | ||||||
| 					purge := c.Bool("purge") |  | ||||||
| 
 |  | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 					if len(args) > 0 { |  | ||||||
| 						helm.SetExtraArgs(args...) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					names := make([]string, len(state.Releases)) |  | ||||||
| 					for i, r := range state.Releases { |  | ||||||
| 						names[i] = fmt.Sprintf("  %s (%s)", r.Name, r.Chart) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					msg := fmt.Sprintf(`Affected releases are: |  | ||||||
| %s |  | ||||||
| 
 |  | ||||||
| Do you really want to delete? |  | ||||||
|   Helmfile will delete all your releases, as shown above. |  | ||||||
| 
 |  | ||||||
| `, strings.Join(names, "\n")) |  | ||||||
| 					interactive := c.GlobalBool("interactive") |  | ||||||
| 					if !interactive || interactive && askForConfirmation(msg) { |  | ||||||
| 						return state.DeleteReleases(&affectedReleases, helm, purge) |  | ||||||
| 					} |  | ||||||
| 					return nil |  | ||||||
| 				}) |  | ||||||
| 				affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) |  | ||||||
| 				return errs |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "destroy", | 			Name:  "destroy", | ||||||
|  | @ -518,35 +337,9 @@ Do you really want to delete? | ||||||
| 					Usage: "pass args to helm exec", | 					Usage: "pass args to helm exec", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				affectedReleases := state.AffectedReleases{} | 				return run.Destroy(c) | ||||||
| 				errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { | 			}), | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 					if len(args) > 0 { |  | ||||||
| 						helm.SetExtraArgs(args...) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					names := make([]string, len(state.Releases)) |  | ||||||
| 					for i, r := range state.Releases { |  | ||||||
| 						names[i] = fmt.Sprintf("  %s (%s)", r.Name, r.Chart) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					msg := fmt.Sprintf(`Affected releases are: |  | ||||||
| %s |  | ||||||
| 
 |  | ||||||
| Do you really want to delete? |  | ||||||
|   Helmfile will delete all your releases, as shown above. |  | ||||||
| 
 |  | ||||||
| `, strings.Join(names, "\n")) |  | ||||||
| 					interactive := c.GlobalBool("interactive") |  | ||||||
| 					if !interactive || interactive && askForConfirmation(msg) { |  | ||||||
| 						return state.DeleteReleases(&affectedReleases, helm, true) |  | ||||||
| 					} |  | ||||||
| 					return nil |  | ||||||
| 				}) |  | ||||||
| 				affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) |  | ||||||
| 				return errs |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "test", | 			Name:  "test", | ||||||
|  | @ -572,20 +365,9 @@ Do you really want to delete? | ||||||
| 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Action: func(c *cli.Context) error { | 			Action: action(func(run *app.App, c configImpl) error { | ||||||
| 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { | 				return run.Test(c) | ||||||
| 					cleanup := c.Bool("cleanup") | 			}), | ||||||
| 					timeout := c.Int("timeout") |  | ||||||
| 					concurrency := c.Int("concurrency") |  | ||||||
| 
 |  | ||||||
| 					args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 					if len(args) > 0 { |  | ||||||
| 						helm.SetExtraArgs(args...) |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					return state.TestReleases(helm, cleanup, timeout, concurrency) |  | ||||||
| 				}) |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -596,41 +378,19 @@ Do you really want to delete? | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func executeSyncCommand(c *cli.Context, affectedReleases *state.AffectedReleases, state *state.HelmState, helm helmexec.Interface) []error { |  | ||||||
| 	args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 	if len(args) > 0 { |  | ||||||
| 		helm.SetExtraArgs(args...) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	values := c.StringSlice("values") |  | ||||||
| 	workers := c.Int("concurrency") |  | ||||||
| 
 |  | ||||||
| 	return state.SyncReleases(affectedReleases, helm, values, workers) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { |  | ||||||
| 	args := argparser.GetArgs(c.String("args"), state) |  | ||||||
| 	values := c.StringSlice("values") |  | ||||||
| 	workers := c.Int("concurrency") |  | ||||||
| 
 |  | ||||||
| 	return state.TemplateReleases(helm, values, args, workers) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Config interface { |  | ||||||
| 	HasCommandName(string) bool |  | ||||||
| 	Values() []string |  | ||||||
| 	Concurrency() int |  | ||||||
| 	Args() string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type configImpl struct { | type configImpl struct { | ||||||
| 	c *cli.Context | 	c *cli.Context | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewUrfaveCliConfigImpl(c *cli.Context) configImpl { | func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { | ||||||
|  | 	if c.NArg() > 0 { | ||||||
|  | 		cli.ShowAppHelp(c) | ||||||
|  | 		return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return configImpl{ | 	return configImpl{ | ||||||
| 		c: c, | 		c: c, | ||||||
| 	} | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c configImpl) Values() []string { | func (c configImpl) Values() []string { | ||||||
|  | @ -649,17 +409,105 @@ func (c configImpl) HasCommandName(name string) bool { | ||||||
| 	return c.c.Command.HasName(name) | 	return c.c.Command.HasName(name) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func ExecuteDiffCommand(c Config, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) { | // DiffConfig
 | ||||||
| 	args := argparser.GetArgs(c.Args(), st) | 
 | ||||||
| 	if len(args) > 0 { | func (c configImpl) SkipDeps() bool { | ||||||
| 		helm.SetExtraArgs(args...) | 	return c.c.Bool("skip-deps") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 	triggerCleanupEvents := c.HasCommandName("diff") | func (c configImpl) DetailedExitcode() bool { | ||||||
| 
 | 	return c.c.Bool("detailed-exitcode") | ||||||
| 	return st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, suppressSecrets, triggerCleanupEvents) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error { | func (c configImpl) SuppressSecrets() bool { | ||||||
| 	return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge) | 	return c.c.Bool("suppress-secrets") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DeleteConfig
 | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Purge() bool { | ||||||
|  | 	return c.c.Bool("purge") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestConfig
 | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Cleanup() bool { | ||||||
|  | 	return c.c.Bool("cleanup") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Timeout() int { | ||||||
|  | 	return c.c.Int("timeout") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GlobalConfig
 | ||||||
|  | 
 | ||||||
|  | func (c configImpl) HelmBinary() string { | ||||||
|  | 	return c.c.GlobalString("helm-binary") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) KubeContext() string { | ||||||
|  | 	return c.c.GlobalString("kube-context") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Namespace() string { | ||||||
|  | 	return c.c.GlobalString("namespace") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) FileOrDir() string { | ||||||
|  | 	return c.c.GlobalString("file") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Selectors() []string { | ||||||
|  | 	return c.c.GlobalStringSlice("selector") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Interactive() bool { | ||||||
|  | 	return c.c.GlobalBool("interactive") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Logger() *zap.SugaredLogger { | ||||||
|  | 	return c.c.App.Metadata["logger"].(*zap.SugaredLogger) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) Env() string { | ||||||
|  | 	env := c.c.GlobalString("environment") | ||||||
|  | 	if env == "" { | ||||||
|  | 		env = state.DefaultEnv | ||||||
|  | 	} | ||||||
|  | 	return env | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func action(do func(*app.App, configImpl) error) func(*cli.Context) error { | ||||||
|  | 	return func(implCtx *cli.Context) error { | ||||||
|  | 		conf, err := NewUrfaveCliConfigImpl(implCtx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		a := app.New(conf) | ||||||
|  | 
 | ||||||
|  | 		a.ErrorHandler = func(err error) error { | ||||||
|  | 			return toCliError(implCtx, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return do(a, conf) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func toCliError(c *cli.Context, err error) error { | ||||||
|  | 	if err != nil { | ||||||
|  | 		switch e := err.(type) { | ||||||
|  | 		case *app.NoMatchingHelmfileError: | ||||||
|  | 			noMatchingExitCode := 3 | ||||||
|  | 			if c.GlobalBool("allow-no-matching-release") { | ||||||
|  | 				noMatchingExitCode = 0 | ||||||
|  | 			} | ||||||
|  | 			return cli.NewExitError(e.Error(), noMatchingExitCode) | ||||||
|  | 		case *app.Error: | ||||||
|  | 			return cli.NewExitError(e.Error(), e.Code()) | ||||||
|  | 		default: | ||||||
|  | 			panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										123
									
								
								pkg/app/app.go
								
								
								
								
							
							
						
						
									
										123
									
								
								pkg/app/app.go
								
								
								
								
							|  | @ -24,6 +24,12 @@ type App struct { | ||||||
| 	Env         string | 	Env         string | ||||||
| 	Namespace   string | 	Namespace   string | ||||||
| 	Selectors   []string | 	Selectors   []string | ||||||
|  | 	HelmBinary  string | ||||||
|  | 	Args        string | ||||||
|  | 
 | ||||||
|  | 	FileOrDir string | ||||||
|  | 
 | ||||||
|  | 	ErrorHandler func(error) error | ||||||
| 
 | 
 | ||||||
| 	readFile          func(string) ([]byte, error) | 	readFile          func(string) ([]byte, error) | ||||||
| 	fileExists        func(string) (bool, error) | 	fileExists        func(string) (bool, error) | ||||||
|  | @ -36,6 +42,19 @@ type App struct { | ||||||
| 	chdir func(string) error | 	chdir func(string) error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func New(conf ConfigProvider) *App { | ||||||
|  | 	return Init(&App{ | ||||||
|  | 		KubeContext: conf.KubeContext(), | ||||||
|  | 		Logger:      conf.Logger(), | ||||||
|  | 		Env:         conf.Env(), | ||||||
|  | 		Namespace:   conf.Namespace(), | ||||||
|  | 		Selectors:   conf.Selectors(), | ||||||
|  | 		HelmBinary:  conf.HelmBinary(), | ||||||
|  | 		Args:        conf.Args(), | ||||||
|  | 		FileOrDir:   conf.FileOrDir(), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func Init(app *App) *App { | func Init(app *App) *App { | ||||||
| 	app.readFile = ioutil.ReadFile | 	app.readFile = ioutil.ReadFile | ||||||
| 	app.glob = filepath.Glob | 	app.glob = filepath.Glob | ||||||
|  | @ -48,6 +67,84 @@ func Init(app *App) *App { | ||||||
| 	return app | 	return app | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *App) Deps(c DepsConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Deps(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Repos(c ReposConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Repos(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) reverse() *App { | ||||||
|  | 	new := *a | ||||||
|  | 	new.Reverse = true | ||||||
|  | 	return &new | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.DeprecatedSyncCharts(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Diff(c DiffConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Diff(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Template(c TemplateConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Template(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Lint(c LintConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Lint(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Sync(c SyncConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Sync(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Apply(c ApplyConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Apply(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Status(c StatusesConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Status(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Delete(c DeleteConfigProvider) error { | ||||||
|  | 	return a.reverse().ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Delete(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Destroy(c DestroyConfigProvider) error { | ||||||
|  | 	return a.reverse().ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Destroy(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *App) Test(c TestConfigProvider) error { | ||||||
|  | 	return a.ForEachState(func(run *Run) []error { | ||||||
|  | 		return run.Test(c) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *App) within(dir string, do func() error) error { | func (a *App) within(dir string, do func() error) error { | ||||||
| 	if dir == "." { | 	if dir == "." { | ||||||
| 		return do() | 		return do() | ||||||
|  | @ -140,7 +237,7 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He | ||||||
| 	return ld.Load(file, op) | 	return ld.Load(file, op) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { | func (a *App) visitStates(fileOrDir string, opts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { | ||||||
| 	noMatchInHelmfiles := true | 	noMatchInHelmfiles := true | ||||||
| 
 | 
 | ||||||
| 	err := a.visitStateFiles(fileOrDir, func(f, d string) error { | 	err := a.visitStateFiles(fileOrDir, func(f, d string) error { | ||||||
|  | @ -195,7 +292,7 @@ func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func( | ||||||
| 				} else { | 				} else { | ||||||
| 					optsForNestedState.Selectors = m.Selectors | 					optsForNestedState.Selectors = m.Selectors | ||||||
| 				} | 				} | ||||||
| 				if err := a.VisitDesiredStates(m.Path, optsForNestedState, converge); err != nil { | 				if err := a.visitStates(m.Path, optsForNestedState, converge); err != nil { | ||||||
| 					switch err.(type) { | 					switch err.(type) { | ||||||
| 					case *NoMatchingHelmfileError: | 					case *NoMatchingHelmfileError: | ||||||
| 
 | 
 | ||||||
|  | @ -229,10 +326,26 @@ func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func( | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *App) ForEachState(do func(*Run) []error) error { | ||||||
|  | 	err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error { | ||||||
|  | 		ctx := NewContext() | ||||||
|  | 
 | ||||||
|  | 		run := NewRun(st, helm, ctx) | ||||||
|  | 
 | ||||||
|  | 		return do(run) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	if err != nil && a.ErrorHandler != nil { | ||||||
|  | 		return a.ErrorHandler(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { | func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { | ||||||
| 	opts := LoadOpts{Selectors: a.Selectors} | 	opts := LoadOpts{Selectors: a.Selectors} | ||||||
| 
 | 
 | ||||||
| 	err := a.VisitDesiredStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | 	err := a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | ||||||
| 		if len(st.Selectors) > 0 { | 		if len(st.Selectors) > 0 { | ||||||
| 			err := st.FilterReleases() | 			err := st.FilterReleases() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -240,6 +353,10 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if a.HelmBinary != "" { | ||||||
|  | 			helm.SetHelmBinary(a.HelmBinary) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		type Key struct { | 		type Key struct { | ||||||
| 			TillerNamespace, Name string | 			TillerNamespace, Name string | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package main | package app | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
|  | @ -11,7 +11,7 @@ import ( | ||||||
| // Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
 | // Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
 | ||||||
| //
 | //
 | ||||||
| // Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
 | // Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
 | ||||||
| func askForConfirmation(s string) bool { | func AskForConfirmation(s string) bool { | ||||||
| 	reader := bufio.NewReader(os.Stdin) | 	reader := bufio.NewReader(os.Stdin) | ||||||
| 
 | 
 | ||||||
| 	for { | 	for { | ||||||
|  | @ -0,0 +1,128 @@ | ||||||
|  | package app | ||||||
|  | 
 | ||||||
|  | import "go.uber.org/zap" | ||||||
|  | 
 | ||||||
|  | type ConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 	HelmBinary() string | ||||||
|  | 
 | ||||||
|  | 	FileOrDir() string | ||||||
|  | 	KubeContext() string | ||||||
|  | 	Namespace() string | ||||||
|  | 	Selectors() []string | ||||||
|  | 	Env() string | ||||||
|  | 
 | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type DeprecatedChartsConfigProvider interface { | ||||||
|  | 	Values() []string | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type DepsConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ReposConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ApplyConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Values() []string | ||||||
|  | 	SkipDeps() bool | ||||||
|  | 
 | ||||||
|  | 	SuppressSecrets() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | 	interactive | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type SyncConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Values() []string | ||||||
|  | 	SkipDeps() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type DiffConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Values() []string | ||||||
|  | 	SkipDeps() bool | ||||||
|  | 
 | ||||||
|  | 	SuppressSecrets() bool | ||||||
|  | 
 | ||||||
|  | 	DetailedExitcode() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type DeleteConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Purge() bool | ||||||
|  | 
 | ||||||
|  | 	interactive | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type DestroyConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	interactive | ||||||
|  | 	loggingConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type TestConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Timeout() int | ||||||
|  | 	Cleanup() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type LintConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Values() []string | ||||||
|  | 	SkipDeps() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type TemplateConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	Values() []string | ||||||
|  | 	SkipDeps() bool | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type StatusesConfigProvider interface { | ||||||
|  | 	Args() string | ||||||
|  | 
 | ||||||
|  | 	concurrencyConfig | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type concurrencyConfig interface { | ||||||
|  | 	Concurrency() int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type loggingConfig interface { | ||||||
|  | 	Logger() *zap.SugaredLogger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type interactive interface { | ||||||
|  | 	Interactive() bool | ||||||
|  | } | ||||||
|  | @ -0,0 +1,302 @@ | ||||||
|  | package app | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/roboll/helmfile/pkg/argparser" | ||||||
|  | 	"github.com/roboll/helmfile/pkg/helmexec" | ||||||
|  | 	"github.com/roboll/helmfile/pkg/state" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type Run struct { | ||||||
|  | 	state *state.HelmState | ||||||
|  | 	helm  helmexec.Interface | ||||||
|  | 	ctx   Context | ||||||
|  | 
 | ||||||
|  | 	Ask func(string) bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run { | ||||||
|  | 	return &Run{state: st, helm: helm, ctx: ctx} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) askForConfirmation(msg string) bool { | ||||||
|  | 	if r.Ask != nil { | ||||||
|  | 		return r.Ask(msg) | ||||||
|  | 	} | ||||||
|  | 	return AskForConfirmation(msg) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Deps(c DepsConfigProvider) []error { | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	return r.state.UpdateDeps(r.helm) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Repos(c ReposConfigProvider) []error { | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	return r.ctx.SyncReposOnce(r.state, r.helm) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) []error { | ||||||
|  | 	st := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 
 | ||||||
|  | 	affectedReleases := state.AffectedReleases{} | ||||||
|  | 	errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) | ||||||
|  | 	affectedReleases.DisplayAffectedReleases(c.Logger()) | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Status(c StatusesConfigProvider) []error { | ||||||
|  | 	workers := c.Concurrency() | ||||||
|  | 
 | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	return r.state.ReleaseStatuses(r.helm, workers) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Delete(c DeleteConfigProvider) []error { | ||||||
|  | 	affectedReleases := state.AffectedReleases{} | ||||||
|  | 	purge := c.Purge() | ||||||
|  | 
 | ||||||
|  | 	errs := []error{} | ||||||
|  | 
 | ||||||
|  | 	names := make([]string, len(r.state.Releases)) | ||||||
|  | 	for i, r := range r.state.Releases { | ||||||
|  | 		names[i] = fmt.Sprintf("  %s (%s)", r.Name, r.Chart) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	msg := fmt.Sprintf(`Affected releases are: | ||||||
|  | %s | ||||||
|  | 
 | ||||||
|  | Do you really want to delete? | ||||||
|  |   Helmfile will delete all your releases, as shown above. | ||||||
|  | 
 | ||||||
|  | `, strings.Join(names, "\n")) | ||||||
|  | 	interactive := c.Interactive() | ||||||
|  | 	if !interactive || interactive && r.askForConfirmation(msg) { | ||||||
|  | 		r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 		errs = r.state.DeleteReleases(&affectedReleases, r.helm, purge) | ||||||
|  | 	} | ||||||
|  | 	affectedReleases.DisplayAffectedReleases(c.Logger()) | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Destroy(c DestroyConfigProvider) []error { | ||||||
|  | 	errs := []error{} | ||||||
|  | 	affectedReleases := state.AffectedReleases{} | ||||||
|  | 
 | ||||||
|  | 	names := make([]string, len(r.state.Releases)) | ||||||
|  | 	for i, r := range r.state.Releases { | ||||||
|  | 		names[i] = fmt.Sprintf("  %s (%s)", r.Name, r.Chart) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	msg := fmt.Sprintf(`Affected releases are: | ||||||
|  | %s | ||||||
|  | 
 | ||||||
|  | Do you really want to delete? | ||||||
|  |   Helmfile will delete all your releases, as shown above. | ||||||
|  | 
 | ||||||
|  | `, strings.Join(names, "\n")) | ||||||
|  | 	interactive := c.Interactive() | ||||||
|  | 	if !interactive || interactive && r.askForConfirmation(msg) { | ||||||
|  | 		r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 		errs = r.state.DeleteReleases(&affectedReleases, r.helm, true) | ||||||
|  | 	} | ||||||
|  | 	affectedReleases.DisplayAffectedReleases(c.Logger()) | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Apply(c ApplyConfigProvider) []error { | ||||||
|  | 	st := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 	ctx := r.ctx | ||||||
|  | 
 | ||||||
|  | 	affectedReleases := state.AffectedReleases{} | ||||||
|  | 	if !c.SkipDeps() { | ||||||
|  | 		if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 		if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
 | ||||||
|  | 	detailedExitCode := true | ||||||
|  | 
 | ||||||
|  | 	releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false) | ||||||
|  | 
 | ||||||
|  | 	releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm) | ||||||
|  | 	if err != nil { | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fatalErrs := []error{} | ||||||
|  | 
 | ||||||
|  | 	noError := true | ||||||
|  | 	for _, e := range errs { | ||||||
|  | 		switch err := e.(type) { | ||||||
|  | 		case *state.ReleaseError: | ||||||
|  | 			if err.Code != 2 { | ||||||
|  | 				noError = false | ||||||
|  | 				fatalErrs = append(fatalErrs, e) | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			noError = false | ||||||
|  | 			fatalErrs = append(fatalErrs, e) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sync only when there are changes
 | ||||||
|  | 	if noError { | ||||||
|  | 		if len(releases) == 0 && len(releasesToBeDeleted) == 0 { | ||||||
|  | 			// TODO better way to get the logger
 | ||||||
|  | 			logger := c.Logger() | ||||||
|  | 			logger.Infof("") | ||||||
|  | 			logger.Infof("No affected releases") | ||||||
|  | 		} else { | ||||||
|  | 			names := []string{} | ||||||
|  | 			for _, r := range releases { | ||||||
|  | 				names = append(names, fmt.Sprintf("  %s (%s) UPDATED", r.Name, r.Chart)) | ||||||
|  | 			} | ||||||
|  | 			for _, r := range releasesToBeDeleted { | ||||||
|  | 				names = append(names, fmt.Sprintf("  %s (%s) DELETED", r.Name, r.Chart)) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			msg := fmt.Sprintf(`Affected releases are: | ||||||
|  | %s | ||||||
|  | 
 | ||||||
|  | Do you really want to apply? | ||||||
|  |   Helmfile will apply all your changes, as shown above. | ||||||
|  | 
 | ||||||
|  | `, strings.Join(names, "\n")) | ||||||
|  | 			interactive := c.Interactive() | ||||||
|  | 			if !interactive || interactive && r.askForConfirmation(msg) { | ||||||
|  | 				rs := []state.ReleaseSpec{} | ||||||
|  | 				for _, r := range releases { | ||||||
|  | 					rs = append(rs, *r) | ||||||
|  | 				} | ||||||
|  | 				for _, r := range releasesToBeDeleted { | ||||||
|  | 					rs = append(rs, *r) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 				st.Releases = rs | ||||||
|  | 				return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	affectedReleases.DisplayAffectedReleases(c.Logger()) | ||||||
|  | 	return fatalErrs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Diff(c DiffConfigProvider) []error { | ||||||
|  | 	st := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 	ctx := r.ctx | ||||||
|  | 
 | ||||||
|  | 	if !c.SkipDeps() { | ||||||
|  | 		if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 		if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true) | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Sync(c SyncConfigProvider) []error { | ||||||
|  | 	st := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 	ctx := r.ctx | ||||||
|  | 
 | ||||||
|  | 	affectedReleases := state.AffectedReleases{} | ||||||
|  | 	if !c.SkipDeps() { | ||||||
|  | 		if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 		if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) | ||||||
|  | 	affectedReleases.DisplayAffectedReleases(c.Logger()) | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Template(c TemplateConfigProvider) []error { | ||||||
|  | 	state := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 	ctx := r.ctx | ||||||
|  | 
 | ||||||
|  | 	if !c.SkipDeps() { | ||||||
|  | 		if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 		if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	args := argparser.GetArgs(c.Args(), state) | ||||||
|  | 	return state.TemplateReleases(helm, c.Values(), args, c.Concurrency()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Test(c TestConfigProvider) []error { | ||||||
|  | 	cleanup := c.Cleanup() | ||||||
|  | 	timeout := c.Timeout() | ||||||
|  | 	concurrency := c.Concurrency() | ||||||
|  | 
 | ||||||
|  | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
|  | 
 | ||||||
|  | 	return r.state.TestReleases(r.helm, cleanup, timeout, concurrency) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Run) Lint(c LintConfigProvider) []error { | ||||||
|  | 	state := r.state | ||||||
|  | 	helm := r.helm | ||||||
|  | 	ctx := r.ctx | ||||||
|  | 
 | ||||||
|  | 	values := c.Values() | ||||||
|  | 	args := argparser.GetArgs(c.Args(), state) | ||||||
|  | 	workers := c.Concurrency() | ||||||
|  | 	if !c.SkipDeps() { | ||||||
|  | 		if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 		if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 			return errs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 	return state.LintReleases(helm, values, args, workers) | ||||||
|  | } | ||||||
|  | @ -514,6 +514,9 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr | ||||||
| 
 | 
 | ||||||
| // TemplateReleases wrapper for executing helm template on the releases
 | // TemplateReleases wrapper for executing helm template on the releases
 | ||||||
| func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { | func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { | ||||||
|  | 	// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
 | ||||||
|  | 	helm.SetExtraArgs() | ||||||
|  | 
 | ||||||
| 	errs := []error{} | 	errs := []error{} | ||||||
| 	// Create tmp directory and bail immediately if it fails
 | 	// Create tmp directory and bail immediately if it fails
 | ||||||
| 	dir, err := ioutil.TempDir("", "") | 	dir, err := ioutil.TempDir("", "") | ||||||
|  | @ -577,6 +580,9 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues | ||||||
| 
 | 
 | ||||||
| // LintReleases wrapper for executing helm lint on the releases
 | // LintReleases wrapper for executing helm lint on the releases
 | ||||||
| func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { | func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { | ||||||
|  | 	// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
 | ||||||
|  | 	helm.SetExtraArgs() | ||||||
|  | 
 | ||||||
| 	errs := []error{} | 	errs := []error{} | ||||||
| 	// Create tmp directory and bail immediately if it fails
 | 	// Create tmp directory and bail immediately if it fails
 | ||||||
| 	dir, err := ioutil.TempDir("", "") | 	dir, err := ioutil.TempDir("", "") | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue