Add --output-dir-template flag to the fetch command (#456)

* Add --output-dir-template flag to the fetch command

Signed-off-by: elchenberg <elchenberg@users.noreply.github.com>
This commit is contained in:
Helge Eichelberg 2022-11-05 11:55:02 +01:00 committed by GitHub
parent 712ee3a0a2
commit 8f59a1c18a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 28 deletions

View File

@ -5,6 +5,7 @@ import (
"github.com/helmfile/helmfile/pkg/app"
"github.com/helmfile/helmfile/pkg/config"
"github.com/helmfile/helmfile/pkg/state"
)
// NewFetchCmd returns fetch subcmd
@ -34,6 +35,7 @@ func NewFetchCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.IntVar(&fetchOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited")
f.BoolVar(&fetchOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`)
f.StringVar(&fetchOptions.OutputDir, "output-dir", "", "directory to store charts (default: temporary directory which is deleted when the command terminates)")
f.StringVar(&fetchOptions.OutputDirTemplate, "output-dir-template", state.DefaultFetchOutputDirTemplate, "go text template for generating the output directory")
return cmd
}

View File

@ -345,11 +345,12 @@ func (a *App) Lint(c LintConfigProvider) error {
func (a *App) Fetch(c FetchConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
prepErr := run.withPreparedCharts("pull", state.ChartPrepareOptions{
ForceDownload: true,
SkipRepos: c.SkipDeps(),
SkipDeps: c.SkipDeps(),
OutputDir: c.OutputDir(),
Concurrency: c.Concurrency(),
ForceDownload: true,
SkipRepos: c.SkipDeps(),
SkipDeps: c.SkipDeps(),
OutputDir: c.OutputDir(),
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
}, func() {
})

View File

@ -179,6 +179,7 @@ type LintConfigProvider interface {
type FetchConfigProvider interface {
SkipDeps() bool
OutputDir() string
OutputDirTemplate() string
concurrencyConfig
}

View File

@ -8,6 +8,8 @@ type FetchOptions struct {
SkipDeps bool
// OutputDir is the output directory
OutputDir string
// OutputDirTemplate is the go template to generate the path of output directory
OutputDirTemplate string
}
// NewFetchOptions creates a new Apply
@ -43,3 +45,8 @@ func (c *FetchImpl) SkipDeps() bool {
func (c *FetchImpl) OutputDir() string {
return c.FetchOptions.OutputDir
}
// OutputDirTemplate returns the go template to generate the path of output directory
func (c *FetchImpl) OutputDirTemplate() string {
return c.FetchOptions.OutputDirTemplate
}

View File

@ -359,6 +359,16 @@ const MissingFileHandlerWarn = "Warn"
// MissingFileHandlerDebug is the debug returned when a file is missing
const MissingFileHandlerDebug = "Debug"
var DefaultFetchOutputDirTemplate = path.Join(
"{{ .OutputDir }}{{ if .Release.TillerNamespace }}",
"{{ .Release.TillerNamespace }}{{ end }}{{ if .Release.Namespace }}",
"{{ .Release.Namespace }}{{ end }}{{ if .Release.KubeContext }}",
"{{ .Release.KubeContext }}{{ end }}",
"{{ .Release.Name }}",
"{{ .ChartName }}",
"{{ or .Release.Version \"latest\" }}",
)
func (st *HelmState) ApplyOverrides(spec *ReleaseSpec) {
if st.OverrideKubeContext != "" {
spec.KubeContext = st.OverrideKubeContext
@ -983,6 +993,7 @@ type ChartPrepareOptions struct {
Wait bool
WaitForJobs bool
OutputDir string
OutputDirTemplate string
IncludeTransitiveNeeds bool
Concurrency int
}
@ -1211,31 +1222,12 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
// For helm 2, we `helm fetch` with the version flags and call `helm template`
// WITHOUT the version flags.
} else {
pathElems := []string{
dir,
chartPath, err = generateChartPath(chartName, dir, release, opts.OutputDirTemplate)
if err != nil {
results <- &chartPrepareResult{err: err}
return
}
if release.TillerNamespace != "" {
pathElems = append(pathElems, release.TillerNamespace)
}
if release.Namespace != "" {
pathElems = append(pathElems, release.Namespace)
}
if release.KubeContext != "" {
pathElems = append(pathElems, release.KubeContext)
}
chartVersion := "latest"
if release.Version != "" {
chartVersion = release.Version
}
pathElems = append(pathElems, release.Name, chartName, chartVersion)
chartPath = path.Join(pathElems...)
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
fetchFlags := st.chartVersionFlags(release)
@ -1244,6 +1236,8 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
results <- &chartPrepareResult{err: err}
return
}
} else {
st.logger.Infof("\"%s\" has not been downloaded because the output directory \"%s\" already exists", chartName, chartPath)
}
// Set chartPath to be the path containing Chart.yaml, if found
@ -3181,6 +3175,36 @@ func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, o
return buf.String(), nil
}
// generateChartPath generates the path of the output directory of the `helmfile fetch` command.
// It uses a go template with data from the chart name, output directory and release spec.
// If no template was provided (via the `--output-dir-template` flag) it uses the DefaultFetchOutputDirTemplate.
func generateChartPath(chartName string, outputDir string, release *ReleaseSpec, outputDirTemplate string) (string, error) {
if outputDirTemplate == "" {
outputDirTemplate = DefaultFetchOutputDirTemplate
}
t, err := template.New("output-dir-template").Parse(outputDirTemplate)
if err != nil {
return "", fmt.Errorf("parsing output-dir-template template %q: %w", outputDirTemplate, err)
}
buf := &bytes.Buffer{}
data := struct {
ChartName string
OutputDir string
Release ReleaseSpec
}{
ChartName: chartName,
OutputDir: outputDir,
Release: *release,
}
if err := t.Execute(buf, data); err != nil {
return "", fmt.Errorf("executing output-dir-template template: %w", err)
}
return buf.String(), nil
}
func (st *HelmState) GenerateOutputFilePath(release *ReleaseSpec, outputFileTemplate string) (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

View File

@ -2812,3 +2812,149 @@ func TestGetOCIQualifiedChartName(t *testing.T) {
})
}
}
func TestGenerateChartPath(t *testing.T) {
tests := []struct {
testName string
chartName string
release *ReleaseSpec
outputDir string
outputDirTemplate string
wantErr bool
expected string
}{
{
testName: "PathGeneratedWithGivenOutputDirAndDefaultReleaseVersion",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseVersion",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Version: "0.0.0"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-name/chart-name/0.0.0",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseTillerNamespace",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", TillerNamespace: "tiller-namespace"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/tiller-namespace/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseNamespace",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Namespace: "release-namespace"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-namespace/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseTillerNamespaceAndGivenReleaseNamespace",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", TillerNamespace: "tiller-namespace", Namespace: "release-namespace"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/tiller-namespace/release-namespace/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseNamespaceAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Namespace: "release-namespace", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-namespace/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseTillerNamespaceAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", TillerNamespace: "tiller-namespace", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/tiller-namespace/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseTillerNamespaceAndGivenReleaseNamespaceAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", TillerNamespace: "tiller-namespace", Namespace: "release-namespace", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/tiller-namespace/release-namespace/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenOutputDirTemplateWithFieldNameOutputDir",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}",
wantErr: false,
expected: "/output-dir",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenOutputDirTemplateWithFieldNamesOutputDirAndReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Release.Name }}",
wantErr: false,
expected: "/output-dir/release-name",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithFieldNamesOutputDir",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .OutputDir }}",
wantErr: false,
expected: "",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithFieldNameReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .Release.Name }}",
wantErr: false,
expected: "release-name",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithStringAndFieldNameReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "./charts/{{ .Release.Name }}",
wantErr: false,
expected: "./charts/release-name",
},
{
testName: "ErrorReturnedWithGivenInvalidOutputDirTemplate",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .OutputDir }",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
got, err := generateChartPath(tt.chartName, tt.outputDir, tt.release, tt.outputDirTemplate)
if tt.wantErr {
require.Errorf(t, err, "GenerateChartPath() error \"%v\", want error", err)
} else {
require.NoError(t, err, "GenerateChartPath() error \"%v\", want no error", err)
}
require.Equalf(t, tt.expected, got, "GenerateChartPath() got \"%v\", want \"%v\"", got, tt.expected)
})
}
}