feat: add --output-dir on template command (#693)
It generates templates in a subdirectory named "stateFileName-stateFileHash-releaseName"
This commit is contained in:
parent
63b5040ec4
commit
2f9f52033c
8
main.go
8
main.go
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ type TemplateConfigProvider interface {
|
|||
|
||||
Values() []string
|
||||
SkipDeps() bool
|
||||
OutputDir() string
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue