Move Postrenderer to a plugin type

Fix/add back postrenderer args unit tests

Signed-off-by: Scott Rigby <scott@r6by.com>
pull/31217/head
Scott Rigby 2 weeks ago
parent 9bb7e13c66
commit 591d863df5
No known key found for this signature in database
GPG Key ID: C7C6FBB5B91C1155

@ -46,8 +46,9 @@ type ConfigGetter struct {
Protocols []string `yaml:"protocols"`
}
func (c *ConfigCLI) GetType() string { return "cli/v1" }
func (c *ConfigGetter) GetType() string { return "getter/v1" }
// ConfigPostrenderer represents the configuration for postrenderer plugins
// there are no runtime-independent configurations for postrenderer/v1 plugin type
type ConfigPostrenderer struct{}
func (c *ConfigCLI) Validate() error {
// Config validation for CLI plugins
@ -66,6 +67,11 @@ func (c *ConfigGetter) Validate() error {
return nil
}
func (c *ConfigPostrenderer) Validate() error {
// Config validation for postrenderer plugins
return nil
}
func remarshalConfig[T Config](configData map[string]any) (Config, error) {
data, err := yaml.Marshal(configData)
if err != nil {

@ -163,6 +163,31 @@ func TestLoadDirGetter(t *testing.T) {
assert.Equal(t, expect, plug.Metadata())
}
func TestPostRenderer(t *testing.T) {
dirname := "testdata/plugdir/good/postrenderer-v1"
expect := Metadata{
Name: "postrenderer-v1",
Version: "1.2.3",
Type: "postrenderer/v1",
APIVersion: "v1",
Runtime: "subprocess",
Config: &ConfigPostrenderer{},
RuntimeConfig: &RuntimeConfigSubprocess{
PlatformCommands: []PlatformCommand{
{
Command: "${HELM_PLUGIN_DIR}/sed-test.sh",
},
},
},
}
plug, err := LoadDir(dirname)
require.NoError(t, err)
assert.Equal(t, dirname, plug.Dir())
assert.Equal(t, expect, plug.Metadata())
}
func TestDetectDuplicates(t *testing.T) {
plugs := []Plugin{
mockSubprocessCLIPlugin(t, "foo"),
@ -195,13 +220,14 @@ func TestLoadAll(t *testing.T) {
plugsMap[p.Metadata().Name] = p
}
assert.Len(t, plugsMap, 6)
assert.Len(t, plugsMap, 7)
assert.Contains(t, plugsMap, "downloader")
assert.Contains(t, plugsMap, "echo-legacy")
assert.Contains(t, plugsMap, "echo-v1")
assert.Contains(t, plugsMap, "getter")
assert.Contains(t, plugsMap, "hello-legacy")
assert.Contains(t, plugsMap, "hello-v1")
assert.Contains(t, plugsMap, "postrenderer-v1")
}
func TestFindPlugins(t *testing.T) {
@ -228,7 +254,7 @@ func TestFindPlugins(t *testing.T) {
{
name: "normal",
plugdirs: "./testdata/plugdir/good",
expected: 6,
expected: 7,
},
}
for _, c := range cases {

@ -31,7 +31,7 @@ type Metadata struct {
// Name is the name of the plugin
Name string
// Type of plugin (eg, cli/v1, getter/v1)
// Type of plugin (eg, cli/v1, getter/v1, postrenderer/v1)
Type string
// Runtime specifies the runtime type (subprocess, wasm)
@ -191,6 +191,8 @@ func convertMetadataConfig(pluginType string, configRaw map[string]any) (Config,
config, err = remarshalConfig[*ConfigCLI](configRaw)
case "getter/v1":
config, err = remarshalConfig[*ConfigGetter](configRaw)
case "postrenderer/v1":
config, err = remarshalConfig[*ConfigPostrenderer](configRaw)
default:
return nil, fmt.Errorf("unsupported plugin type: %s", pluginType)
}

@ -27,7 +27,7 @@ type MetadataV1 struct {
// Name is the name of the plugin
Name string `yaml:"name"`
// Type of plugin (eg, cli/v1, getter/v1)
// Type of plugin (eg, cli/v1, getter/v1, postrenderer/v1)
Type string `yaml:"type"`
// Runtime specifies the runtime type (subprocess, wasm)

@ -16,9 +16,11 @@ limitations under the License.
package plugin
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"syscall"
@ -36,7 +38,7 @@ type SubprocessProtocolCommand struct {
Command string `yaml:"command"`
}
// RuntimeConfigSubprocess represents configuration for subprocess runtime
// RuntimeConfigSubprocess implements RuntimeConfig for RuntimeSubprocess
type RuntimeConfigSubprocess struct {
// PlatformCommand is a list containing a plugin command, with a platform selector and support for args.
PlatformCommands []PlatformCommand `yaml:"platformCommand"`
@ -73,7 +75,7 @@ type RuntimeSubprocess struct{}
var _ Runtime = (*RuntimeSubprocess)(nil)
// CreateRuntime implementation for RuntimeConfig
// CreatePlugin implementation for Runtime
func (r *RuntimeSubprocess) CreatePlugin(pluginDir string, metadata *Metadata) (Plugin, error) {
return &SubprocessPluginRuntime{
metadata: *metadata,
@ -82,7 +84,7 @@ func (r *RuntimeSubprocess) CreatePlugin(pluginDir string, metadata *Metadata) (
}, nil
}
// RuntimeSubprocess implements the Runtime interface for subprocess execution
// SubprocessPluginRuntime implements the Plugin interface for subprocess execution
type SubprocessPluginRuntime struct {
metadata Metadata
pluginDir string
@ -105,6 +107,8 @@ func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Outp
return r.runCLI(input)
case schema.InputMessageGetterV1:
return r.runGetter(input)
case schema.InputMessagePostRendererV1:
return r.runPostrenderer(input)
default:
return nil, fmt.Errorf("unsupported subprocess plugin type %q", r.metadata.Type)
}
@ -216,6 +220,62 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
}, nil
}
func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) {
if _, ok := input.Message.(schema.InputMessagePostRendererV1); !ok {
return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name)
}
msg := input.Message.(schema.InputMessagePostRendererV1)
extraArgs := msg.ExtraArgs
settings := msg.Settings
// Setup plugin environment
SetupPluginEnv(settings, r.metadata.Name, r.pluginDir)
cmds := r.RuntimeConfig.PlatformCommands
if len(cmds) == 0 && len(r.RuntimeConfig.Command) > 0 {
cmds = []PlatformCommand{{Command: r.RuntimeConfig.Command}}
}
command, args, err := PrepareCommands(cmds, true, extraArgs)
if err != nil {
return nil, fmt.Errorf("failed to prepare plugin command: %w", err)
}
// TODO de-duplicate code here by calling RuntimeSubprocess.invokeWithEnv()
cmd := exec.Command(
command,
args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
go func() {
defer stdin.Close()
io.Copy(stdin, msg.Manifests)
}()
postRendered := &bytes.Buffer{}
stderr := &bytes.Buffer{}
//cmd.Env = pluginExec.env
cmd.Stdout = postRendered
cmd.Stderr = stderr
if err := executeCmd(cmd, r.metadata.Name); err != nil {
slog.Info("plugin execution failed", slog.String("stderr", stderr.String()))
return nil, err
}
return &Output{
Message: &schema.OutputMessagePostRendererV1{
Manifests: postRendered,
},
}, nil
}
// SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because
// the plugin subsystem itself needs access to the environment variables
// created here.

@ -14,16 +14,22 @@ 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)
package schema
import (
"bytes"
"helm.sh/helm/v4/pkg/cli"
)
// InputMessagePostRendererV1 implements Input.Message
type InputMessagePostRendererV1 struct {
Manifests *bytes.Buffer `json:"manifests"`
// from CLI --post-renderer-args
ExtraArgs []string `json:"extraArgs"`
Settings *cli.EnvSettings `json:"settings"`
}
type OutputMessagePostRendererV1 struct {
Manifests *bytes.Buffer `json:"manifests"`
}

@ -0,0 +1,8 @@
name: "postrenderer-v1"
version: "1.2.3"
type: postrenderer/v1
apiVersion: v1
runtime: subprocess
runtimeConfig:
platformCommand:
- command: "${HELM_PLUGIN_DIR}/sed-test.sh"

@ -0,0 +1,6 @@
#!/bin/sh
if [ $# -eq 0 ]; then
sed s/FOOTEST/BARTEST/g <&0
else
sed s/FOOTEST/"$*"/g <&0
fi

@ -43,7 +43,7 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/engine"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
@ -176,7 +176,7 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) {
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
//
// This code has to do with writing files to disk.
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
var hs []*release.Hook
b := bytes.NewBuffer(nil)

@ -48,7 +48,7 @@ import (
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
@ -124,7 +124,7 @@ type Install struct {
UseReleaseName bool
// TakeOwnership will ignore the check for helm annotations and take ownership of the resources.
TakeOwnership bool
PostRenderer postrender.PostRenderer
PostRenderer postrenderer.PostRenderer
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}

@ -31,7 +31,7 @@ import (
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
@ -114,7 +114,7 @@ type Upgrade struct {
//
// If this is non-nil, then after templates are rendered, they will be sent to the
// post renderer before sending to the Kubernetes API server.
PostRenderer postrender.PostRenderer
PostRenderer postrenderer.PostRenderer
// DisableOpenAPIValidation controls whether OpenAPI validation is enforced.
DisableOpenAPIValidation bool
// Get missing dependencies

@ -31,11 +31,12 @@ import (
"k8s.io/klog/v2"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/repo"
)
@ -164,16 +165,18 @@ func (o *outputValue) Set(s string) error {
return nil
}
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
p := &postRendererOptions{varRef, "", []string{}}
cmd.Flags().Var(&postRendererString{p}, 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")
// TODO there is probably a better way to pass cobra settings than as a param
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrenderer.PostRenderer, settings *cli.EnvSettings) {
p := &postRendererOptions{varRef, "", []string{}, settings}
cmd.Flags().Var(&postRendererString{p}, postRenderFlag, "the name of a postrenderer type plugin to be used for post rendering. If it exists, the plugin will be used")
cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)")
}
type postRendererOptions struct {
renderer *postrender.PostRenderer
binaryPath string
renderer *postrenderer.PostRenderer
pluginName string
args []string
settings *cli.EnvSettings
}
type postRendererString struct {
@ -181,7 +184,7 @@ type postRendererString struct {
}
func (p *postRendererString) String() string {
return p.options.binaryPath
return p.options.pluginName
}
func (p *postRendererString) Type() string {
@ -192,11 +195,11 @@ func (p *postRendererString) Set(val string) error {
if val == "" {
return nil
}
if p.options.binaryPath != "" {
if p.options.pluginName != "" {
return fmt.Errorf("cannot specify --post-renderer flag more than once")
}
p.options.binaryPath = val
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
p.options.pluginName = val
pr, err := postrenderer.NewPostRendererPlugin(p.options.settings, p.options.pluginName, p.options.args...)
if err != nil {
return err
}
@ -221,11 +224,11 @@ func (p *postRendererArgsSlice) Set(val string) error {
// a post-renderer defined by a user may accept empty arguments
p.options.args = append(p.options.args, val)
if p.options.binaryPath == "" {
if p.options.pluginName == "" {
return nil
}
// overwrite if already create PostRenderer by `post-renderer` flags
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
pr, err := postrenderer.NewPostRendererPlugin(p.options.settings, p.options.pluginName, p.options.args...)
if err != nil {
return err
}

@ -101,20 +101,22 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
func TestPostRendererFlagSetOnce(t *testing.T) {
cfg := action.Configuration{}
client := action.NewInstall(&cfg)
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
str := postRendererString{
options: &postRendererOptions{
renderer: &client.PostRenderer,
settings: settings,
},
}
// Set the binary once
err := str.Set("echo")
// Set the plugin name once
err := str.Set("postrenderer-v1")
require.NoError(t, err)
// Set the binary again to the same value is not ok
err = str.Set("echo")
// Set the plugin name again to the same value is not ok
err = str.Set("postrenderer-v1")
require.Error(t, err)
// Set the binary again to a different value is not ok
// Set the plugin name again to a different value is not ok
err = str.Set("cat")
require.Error(t, err)
}

@ -179,7 +179,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f := cmd.Flags()
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
return cmd
}

@ -203,7 +203,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &client.PostRenderer)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
return cmd
}

@ -0,0 +1,8 @@
name: "postrenderer-v1"
version: "1.2.3"
type: postrenderer/v1
apiVersion: v1
runtime: subprocess
runtimeConfig:
platformCommand:
- command: "${HELM_PLUGIN_DIR}/sed-test.sh"

@ -0,0 +1,6 @@
#!/bin/sh
if [ $# -eq 0 ]; then
sed s/FOOTEST/BARTEST/g <&0
else
sed s/FOOTEST/"$*"/g <&0
fi

@ -300,7 +300,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)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -1,114 +0,0 @@
/*
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"
"fmt"
"io"
"os/exec"
"path/filepath"
)
type execRender struct {
binaryPath string
args []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, args ...string) (PostRenderer, error) {
fullPath, err := getFullPath(binaryPath)
if err != nil {
return nil, err
}
return &execRender{fullPath, args}, nil
}
// Run the configured binary for the post render
func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) {
cmd := exec.Command(p.binaryPath, p.args...)
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, fmt.Errorf("error while running command %s. error output:\n%s: %w", p.binaryPath, stderr.String(), err)
}
// If the binary returned almost nothing, it's likely that it didn't
// successfully render anything
if len(bytes.TrimSpace(postRendered.Bytes())) == 0 {
return nil, fmt.Errorf("post-renderer %q produced empty output", p.binaryPath)
}
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 plugins, 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 multiple paths, so loop through those
// for _, p := range filepath.SplitList(pluginDir) {
// _, err := os.Stat(filepath.Join(p, binaryPath))
// if err != nil && !errors.Is(err, fs.ErrNotExist) {
// 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 "", fmt.Errorf("unable to find binary at %s: %w", binaryPath, err)
}
return filepath.Abs(checkedPath)
}

@ -1,193 +0,0 @@
/*
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"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testingScript = `#!/bin/sh
if [ $# -eq 0 ]; then
sed s/FOOTEST/BARTEST/g <&0
else
sed s/FOOTEST/"$*"/g <&0
fi
`
func TestGetFullPath(t *testing.T) {
is := assert.New(t)
t.Run("full path resolves correctly", func(t *testing.T) {
testpath := setupTestingScript(t)
fullPath, err := getFullPath(testpath)
is.NoError(err)
is.Equal(testpath, fullPath)
})
t.Run("relative path resolves correctly", func(t *testing.T) {
testpath := setupTestingScript(t)
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 := setupTestingScript(t)
t.Setenv("PATH", filepath.Dir(testpath))
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 := setupTestingScript(t)
renderer, err := NewExec(testpath)
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "BARTEST")
}
func TestExecRunWithNoOutput(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 := setupTestingScript(t)
renderer, err := NewExec(testpath)
require.NoError(t, err)
_, err = renderer.Run(bytes.NewBufferString(""))
is.Error(err)
}
func TestNewExecWithOneArgsRun(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 := setupTestingScript(t)
renderer, err := NewExec(testpath, "ARG1")
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "ARG1")
}
func TestNewExecWithTwoArgsRun(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 := setupTestingScript(t)
renderer, err := NewExec(testpath, "ARG1", "ARG2")
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "ARG1 ARG2")
}
func setupTestingScript(t *testing.T) (filepath string) {
t.Helper()
tempdir := t.TempDir()
f, err := os.CreateTemp(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(0o755)
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()
}

@ -0,0 +1,85 @@
/*
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 postrenderer
import (
"bytes"
"context"
"fmt"
"path/filepath"
"helm.sh/helm/v4/internal/plugin/schema"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/pkg/cli"
)
// PostRenderer is an interface different plugin runtimes
// it may be also be used without the factory for custom post-renderers
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)
}
// NewPostRendererPlugin creates a PostRenderer that uses the plugin's Runtime
func NewPostRendererPlugin(settings *cli.EnvSettings, pluginName string, args ...string) (PostRenderer, error) {
descriptor := plugin.Descriptor{
Name: pluginName,
Type: "postrenderer/v1",
}
p, err := plugin.FindPlugin(filepath.SplitList(settings.PluginsDirectory), descriptor)
if err != nil {
return nil, err
}
return &postRendererPlugin{
plugin: p,
args: args,
settings: settings,
}, nil
}
// postRendererPlugin implements PostRenderer by delegating to the plugin's Runtime
type postRendererPlugin struct {
plugin plugin.Plugin
args []string
settings *cli.EnvSettings
}
// Run implements PostRenderer by using the plugin's Runtime
func (r *postRendererPlugin) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) {
input := &plugin.Input{
Message: schema.InputMessagePostRendererV1{
ExtraArgs: r.args,
Manifests: renderedManifests,
Settings: r.settings,
},
}
output, err := r.plugin.Invoke(context.Background(), input)
if err != nil {
return nil, fmt.Errorf("failed to invoke post-renderer plugin %q: %w", r.plugin.Metadata().Name, err)
}
outputMessage := output.Message.(*schema.OutputMessagePostRendererV1)
// If the binary returned almost nothing, it's likely that it didn't
// successfully render anything
if len(bytes.TrimSpace(outputMessage.Manifests.Bytes())) == 0 {
return nil, fmt.Errorf("post-renderer %q produced empty output", r.plugin.Metadata().Name)
}
return outputMessage.Manifests, nil
}

@ -0,0 +1,89 @@
/*
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 postrenderer
import (
"bytes"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/pkg/cli"
)
func TestNewPostRenderPluginRunWithNoOutput(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)
s := cli.New()
s.PluginsDirectory = "testdata/plugins"
name := "postrenderer-v1"
base := filepath.Join(s.PluginsDirectory, name)
plugin.SetupPluginEnv(s, name, base)
renderer, err := NewPostRendererPlugin(s, name, "")
require.NoError(t, err)
_, err = renderer.Run(bytes.NewBufferString(""))
is.Error(err)
}
func TestNewPostRenderPluginWithOneArgsRun(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)
s := cli.New()
s.PluginsDirectory = "testdata/plugins"
name := "postrenderer-v1"
base := filepath.Join(s.PluginsDirectory, name)
plugin.SetupPluginEnv(s, name, base)
renderer, err := NewPostRendererPlugin(s, name, "ARG1")
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "ARG1")
}
func TestNewPostRenderPluginWithTwoArgsRun(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)
s := cli.New()
s.PluginsDirectory = "testdata/plugins"
name := "postrenderer-v1"
base := filepath.Join(s.PluginsDirectory, name)
plugin.SetupPluginEnv(s, name, base)
renderer, err := NewPostRendererPlugin(s, name, "ARG1", "ARG2")
require.NoError(t, err)
output, err := renderer.Run(bytes.NewBufferString("FOOTEST"))
is.NoError(err)
is.Contains(output.String(), "ARG1 ARG2")
}

@ -0,0 +1,8 @@
name: "postrenderer-v1"
version: "1.2.3"
type: postrenderer/v1
apiVersion: v1
runtime: subprocess
runtimeConfig:
platformCommand:
- command: "${HELM_PLUGIN_DIR}/sed-test.sh"

@ -0,0 +1,6 @@
#!/bin/sh
if [ $# -eq 0 ]; then
sed s/FOOTEST/BARTEST/g <&0
else
sed s/FOOTEST/"$*"/g <&0
fi
Loading…
Cancel
Save