Merge pull request #7259 from thomastaylor312/feat/post-render

feat(*): Post render hooks for Helm
pull/7553/head
Taylor Thomas 5 years ago committed by GitHub
commit d3d1ceab4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
// 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
}

@ -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
}

@ -132,6 +132,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &client.PostRenderer)
return cmd
}

@ -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
}

@ -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/<ReleaseName>
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
}

@ -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
}

@ -0,0 +1,108 @@
/*
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/exec"
"path/filepath"
"github.com/pkg/errors"
)
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 path does not
// contain any separators, it will search 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 in $PATH, otherwise it will
// resolve any relative paths to a fully qualified path
func getFullPath(binaryPath string) (string, error) {
// NOTE(thomastaylor312): I am leaving this code commented out here. During
// the implementation of post-render, it was brought up that if we are
// relying on plguins, we should actually use the plugin system so it can
// properly handle multiple OSs. This will be a feature add in the future,
// so I left this code for reference. It can be deleted or reused once the
// feature is implemented
// Manually check the plugin dir first
// if !strings.Contains(binaryPath, string(filepath.Separator)) {
// // First check the plugin dir
// pluginDir := helmpath.DataPath("plugins") // Default location
// // If location for plugins is explicitly set, check there
// if v, ok := os.LookupEnv("HELM_PLUGINS"); ok {
// pluginDir = v
// }
// // The plugins variable can actually contain multple paths, so loop through those
// for _, p := range filepath.SplitList(pluginDir) {
// _, err := os.Stat(filepath.Join(p, binaryPath))
// if err != nil && !os.IsNotExist(err) {
// return "", err
// } else if err == nil {
// binaryPath = filepath.Join(p, binaryPath)
// break
// }
// }
// }
// 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)
}

@ -0,0 +1,155 @@
/*
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/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/internal/test/ensure"
)
const testingScript = `#!/bin/sh
sed s/FOOTEST/BARTEST/g <&0
`
func TestGetFullPath(t *testing.T) {
is := assert.New(t)
t.Run("full path resolves correctly", func(t *testing.T) {
testpath, cleanup := setupTestingScript(t)
defer cleanup()
fullPath, err := getFullPath(testpath)
is.NoError(err)
is.Equal(testpath, fullPath)
})
t.Run("relative path resolves correctly", func(t *testing.T) {
testpath, cleanup := setupTestingScript(t)
defer cleanup()
currentDir, err := os.Getwd()
require.NoError(t, err)
relative, err := filepath.Rel(currentDir, testpath)
require.NoError(t, err)
fullPath, err := getFullPath(relative)
is.NoError(err)
is.Equal(testpath, fullPath)
})
t.Run("binary in PATH resolves correctly", func(t *testing.T) {
testpath, cleanup := setupTestingScript(t)
defer cleanup()
realPath := os.Getenv("PATH")
os.Setenv("PATH", filepath.Dir(testpath))
defer func() {
os.Setenv("PATH", realPath)
}()
fullPath, err := getFullPath(filepath.Base(testpath))
is.NoError(err)
is.Equal(testpath, fullPath)
})
// NOTE(thomastaylor312): See note in getFullPath for more details why this
// is here
// t.Run("binary in plugin path resolves correctly", func(t *testing.T) {
// testpath, cleanup := setupTestingScript(t)
// defer cleanup()
// realPath := os.Getenv("HELM_PLUGINS")
// os.Setenv("HELM_PLUGINS", filepath.Dir(testpath))
// defer func() {
// os.Setenv("HELM_PLUGINS", realPath)
// }()
// fullPath, err := getFullPath(filepath.Base(testpath))
// is.NoError(err)
// is.Equal(testpath, fullPath)
// })
// t.Run("binary in multiple plugin paths resolves correctly", func(t *testing.T) {
// testpath, cleanup := setupTestingScript(t)
// defer cleanup()
// realPath := os.Getenv("HELM_PLUGINS")
// os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)+string(os.PathListSeparator)+"/another/dir")
// defer func() {
// os.Setenv("HELM_PLUGINS", realPath)
// }()
// fullPath, err := getFullPath(filepath.Base(testpath))
// is.NoError(err)
// is.Equal(testpath, fullPath)
// })
}
func TestExecRun(t *testing.T) {
if runtime.GOOS == "windows" {
// the actual Run test uses a basic sed example, so skip this test on windows
t.Skip("skipping on windows")
}
is := assert.New(t)
testpath, cleanup := setupTestingScript(t)
defer cleanup()
renderer, err := NewExec(testpath)
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "BARTEST")
}
func setupTestingScript(t *testing.T) (filepath string, cleanup func()) {
t.Helper()
tempdir := ensure.TempDir(t)
f, err := ioutil.TempFile(tempdir, "post-render-test.sh")
if err != nil {
t.Fatalf("unable to create tempfile for testing: %s", err)
}
_, err = f.WriteString(testingScript)
if err != nil {
t.Fatalf("unable to write tempfile for testing: %s", err)
}
err = f.Chmod(0755)
if err != nil {
t.Fatalf("unable to make tempfile executable for testing: %s", err)
}
err = f.Close()
if err != nil {
t.Fatalf("unable to close tempfile after writing: %s", err)
}
return f.Name(), func() {
os.RemoveAll(tempdir)
}
}

@ -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)
}
Loading…
Cancel
Save