diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 65575a5c1..dfaef04a1 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -27,9 +27,11 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/postrender" ) const outputFlag = "output" +const postRenderFlag = "post-renderer" func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") @@ -94,3 +96,33 @@ func (o *outputValue) Set(s string) error { *o = outputValue(outfmt) return nil } + +func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) { + cmd.Flags().Var(&postRenderer{varRef}, postRenderFlag, "the path to an executable to be used for post rendering. If a non-absolute path is provided, the plugin directory and $PATH will be searched") + // Setup shell completion for the flag + cmd.MarkFlagCustom(outputFlag, "__helm_output_options") +} + +type postRenderer struct { + renderer *postrender.PostRenderer +} + +func (p postRenderer) String() string { + return "exec" +} + +func (p postRenderer) Type() string { + return "postrenderer" +} + +func (p postRenderer) Set(s string) error { + if s == "" { + return nil + } + pr, err := postrender.NewExec(s) + if err != nil { + return err + } + *p.renderer = pr + return nil +} diff --git a/cmd/helm/install.go b/cmd/helm/install.go index dbdfb3418..ec2c75a12 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -130,6 +130,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addInstallFlags(cmd.Flags(), client, valueOpts) bindOutputFlag(cmd, &outfmt) + bindPostRenderFlag(cmd, &client.PostRenderer) return cmd } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 6c967b796..54badb32c 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -108,6 +108,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Devel = client.Devel instClient.Namespace = client.Namespace instClient.Atomic = client.Atomic + instClient.PostRenderer = client.PostRenderer rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { @@ -176,6 +177,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) + bindPostRenderFlag(cmd, &client.PostRenderer) return cmd } diff --git a/pkg/action/install.go b/pkg/action/install.go index 55a44aaed..49c9b5728 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -39,6 +39,7 @@ import ( "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/getter" kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/repo" @@ -94,6 +95,7 @@ type Install struct { // Used by helm template to add the release as part of OutputDir path // OutputDir/ UseReleaseName bool + PostRenderer postrender.PostRenderer } // ChartPathOptions captures common options used for controlling chart paths @@ -225,7 +227,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. rel := i.createRelease(chrt, vals) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() @@ -429,7 +431,7 @@ func (i *Install) replaceRelease(rel *release.Release) error { } // renderResources renders the templates in a chart -func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName string, outputDir string, subNotes, useReleaseName bool, includeCrds bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer) ([]*release.Hook, *bytes.Buffer, string, error) { hs := []*release.Hook{} b := bytes.NewBuffer(nil) @@ -525,6 +527,10 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values if useReleaseName { newDir = filepath.Join(outputDir, releaseName) } + // NOTE: We do not have to worry about the post-renderer because + // output dir is only used by `helm template`. In the next major + // release, we should move this logic to template only as it is not + // used by install or upgrade err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) if err != nil { return hs, b, "", err @@ -533,6 +539,13 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values } } + if pr != nil { + b, err = pr.Run(b) + if err != nil { + return hs, b, notes, errors.Wrap(err, "error while running post render on files") + } + } + return hs, b, notes, nil } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3e8a04a56..825920793 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" ) @@ -59,6 +60,7 @@ type Upgrade struct { CleanupOnFail bool SubNotes bool Description string + PostRenderer postrender.PostRenderer } // NewUpgrade creates a new Upgrade object with the given configuration. @@ -161,7 +163,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer) if err != nil { return nil, nil, err } diff --git a/pkg/postrender/exec.go b/pkg/postrender/exec.go new file mode 100644 index 000000000..dde30f6e3 --- /dev/null +++ b/pkg/postrender/exec.go @@ -0,0 +1,99 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package postrender + +import ( + "bytes" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/helmpath" +) + +type execRender struct { + binaryPath string +} + +// NewExec returns a PostRenderer implementation that calls the provided binary. +// It returns an error if the binary cannot be found. If the provided path does +// not contain any separators, it will search first in the plugins directory, +// then in $PATH, otherwise it will resolve any relative paths to a fully +// qualified path +func NewExec(binaryPath string) (PostRenderer, error) { + fullPath, err := getFullPath(binaryPath) + if err != nil { + return nil, err + } + return &execRender{fullPath}, nil +} + +// Run the configured binary for the post render +func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { + cmd := exec.Command(p.binaryPath) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + var postRendered = &bytes.Buffer{} + var stderr = &bytes.Buffer{} + cmd.Stdout = postRendered + cmd.Stderr = stderr + + go func() { + defer stdin.Close() + io.Copy(stdin, renderedManifests) + }() + err = cmd.Run() + if err != nil { + return nil, errors.Wrapf(err, "error while running command %s. error output:\n%s", p.binaryPath, stderr.String()) + } + + return postRendered, nil +} + +// getFullPath returns the full filepath to the binary to execute. If the path +// does not contain any separators, it will search first in the plugins +// directory, then in $PATH, otherwise it will resolve any relative paths to a +// fully qualified path +func getFullPath(binaryPath string) (string, error) { + // Manually check the plugin dir first + if !strings.Contains(binaryPath, string(filepath.Separator)) { + // First check the plugin dir + pluginDir := helmpath.DataPath("plugins") + _, err := os.Stat(filepath.Join(pluginDir, binaryPath)) + if err != nil && !os.IsNotExist(err) { + return "", err + } else if err == nil { + binaryPath = filepath.Join(pluginDir, binaryPath) + } + } + + // Now check for the binary using the given path or check if it exists in + // the path and is executable + checkedPath, err := exec.LookPath(binaryPath) + if err != nil { + return "", errors.Wrapf(err, "unable to find binary at %s", binaryPath) + } + + return filepath.Abs(checkedPath) +} diff --git a/pkg/postrender/postrender.go b/pkg/postrender/postrender.go new file mode 100644 index 000000000..76f0f5a74 --- /dev/null +++ b/pkg/postrender/postrender.go @@ -0,0 +1,29 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// package postrender contains an interface that can be implemented for custom +// post-renderers and an exec implementation that can be used for arbitrary +// binaries and scripts +package postrender + +import "bytes" + +type PostRenderer interface { + // Run expects a single buffer filled with Helm rendered manifests. It + // expects the modified results to be returned on a separate buffer or an + // error if there was an issue or failure while running the post render step + Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) +}