Add experimental write-values command for writing values files only (#1469)
Ref #1460
This commit is contained in:
parent
832dcf47a5
commit
0fad9f0544
34
main.go
34
main.go
|
|
@ -257,6 +257,36 @@ func main() {
|
||||||
return run.Template(c)
|
return run.Template(c)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "write-values",
|
||||||
|
Usage: "write values files for releases. Similar to `helmfile template`, write values files instead of manifests.",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "set",
|
||||||
|
Usage: "additional values to be merged into the command",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "values",
|
||||||
|
Usage: "additional value files to be merged into the command",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "output-file-template",
|
||||||
|
Usage: "go text template for generating the output file. Default: {{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}/{{ .Release.Name}}.yaml",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "concurrency",
|
||||||
|
Value: 0,
|
||||||
|
Usage: "maximum number of concurrent downloads of release charts",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "skip-deps",
|
||||||
|
Usage: "skip running `helm repo update` and `helm dependency build`",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: action(func(run *app.App, c configImpl) error {
|
||||||
|
return run.WriteValues(c)
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "lint",
|
Name: "lint",
|
||||||
Usage: "lint charts from state file (helm lint)",
|
Usage: "lint charts from state file (helm lint)",
|
||||||
|
|
@ -574,6 +604,10 @@ func (c configImpl) OutputDirTemplate() string {
|
||||||
return c.c.String("output-dir-template")
|
return c.c.String("output-dir-template")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c configImpl) OutputFileTemplate() string {
|
||||||
|
return c.c.String("output-file-template")
|
||||||
|
}
|
||||||
|
|
||||||
func (c configImpl) Validate() bool {
|
func (c configImpl) Validate() bool {
|
||||||
return c.c.Bool("validate")
|
return c.c.Bool("validate")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,25 @@ func (a *App) Template(c TemplateConfigProvider) error {
|
||||||
}, SetFilter(true))
|
}, SetFilter(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) WriteValues(c WriteValuesConfigProvider) error {
|
||||||
|
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||||
|
// `helm template` in helm v2 does not support local chart.
|
||||||
|
// So, we set forceDownload=true for helm v2 only
|
||||||
|
prepErr := run.withPreparedCharts("write-values", state.ChartPrepareOptions{
|
||||||
|
ForceDownload: !run.helm.IsHelm3(),
|
||||||
|
SkipRepos: c.SkipDeps(),
|
||||||
|
}, func() {
|
||||||
|
ok, errs = a.writeValues(run, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
if prepErr != nil {
|
||||||
|
errs = append(errs, prepErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}, SetFilter(true))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Lint(c LintConfigProvider) error {
|
func (a *App) Lint(c LintConfigProvider) error {
|
||||||
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
|
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
|
||||||
// `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here
|
// `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here
|
||||||
|
|
@ -1418,6 +1437,64 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) {
|
||||||
return true, errs
|
return true, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) writeValues(r *Run, c WriteValuesConfigProvider) (bool, []error) {
|
||||||
|
st := r.state
|
||||||
|
helm := r.helm
|
||||||
|
|
||||||
|
allReleases := st.GetReleasesWithOverrides()
|
||||||
|
|
||||||
|
toRender, err := a.getSelectedReleases(r)
|
||||||
|
if err != nil {
|
||||||
|
return false, []error{err}
|
||||||
|
}
|
||||||
|
if len(toRender) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do build deps and prepare only on selected releases so that we won't waste time
|
||||||
|
// on running various helm commands on unnecessary releases
|
||||||
|
st.Releases = toRender
|
||||||
|
|
||||||
|
releasesToWrite := map[string]state.ReleaseSpec{}
|
||||||
|
for _, r := range toRender {
|
||||||
|
id := state.ReleaseToID(&r)
|
||||||
|
if r.Installed != nil && !*r.Installed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
releasesToWrite[id] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
|
||||||
|
st.Releases = allReleases
|
||||||
|
|
||||||
|
if len(releasesToWrite) > 0 {
|
||||||
|
_, writeErrs := withDAG(st, helm, a.Logger, false, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
|
||||||
|
var rs []state.ReleaseSpec
|
||||||
|
|
||||||
|
for _, r := range subst.Releases {
|
||||||
|
if r2, ok := releasesToWrite[state.ReleaseToID(&r)]; ok {
|
||||||
|
rs = append(rs, r2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subst.Releases = rs
|
||||||
|
|
||||||
|
opts := &state.WriteValuesOpts{
|
||||||
|
Set: c.Set(),
|
||||||
|
OutputFileTemplate: c.OutputFileTemplate(),
|
||||||
|
}
|
||||||
|
return subst.WriteReleasesValues(helm, c.Values(), opts)
|
||||||
|
}))
|
||||||
|
|
||||||
|
if writeErrs != nil && len(writeErrs) > 0 {
|
||||||
|
errs = append(errs, writeErrs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, errs
|
||||||
|
}
|
||||||
|
|
||||||
func fileExistsAt(path string) bool {
|
func fileExistsAt(path string) bool {
|
||||||
fileInfo, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
return err == nil && fileInfo.Mode().IsRegular()
|
return err == nil && fileInfo.Mode().IsRegular()
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,13 @@ type TemplateConfigProvider interface {
|
||||||
concurrencyConfig
|
concurrencyConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WriteValuesConfigProvider interface {
|
||||||
|
Values() []string
|
||||||
|
Set() []string
|
||||||
|
OutputFileTemplate() string
|
||||||
|
SkipDeps() bool
|
||||||
|
}
|
||||||
|
|
||||||
type StatusesConfigProvider interface {
|
type StatusesConfigProvider interface {
|
||||||
Args() string
|
Args() string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
@ -1225,6 +1226,103 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WriteValuesOpts struct {
|
||||||
|
Set []string
|
||||||
|
OutputFileTemplate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriteValuesOpt interface{ Apply(*WriteValuesOpts) }
|
||||||
|
|
||||||
|
func (o *WriteValuesOpts) Apply(opts *WriteValuesOpts) {
|
||||||
|
*opts = *o
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteReleasesValues writes values files for releases
|
||||||
|
func (st *HelmState) WriteReleasesValues(helm helmexec.Interface, additionalValues []string, opt ...WriteValuesOpt) []error {
|
||||||
|
opts := &WriteValuesOpts{}
|
||||||
|
for _, o := range opt {
|
||||||
|
o.Apply(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range st.Releases {
|
||||||
|
release := &st.Releases[i]
|
||||||
|
|
||||||
|
if !release.Desired() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
st.ApplyOverrides(release)
|
||||||
|
|
||||||
|
generatedFiles, err := st.generateValuesFiles(helm, release, i)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
st.removeFiles(generatedFiles)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, value := range additionalValues {
|
||||||
|
valfile, err := filepath.Abs(value)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(valfile); os.IsNotExist(err) {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
generatedFiles = append(generatedFiles, valfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputValuesFile, err := st.GenerateOutputFilePath(release, opts.OutputFileTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(outputValuesFile), 0755); err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
st.logger.Infof("Writing values file %s", outputValuesFile)
|
||||||
|
|
||||||
|
merged := map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, f := range generatedFiles {
|
||||||
|
src := map[string]interface{}{}
|
||||||
|
|
||||||
|
srcBytes, err := st.readFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return []error{fmt.Errorf("reading %s: %w", f, err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(srcBytes, &src); err != nil {
|
||||||
|
return []error{fmt.Errorf("unmarshalling yaml %s: %w", f, err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergo.Merge(&merged, &src, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||||
|
return []error{fmt.Errorf("merging %s: %w", f, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
y := yaml.NewEncoder(&buf)
|
||||||
|
if err := y.Encode(merged); err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(outputValuesFile, buf.Bytes(), 0644); err != nil {
|
||||||
|
return []error{fmt.Errorf("writing values file %s: %w", outputValuesFile, err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := st.TriggerCleanupEvent(release, "write-values"); err != nil {
|
||||||
|
st.logger.Warnf("warn: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type LintOpts struct {
|
type LintOpts struct {
|
||||||
Set []string
|
Set []string
|
||||||
}
|
}
|
||||||
|
|
@ -2580,6 +2678,68 @@ func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, o
|
||||||
return buf.String(), nil
|
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
|
||||||
|
// 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)]
|
||||||
|
|
||||||
|
sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8]
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(stateFileName)
|
||||||
|
sb.WriteString("-")
|
||||||
|
sb.WriteString(sha1sum)
|
||||||
|
sb.WriteString("-")
|
||||||
|
sb.WriteString(release.Name)
|
||||||
|
|
||||||
|
if outputFileTemplate == "" {
|
||||||
|
outputFileTemplate = filepath.Join("{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}", "{{ .Release.Name}}.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.New("output-file").Parse(outputFileTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing output-file templmate")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
BaseName string
|
||||||
|
Path string
|
||||||
|
AbsPath string
|
||||||
|
AbsPathSHA1 string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
State state
|
||||||
|
Release *ReleaseSpec
|
||||||
|
}{
|
||||||
|
State: state{
|
||||||
|
BaseName: stateFileName,
|
||||||
|
Path: st.FilePath,
|
||||||
|
AbsPath: stateAbsPath,
|
||||||
|
AbsPathSHA1: sha1sum,
|
||||||
|
},
|
||||||
|
Release: release,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Execute(buf, data); err != nil {
|
||||||
|
return "", fmt.Errorf("executing output-file template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (st *HelmState) ToYaml() (string, error) {
|
func (st *HelmState) ToYaml() (string, error) {
|
||||||
if result, err := yaml.Marshal(st); err != nil {
|
if result, err := yaml.Marshal(st); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue