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:
Karl Stoney 2018-09-04 03:31:43 +01:00 committed by KUOKA Yusuke
parent 8a90e5320c
commit 93c5d4c219
5 changed files with 177 additions and 5 deletions

View File

@ -6,9 +6,10 @@ import (
"os"
"strings"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"sync"
)
const (
@ -143,6 +144,12 @@ func (helm *execer) DecryptSecret(name string) (string, error) {
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 {
helm.logger.Infof("Comparing %v %v", name, chart)
out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...)

View File

@ -10,6 +10,7 @@ type Interface interface {
UpdateDeps(chart string) error
SyncRelease(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
Lint(chart string, flags ...string) error
ReleaseStatus(name string) error

48
main.go
View File

@ -11,6 +11,8 @@ import (
"os/exec"
"io/ioutil"
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
@ -19,7 +21,6 @@ import (
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io/ioutil"
)
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",
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)
}
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 {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {

View File

@ -3,7 +3,6 @@ package state
import (
"errors"
"fmt"
"github.com/roboll/helmfile/helmexec"
"io/ioutil"
"os"
"path"
@ -12,6 +11,8 @@ import (
"strings"
"sync"
"github.com/roboll/helmfile/helmexec"
"regexp"
"github.com/roboll/helmfile/environment"
@ -270,6 +271,111 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
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
func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) []error {
var wgRelease sync.WaitGroup
@ -691,6 +797,15 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas
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) {
flags := []string{}
if release.Version != "" {

View File

@ -6,8 +6,9 @@ import (
"testing"
"errors"
"github.com/roboll/helmfile/helmexec"
"strings"
"github.com/roboll/helmfile/helmexec"
)
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 {
return nil
}
func (helm *mockHelmExec) TemplateRelease(chart string, flags ...string) error {
return nil
}
func TestHelmState_SyncRepos(t *testing.T) {
tests := []struct {
name string