feat: `helmfile template` (#284)
`helmfile template` runs `helm template` over releases within the helmfiles, and provide you a stream of generated yaml documents of Kubernetes resources via stdout. Resolves #283
This commit is contained in:
		
							parent
							
								
									8a90e5320c
								
							
						
					
					
						commit
						93c5d4c219
					
				|  | @ -6,9 +6,10 @@ import ( | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
| 	"go.uber.org/zap" | 	"go.uber.org/zap" | ||||||
| 	"go.uber.org/zap/zapcore" | 	"go.uber.org/zap/zapcore" | ||||||
| 	"sync" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -143,6 +144,12 @@ func (helm *execer) DecryptSecret(name string) (string, error) { | ||||||
| 	return tmpFile.Name(), err | 	return tmpFile.Name(), err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (helm *execer) TemplateRelease(chart string, flags ...string) error { | ||||||
|  | 	out, err := helm.exec(append([]string{"template", chart}, flags...)...) | ||||||
|  | 	helm.write(out) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (helm *execer) DiffRelease(name, chart string, flags ...string) error { | func (helm *execer) DiffRelease(name, chart string, flags ...string) error { | ||||||
| 	helm.logger.Infof("Comparing %v %v", name, chart) | 	helm.logger.Infof("Comparing %v %v", name, chart) | ||||||
| 	out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...) | 	out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...) | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ type Interface interface { | ||||||
| 	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 | ||||||
|  | 	TemplateRelease(chart string, flags ...string) error | ||||||
| 	Fetch(chart string, flags ...string) error | 	Fetch(chart string, flags ...string) error | ||||||
| 	Lint(chart string, flags ...string) error | 	Lint(chart string, flags ...string) error | ||||||
| 	ReleaseStatus(name string) error | 	ReleaseStatus(name string) error | ||||||
|  |  | ||||||
							
								
								
									
										48
									
								
								main.go
								
								
								
								
							
							
						
						
									
										48
									
								
								main.go
								
								
								
								
							|  | @ -11,6 +11,8 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 
 | 
 | ||||||
|  | 	"io/ioutil" | ||||||
|  | 
 | ||||||
| 	"github.com/roboll/helmfile/args" | 	"github.com/roboll/helmfile/args" | ||||||
| 	"github.com/roboll/helmfile/environment" | 	"github.com/roboll/helmfile/environment" | ||||||
| 	"github.com/roboll/helmfile/helmexec" | 	"github.com/roboll/helmfile/helmexec" | ||||||
|  | @ -19,7 +21,6 @@ import ( | ||||||
| 	"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" | ||||||
| 	"io/ioutil" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -197,6 +198,31 @@ func main() { | ||||||
| 				}) | 				}) | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "template", | ||||||
|  | 			Usage: "template releases from state file against env (helm template)", | ||||||
|  | 			Flags: []cli.Flag{ | ||||||
|  | 				cli.StringFlag{ | ||||||
|  | 					Name:  "args", | ||||||
|  | 					Value: "", | ||||||
|  | 					Usage: "pass args to helm template", | ||||||
|  | 				}, | ||||||
|  | 				cli.StringSliceFlag{ | ||||||
|  | 					Name:  "values", | ||||||
|  | 					Usage: "additional value files to be merged into the command", | ||||||
|  | 				}, | ||||||
|  | 				cli.IntFlag{ | ||||||
|  | 					Name:  "concurrency", | ||||||
|  | 					Value: 0, | ||||||
|  | 					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Action: func(c *cli.Context) error { | ||||||
|  | 				return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error { | ||||||
|  | 					return executeTemplateCommand(c, state, helm) | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "lint", | 			Name:  "lint", | ||||||
| 			Usage: "lint charts from state file (helm lint)", | 			Usage: "lint charts from state file (helm lint)", | ||||||
|  | @ -449,6 +475,26 @@ func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.In | ||||||
| 	return state.SyncReleases(helm, values, workers) | 	return state.SyncReleases(helm, values, workers) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { | ||||||
|  | 	if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if c.GlobalString("helm-binary") != "" { | ||||||
|  | 		helm.SetHelmBinary(c.GlobalString("helm-binary")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	args := args.GetArgs(c.String("args"), state) | ||||||
|  | 	values := c.StringSlice("values") | ||||||
|  | 	workers := c.Int("concurrency") | ||||||
|  | 
 | ||||||
|  | 	return state.TemplateReleases(helm, values, workers, args) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) []error { | func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) []error { | ||||||
| 	args := args.GetArgs(c.String("args"), state) | 	args := args.GetArgs(c.String("args"), state) | ||||||
| 	if len(args) > 0 { | 	if len(args) > 0 { | ||||||
|  |  | ||||||
							
								
								
									
										117
									
								
								state/state.go
								
								
								
								
							
							
						
						
									
										117
									
								
								state/state.go
								
								
								
								
							|  | @ -3,7 +3,6 @@ package state | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/roboll/helmfile/helmexec" |  | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | @ -12,6 +11,8 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/roboll/helmfile/helmexec" | ||||||
|  | 
 | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 
 | 
 | ||||||
| 	"github.com/roboll/helmfile/environment" | 	"github.com/roboll/helmfile/environment" | ||||||
|  | @ -270,6 +271,111 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // TemplateReleases wrapper for executing helm template on the releases
 | ||||||
|  | func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, args []string) []error { | ||||||
|  | 	var wgRelease sync.WaitGroup | ||||||
|  | 	var wgError sync.WaitGroup | ||||||
|  | 	errs := []error{} | ||||||
|  | 	jobQueue := make(chan *ReleaseSpec, len(state.Releases)) | ||||||
|  | 	errQueue := make(chan error) | ||||||
|  | 
 | ||||||
|  | 	if workerLimit < 1 { | ||||||
|  | 		workerLimit = len(state.Releases) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	wgRelease.Add(len(state.Releases)) | ||||||
|  | 
 | ||||||
|  | 	// Create tmp directory and bail immediately if it fails
 | ||||||
|  | 	dir, err := ioutil.TempDir("", "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 	defer os.RemoveAll(dir) | ||||||
|  | 
 | ||||||
|  | 	for w := 1; w <= workerLimit; w++ { | ||||||
|  | 		go func() { | ||||||
|  | 			for release := range jobQueue { | ||||||
|  | 				errs := []error{} | ||||||
|  | 				flags, err := state.flagsForTemplate(helm, release) | ||||||
|  | 				if err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 				} | ||||||
|  | 				for _, value := range additionalValues { | ||||||
|  | 					valfile, err := filepath.Abs(value) | ||||||
|  | 					if err != nil { | ||||||
|  | 						errs = append(errs, err) | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					if _, err := os.Stat(valfile); os.IsNotExist(err) { | ||||||
|  | 						errs = append(errs, err) | ||||||
|  | 					} | ||||||
|  | 					flags = append(flags, "--values", valfile) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				chartPath := "" | ||||||
|  | 				if pathExists(normalizeChart(state.basePath, release.Chart)) { | ||||||
|  | 					chartPath = normalizeChart(state.basePath, release.Chart) | ||||||
|  | 				} else { | ||||||
|  | 					fetchFlags := []string{} | ||||||
|  | 					if release.Version != "" { | ||||||
|  | 						chartPath = path.Join(dir, release.Name, release.Version, release.Chart) | ||||||
|  | 						fetchFlags = append(fetchFlags, "--version", release.Version) | ||||||
|  | 					} else { | ||||||
|  | 						chartPath = path.Join(dir, release.Name, "latest", release.Chart) | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					// only fetch chart if it is not already fetched
 | ||||||
|  | 					if _, err := os.Stat(chartPath); os.IsNotExist(err) { | ||||||
|  | 						fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath) | ||||||
|  | 						if err := helm.Fetch(release.Chart, fetchFlags...); err != nil { | ||||||
|  | 							errs = append(errs, err) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					chartPath = path.Join(chartPath, chartNameWithoutRepository(release.Chart)) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if len(args) > 0 { | ||||||
|  | 					helm.SetExtraArgs(args...) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if len(errs) == 0 { | ||||||
|  | 					if err := helm.TemplateRelease(chartPath, flags...); err != nil { | ||||||
|  | 						errs = append(errs, err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				for _, err := range errs { | ||||||
|  | 					errQueue <- err | ||||||
|  | 				} | ||||||
|  | 				wgRelease.Done() | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	} | ||||||
|  | 	wgError.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		for err := range errQueue { | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 		wgError.Done() | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	for i := 0; i < len(state.Releases); i++ { | ||||||
|  | 		jobQueue <- &state.Releases[i] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	close(jobQueue) | ||||||
|  | 	wgRelease.Wait() | ||||||
|  | 
 | ||||||
|  | 	close(errQueue) | ||||||
|  | 	wgError.Wait() | ||||||
|  | 
 | ||||||
|  | 	if len(errs) != 0 { | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // DiffReleases wrapper for executing helm diff on the releases
 | // DiffReleases wrapper for executing helm diff on the releases
 | ||||||
| func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) []error { | func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) []error { | ||||||
| 	var wgRelease sync.WaitGroup | 	var wgRelease sync.WaitGroup | ||||||
|  | @ -691,6 +797,15 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas | ||||||
| 	return append(flags, common...), nil | 	return append(flags, common...), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (state *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { | ||||||
|  | 	flags := []string{} | ||||||
|  | 	common, err := state.namespaceAndValuesFlags(helm, release) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return append(flags, common...), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (state *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { | func (state *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { | ||||||
| 	flags := []string{} | 	flags := []string{} | ||||||
| 	if release.Version != "" { | 	if release.Version != "" { | ||||||
|  |  | ||||||
|  | @ -6,8 +6,9 @@ import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"github.com/roboll/helmfile/helmexec" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/roboll/helmfile/helmexec" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var logger = helmexec.NewLogger(os.Stdout, "warn") | var logger = helmexec.NewLogger(os.Stdout, "warn") | ||||||
|  | @ -537,7 +538,9 @@ func (helm *mockHelmExec) Fetch(chart string, flags ...string) error { | ||||||
| func (helm *mockHelmExec) Lint(chart string, flags ...string) error { | func (helm *mockHelmExec) Lint(chart string, flags ...string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | func (helm *mockHelmExec) TemplateRelease(chart string, flags ...string) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| func TestHelmState_SyncRepos(t *testing.T) { | func TestHelmState_SyncRepos(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name  string | 		name  string | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue