mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
Move Postrenderer to a plugin type
Fix/add back postrenderer args unit tests Signed-off-by: Scott Rigby <scott@r6by.com>
This commit is contained in:
parent
9bb7e13c66
commit
591d863df5
24 changed files with 366 additions and 352 deletions
|
|
@ -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
|
||||
package schema
|
||||
|
||||
import "bytes"
|
||||
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)
|
||||
"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"`
|
||||
}
|
||||
8
internal/plugin/testdata/plugdir/good/postrenderer-v1/plugin.yaml
vendored
Normal file
8
internal/plugin/testdata/plugdir/good/postrenderer-v1/plugin.yaml
vendored
Normal file
|
|
@ -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"
|
||||
6
internal/plugin/testdata/plugdir/good/postrenderer-v1/sed-test.sh
vendored
Executable file
6
internal/plugin/testdata/plugdir/good/postrenderer-v1/sed-test.sh
vendored
Executable file
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
8
pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/plugin.yaml
vendored
Normal file
8
pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/plugin.yaml
vendored
Normal file
|
|
@ -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"
|
||||
6
pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/sed-test.sh
vendored
Executable file
6
pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/sed-test.sh
vendored
Executable file
|
|
@ -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()
|
||||
}
|
||||
85
pkg/postrenderer/postrenderer.go
Normal file
85
pkg/postrenderer/postrenderer.go
Normal file
|
|
@ -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
|
||||
}
|
||||
89
pkg/postrenderer/postrenderer_test.go
Normal file
89
pkg/postrenderer/postrenderer_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
8
pkg/postrenderer/testdata/plugins/postrenderer-v1/plugin.yaml
vendored
Normal file
8
pkg/postrenderer/testdata/plugins/postrenderer-v1/plugin.yaml
vendored
Normal file
|
|
@ -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"
|
||||
6
pkg/postrenderer/testdata/plugins/postrenderer-v1/sed-test.sh
vendored
Executable file
6
pkg/postrenderer/testdata/plugins/postrenderer-v1/sed-test.sh
vendored
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
if [ $# -eq 0 ]; then
|
||||
sed s/FOOTEST/BARTEST/g <&0
|
||||
else
|
||||
sed s/FOOTEST/"$*"/g <&0
|
||||
fi
|
||||
Loading…
Reference in a new issue