feat: add --output-dir on template command (#693)

It generates templates in a subdirectory named "stateFileName-stateFileHash-releaseName"
This commit is contained in:
OlivierB 2019-07-11 02:07:46 +02:00 committed by KUOKA Yusuke
parent 63b5040ec4
commit 2f9f52033c
11 changed files with 252 additions and 16 deletions

View File

@ -200,6 +200,10 @@ func main() {
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.StringFlag{
Name: "output-dir",
Usage: "output directory to pass to helm template (helm template --output-dir)",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
@ -440,6 +444,10 @@ func (c configImpl) Args() string {
return c.c.String("args")
}
func (c configImpl) OutputDir() string {
return c.c.String("output-dir")
}
func (c configImpl) Concurrency() int {
return c.c.Int("concurrency")
}

View File

@ -45,6 +45,8 @@ type App struct {
chdir func(string) error
remote *remote.Remote
helmExecer helmexec.Interface
}
func New(conf ConfigProvider) *App {
@ -59,6 +61,9 @@ func New(conf ConfigProvider) *App {
FileOrDir: conf.FileOrDir(),
ValuesFiles: conf.ValuesFiles(),
Set: conf.Set(),
helmExecer: helmexec.New(conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
Logger: conf.Logger(),
}),
})
}
@ -274,7 +279,7 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
ctx := context{a, st}
helm := helmexec.New(a.Logger, a.KubeContext)
helm := a.helmExecer
if err != nil {
switch stateLoadErr := err.(type) {

View File

@ -1,6 +1,7 @@
package app
import (
"bytes"
"fmt"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
@ -8,8 +9,10 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"testing"
"go.uber.org/zap"
"gotest.tools/env"
)
@ -1738,3 +1741,158 @@ services:
}
}
}
type configImpl struct {
}
func (c configImpl) Values() []string {
return []string{}
}
func (c configImpl) Args() string {
return "some args"
}
func (c configImpl) SkipDeps() bool {
return true
}
func (c configImpl) OutputDir() string {
return "output/subdir"
}
func (c configImpl) Concurrency() int {
return 1
}
// Mocking the command-line runner
type mockRunner struct {
output []byte
err error
}
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
return []byte{}, nil
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface {
execer := helmexec.New(logger, kubeContext, &mockRunner{})
return execer
}
// mocking helmexec.Interface
type listKey struct {
filter string
flags string
}
type mockHelmExec struct {
templated []mockTemplates
updateDepsCallbacks map[string]func(string) error
}
type mockTemplates struct {
flags []string
}
func (helm *mockHelmExec) TemplateRelease(chart string, flags ...string) error {
helm.templated = append(helm.templated, mockTemplates{flags: flags})
return nil
}
func (helm *mockHelmExec) UpdateDeps(chart string) error {
return nil
}
func (helm *mockHelmExec) BuildDeps(chart string) error {
return nil
}
func (helm *mockHelmExec) SetExtraArgs(args ...string) {
return
}
func (helm *mockHelmExec) SetHelmBinary(bin string) {
return
}
func (helm *mockHelmExec) AddRepo(name, repository, certfile, keyfile, username, password string) error {
return nil
}
func (helm *mockHelmExec) UpdateRepo() error {
return nil
}
func (helm *mockHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
return "", nil
}
func (helm *mockHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
return "", nil
}
func (helm *mockHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Lint(chart string, flags ...string) error {
return nil
}
func TestTemplate_SingleStateFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: myrelease1
chart: mychart1
- name: myrelease2
chart: mychart1
`,
}
var helm = &mockHelmExec{}
var wantReleases = []mockTemplates{
{[]string{"--name", "myrelease1", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease1"}},
{[]string{"--name", "myrelease2", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease2"}},
}
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
app := appWithFs(&App{
glob: filepath.Glob,
abs: filepath.Abs,
KubeContext: "default",
Env: "default",
Logger: logger,
helmExecer: helm,
}, files)
app.Template(configImpl{})
for i := range wantReleases {
for j := range wantReleases[i].flags {
if j == 3 {
matched, _ := regexp.Match(wantReleases[i].flags[j], []byte(helm.templated[i].flags[j]))
if !matched {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
} else if wantReleases[i].flags[j] != helm.templated[i].flags[j] {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
}
}
}

View File

@ -109,6 +109,7 @@ type TemplateConfigProvider interface {
Values() []string
SkipDeps() bool
OutputDir() string
concurrencyConfig
}

View File

@ -270,7 +270,7 @@ func (r *Run) Template(c TemplateConfigProvider) []error {
}
args := argparser.GetArgs(c.Args(), state)
return state.TemplateReleases(helm, c.Values(), args, c.Concurrency())
return state.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency())
}
func (r *Run) Test(c TestConfigProvider) []error {

View File

@ -44,14 +44,12 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
}
// New for running helm commands
func New(logger *zap.SugaredLogger, kubeContext string) *execer {
func New(logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{
helmBinary: command,
logger: logger,
kubeContext: kubeContext,
runner: &ShellRunner{
logger: logger,
},
runner: runner,
}
}

View File

@ -24,8 +24,7 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
execer := New(logger, kubeContext)
execer.runner = &mockRunner{}
execer := New(logger, kubeContext, &mockRunner{})
return execer
}
@ -34,7 +33,9 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(logger, "dev")
helm := New(logger, "dev", &ShellRunner{
Logger: logger,
})
if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext")
}
@ -47,7 +48,11 @@ func TestNewHelmExec(t *testing.T) {
}
func Test_SetExtraArgs(t *testing.T) {
helm := New(NewLogger(os.Stdout, "info"), "dev")
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
helm.SetExtraArgs()
if len(helm.extra) != 0 {
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
@ -63,7 +68,11 @@ func Test_SetExtraArgs(t *testing.T) {
}
func Test_SetHelmBinary(t *testing.T) {
helm := New(NewLogger(os.Stdout, "info"), "dev")
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not helm")
}
@ -478,3 +487,16 @@ func Test_mergeEnv(t *testing.T) {
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
}
}
func Test_Template(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TemplateRelease("path/to/chart", "--values", "file.yml")
expected := `exec: helm template path/to/chart --values file.yml --kube-context dev
exec: helm template path/to/chart --values file.yml --kube-context dev:
`
if buffer.String() != expected {
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}

View File

@ -25,7 +25,7 @@ type Runner interface {
type ShellRunner struct {
Dir string
logger *zap.SugaredLogger
Logger *zap.SugaredLogger
}
// Execute a shell command
@ -33,7 +33,7 @@ func (shell ShellRunner) Execute(cmd string, args []string, env map[string]strin
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
preparedCmd.Env = mergeEnv(os.Environ(), env)
return combinedOutput(preparedCmd, shell.logger)
return combinedOutput(preparedCmd, shell.Logger)
}
func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {

View File

@ -185,7 +185,9 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
}
if len(envSpec.Secrets) > 0 {
helm := helmexec.New(st.logger, "")
helm := helmexec.New(st.logger, "", &helmexec.ShellRunner{
Logger: st.logger,
})
var envSecretFiles []string
for _, urlOrPath := range envSpec.Secrets {

View File

@ -1,8 +1,11 @@
package state
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
@ -534,7 +537,7 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
}
// 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, outputDir string, 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()
@ -569,6 +572,7 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
if err != nil {
errs = append(errs, err)
}
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
@ -581,6 +585,17 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
flags = append(flags, "--values", valfile)
}
if len(outputDir) > 0 {
releaseOutputDir, err := st.GenerateOutputDir(outputDir, release)
if err != nil {
errs = append(errs, err)
}
flags = append(flags, "--output-dir", releaseOutputDir)
st.logger.Debugf("Generating templates to : %s\n", releaseOutputDir)
os.Mkdir(releaseOutputDir, 0755)
}
if len(errs) == 0 {
if err := helm.TemplateRelease(temp[release.Name], flags...); err != nil {
errs = append(errs, err)
@ -1560,3 +1575,28 @@ func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) erro
}
return nil
}
func (st *HelmState) GenerateOutputDir(outputDir string, release ReleaseSpec) (string, error) {
// get absolute path of state file to generate a hash
// use this hash to write helm output in a specific directory by state file and release name
// ie. in a directory named stateFileName-stateFileHash-releaseName
stateAbsPath, err := filepath.Abs(st.FilePath)
if err != nil {
return stateAbsPath, err
}
hasher := sha1.New()
io.WriteString(hasher, stateAbsPath)
var stateFileExtension = filepath.Ext(st.FilePath)
var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)]
var sb strings.Builder
sb.WriteString(stateFileName)
sb.WriteString("-")
sb.WriteString(hex.EncodeToString(hasher.Sum(nil))[:8])
sb.WriteString("-")
sb.WriteString(release.Name)
return path.Join(outputDir, sb.String()), nil
}

View File

@ -524,7 +524,9 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
}
helm := helmexec.New(logger, "default")
helm := helmexec.New(logger, "default", &helmexec.ShellRunner{
Logger: logger,
})
args, err := state.flagsForUpgrade(helm, tt.release, 0)
if err != nil {
t.Errorf("unexpected error flagsForUpgade: %v", err)