Create plugins/invoke interface and move legacy plugins to subprocess runtime

Signed-off-by: George Jenkins <gvjenkins@gmail.com>
pull/31080/head
George Jenkins 2 months ago
parent 98d6d9c1ef
commit ac0fcef641

@ -0,0 +1,74 @@
/*
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 pluginloader // import "helm.sh/helm/v4/internal/plugins/loader"
import (
"helm.sh/helm/v4/internal/plugins"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
)
// FindPlugins returns a list of YAML files that describe plugins
func FindPlugins(pluginsDirs []string, descriptor plugins.PluginDescriptor) ([]plugins.Plugin, error) {
return findPlugins(pluginsDirs, subprocessFindPlugins, makeDescriptorFilter(descriptor))
}
type findFunc func(pluginsDirs string) ([]plugins.Plugin, error)
type filterFunc func(plugins.Plugin) bool
func findPlugins(pluginsDirs []string, findFunc findFunc, filterFunc filterFunc) ([]plugins.Plugin, error) {
found := []plugins.Plugin{}
for _, pluginsDir := range pluginsDirs {
ps, err := findFunc(pluginsDir)
if err != nil {
return nil, err
}
for _, p := range ps {
if filterFunc(p) {
found = append(found, p)
}
}
}
return found, nil
}
func convertSubprocess(subs []*subprocess.Plugin) []plugins.Plugin {
ps := make([]plugins.Plugin, len(subs))
for i, r := range subs {
ps[i] = r
}
return ps
}
func subprocessFindPlugins(pluginsDir string) ([]plugins.Plugin, error) {
ps, err := subprocess.FindPlugins(pluginsDir)
if err != nil {
return nil, err
}
return convertSubprocess(ps), nil
}
func makeDescriptorFilter(descriptor plugins.PluginDescriptor) filterFunc {
return func(p plugins.Plugin) bool {
manifest := p.Manifest()
return manifest.TypeVersion == descriptor.TypeVersion
}
}

@ -0,0 +1,104 @@
/*
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 pluginloader // import "helm.sh/helm/v4/internal/plugins/loader"
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/plugins"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
)
func TestConvertSubprocess(t *testing.T) {
sps := []*subprocess.Plugin{
{
Metadata: &subprocess.Metadata{Name: "test-plugin"},
},
}
ps := convertSubprocess(sps)
require.Equal(t, len(sps), len(ps))
for index := range sps {
assert.Equal(t, sps[index], ps[index].(*subprocess.Plugin))
}
}
func TestFindPlugins(t *testing.T) {
pluginsDirs := []string{"testdata/plugins"}
findFunc := func(pluginsDir string) ([]plugins.Plugin, error) {
return []plugins.Plugin{
&subprocess.Plugin{
Dir: filepath.Join(pluginsDir, "test-plugin"),
Metadata: &subprocess.Metadata{
Name: "test-plugin",
},
},
}, nil
}
filterFunc := func(p plugins.Plugin) bool {
assert.Equal(t, "test-plugin", p.Manifest().Name)
return true
}
ps, err := findPlugins(pluginsDirs, findFunc, filterFunc)
assert.NoError(t, err)
require.Len(t, ps, 1)
assert.Equal(t, "test-plugin", ps[0].Manifest().Name)
}
func TestMakeDescriptorFilter(t *testing.T) {
descriptor := plugins.PluginDescriptor{
TypeVersion: "getter/v1",
}
filterFunc := makeDescriptorFilter(descriptor)
ps := []plugins.Plugin{
&subprocess.Plugin{
Metadata: &subprocess.Metadata{
Name: "test-plugin",
// subprocess plugins classify themselves as "cli" or "downloader" based on presence of Downloaders field
Downloaders: []subprocess.Downloaders{
{
Protocols: []string{"http"},
},
},
},
},
&subprocess.Plugin{
Metadata: &subprocess.Metadata{
Name: "other-plugin",
},
},
}
filtered := []plugins.Plugin{}
for _, p := range ps {
if filterFunc(p) {
filtered = append(filtered, p)
}
}
require.Len(t, filtered, 1)
assert.Equal(t, "test-plugin", filtered[0].Manifest().Name)
}

@ -0,0 +1,72 @@
/*
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 plugins
import (
"context"
)
const PluginFileName = "plugin.yaml"
type PluginDescriptor struct {
TypeVersion string
}
// plugin.yaml definition
type Manifest struct {
// APIVersion of the plugin manifest document
// Currently: 'plugins.helm.sh/v1alpha1'
APIVersion string `json:"apiVersion"`
// Author defined name, version and description of the plugin
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
// Type/version of the plugin: 'getter/v1', 'postrenderer/v1', 'cli/v1', etc
// Describing the situation the plugin is expected to be invoked, and the correspondng message type/version used to invoke
TypeVersion string `json:"typeVersion"`
// Runtime used to execute the plugin
// subprocess, extism/v1, etc
RuntimeClass string `json:"runtimeClass"`
// Additional config associated with the plugin kind: e.g. downloader URI schemes
// (Config is intepreted by the plugin invoker)
Config map[string]any `json:"config,omitempty"`
}
// Input defined the input message and parameters to be passed to the plugin
type Input struct {
// Message represents the type-elided value to be passed to the plugin
// The plugin is expected to interpret the message according to its type/version
// The message object must be JSON-serializable
Message any
}
// Input defined the output message and parameters the passed from the plugin
type Output struct {
// Message represents the type-elided value passed from the plugin
// The invoker is expected to interpret the message according to the plugins type/version
Message any
}
// Plugin defines the "invokable" interface for a plugin, as well a getter for the plugin's describing manifest
// The invoke method can be thought of request/response message passing between the plugin invoker and the plugin itself
type Plugin interface {
Manifest() Manifest
Invoke(ctx context.Context, input *Input) (*Output, error)
}

@ -14,7 +14,7 @@ limitations under the License.
*/ */
// Package cache provides a key generator for vcs urls. // Package cache provides a key generator for vcs urls.
package cache // import "helm.sh/helm/v4/pkg/plugin/cache" package cache // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/cache"
import ( import (
"net/url" "net/url"

@ -0,0 +1,24 @@
/*
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 subprocess // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
func IsDownloader(p *Plugin) bool {
if p.Metadata == nil {
return false
}
return len(p.Metadata.Downloaders) > 0
}

@ -0,0 +1,64 @@
/*
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 subprocess // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsDownloader(t *testing.T) {
testCases := map[string]struct {
Plugin Plugin
Want bool
}{
"nil metadata": {
Plugin: Plugin{
Metadata: nil,
},
Want: false,
},
"no downloaders": {
Plugin: Plugin{
Metadata: &Metadata{
Downloaders: nil,
},
},
Want: false,
},
"downloader": {
Plugin: Plugin{
Metadata: &Metadata{
Downloaders: []Downloaders{
{
Protocols: []string{"test"},
Command: "foo",
},
},
},
},
Want: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got := IsDownloader(&tc.Plugin)
assert.Equal(t, got, tc.Want)
})
}
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package plugin // import "helm.sh/helm/v4/pkg/plugin" package subprocess // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
// Types of hooks // Types of hooks
const ( const (

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"path/filepath" "path/filepath"

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"testing" "testing"

@ -14,4 +14,4 @@ limitations under the License.
*/ */
// Package installer provides an interface for installing Helm plugins. // Package installer provides an interface for installing Helm plugins.
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"archive/tar" "archive/tar"
@ -32,11 +32,11 @@ import (
securejoin "github.com/cyphar/filepath-securejoin" securejoin "github.com/cyphar/filepath-securejoin"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess/cache"
"helm.sh/helm/v4/internal/third_party/dep/fs" "helm.sh/helm/v4/internal/third_party/dep/fs"
"helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/plugin/cache"
) )
// HTTPInstaller installs plugins from an archive served by a web server. // HTTPInstaller installs plugins from an archive served by a web server.

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"archive/tar" "archive/tar"

@ -22,7 +22,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"helm.sh/helm/v4/pkg/plugin" plugins "helm.sh/helm/v4/internal/plugins"
) )
// ErrMissingMetadata indicates that plugin.yaml is missing. // ErrMissingMetadata indicates that plugin.yaml is missing.
@ -119,6 +119,6 @@ func isRemoteHTTPArchive(source string) bool {
// isPlugin checks if the directory contains a plugin.yaml file. // isPlugin checks if the directory contains a plugin.yaml file.
func isPlugin(dirname string) bool { func isPlugin(dirname string) bool {
_, err := os.Stat(filepath.Join(dirname, plugin.PluginFileName)) _, err := os.Stat(filepath.Join(dirname, plugins.PluginFileName))
return err == nil return err == nil
} }

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"errors" "errors"

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"os" "os"

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"errors" "errors"
@ -26,9 +26,9 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/Masterminds/vcs" "github.com/Masterminds/vcs"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess/cache"
"helm.sh/helm/v4/internal/third_party/dep/fs" "helm.sh/helm/v4/internal/third_party/dep/fs"
"helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/plugin/cache"
) )
// VCSInstaller installs plugins from remote a repository. // VCSInstaller installs plugins from remote a repository.

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package installer // import "helm.sh/helm/v4/pkg/plugin/installer" package installer // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
import ( import (
"fmt" "fmt"

@ -13,11 +13,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package plugin // import "helm.sh/helm/v4/pkg/plugin" package subprocess // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
import ( import (
"bytes"
"context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -26,10 +29,12 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v4/internal/plugins"
"helm.sh/helm/v4/internal/plugins/schema"
"helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli"
) )
const PluginFileName = "plugin.yaml" const PluginFileName = plugins.PluginFileName
// Downloaders represents the plugins capability if it can retrieve // Downloaders represents the plugins capability if it can retrieve
// charts from special sources // charts from special sources
@ -142,6 +147,8 @@ type Plugin struct {
Dir string Dir string
} }
var _ plugins.Plugin = (*Plugin)(nil)
// Returns command and args strings based on the following rules in priority order: // Returns command and args strings based on the following rules in priority order:
// - From the PlatformCommand where OS and Arch match the current platform // - From the PlatformCommand where OS and Arch match the current platform
// - From the PlatformCommand where OS matches the current platform and Arch is empty/unspecified // - From the PlatformCommand where OS matches the current platform and Arch is empty/unspecified
@ -311,7 +318,7 @@ func LoadDir(dirname string) (*Plugin, error) {
plug := &Plugin{Dir: dirname} plug := &Plugin{Dir: dirname}
if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil { if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil {
return nil, fmt.Errorf("failed to load plugin at %q: %w", pluginfile, err) return nil, fmt.Errorf("failed to load %s at %q: %w", PluginFileName, pluginfile, err)
} }
return plug, validatePluginData(plug, pluginfile) return plug, validatePluginData(plug, pluginfile)
} }
@ -368,3 +375,141 @@ func SetupPluginEnv(settings *cli.EnvSettings, name, base string) {
os.Setenv(key, val) os.Setenv(key, val)
} }
} }
type pluginExec struct {
command string
argv []string
env []string
}
func convertInputGetterInputV1(p *Plugin, command string, argvBase []string, msg schema.GetterInputV1) (pluginExec, error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), fmt.Sprintf("helm-plugin-%s-", p.Metadata.Name))
if err != nil {
return pluginExec{}, fmt.Errorf("failed to create temporary directory: %w", err)
}
defer os.RemoveAll(tmpDir)
writeTempFile := func(name string, data []byte) (string, error) {
if len(data) == 0 {
return "", nil
}
tempFile := filepath.Join(tmpDir, name)
err := os.WriteFile(tempFile, msg.Options.Cert, 0o640)
if err != nil {
return "", fmt.Errorf("failed to write temporary file: %w", err)
}
return tempFile, nil
}
certFile, err := writeTempFile("cert", msg.Options.Cert)
if err != nil {
return pluginExec{}, err
}
keyFile, err := writeTempFile("key", msg.Options.Cert)
if err != nil {
return pluginExec{}, err
}
caFile, err := writeTempFile("ca", msg.Options.Cert)
if err != nil {
return pluginExec{}, err
}
argv := append(
argvBase,
certFile,
keyFile,
caFile,
msg.URL)
env := append(
os.Environ(),
fmt.Sprintf("HELM_PLUGIN_USERNAME=%s", msg.Options.Username),
fmt.Sprintf("HELM_PLUGIN_PASSWORD=%s", msg.Options.Password),
fmt.Sprintf("HELM_PLUGIN_PASS_CREDENTIALS_ALL=%t", msg.Options.PassCredentialsAll))
return pluginExec{
command: command,
argv: argv,
env: env,
}, nil
}
func convertInput(p *Plugin, input *plugins.Input) (pluginExec, error) {
command, argv, err := p.PrepareCommand([]string{})
if err != nil {
return pluginExec{}, fmt.Errorf("failed to prepare command for plugin %q: %w", p.Dir, err)
}
switch inputMsg := input.Message.(type) {
case schema.GetterInputV1:
return convertInputGetterInputV1(
p,
command,
argv,
inputMsg)
}
return pluginExec{}, fmt.Errorf("unsupported plugin input type %T", input)
}
func convertOutput(buf *bytes.Buffer) *plugins.Output {
return &plugins.Output{
Message: schema.GetterOutputV1{
Data: buf,
},
}
}
func (p *Plugin) Invoke(_ context.Context, input *plugins.Input) (*plugins.Output, error) {
pluginExec, err := convertInput(p, input)
if err != nil {
return nil, fmt.Errorf("failed to convert plugin input: %w", err)
}
pluginCommand := filepath.Join(p.Dir, pluginExec.command)
prog := exec.Command(
pluginCommand,
pluginExec.argv...)
prog.Env = pluginExec.env
buf := bytes.NewBuffer(nil)
prog.Stdout = buf
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
return nil, fmt.Errorf("plugin %q exited with error", pluginCommand)
}
return nil, fmt.Errorf("failed to run plugin %q: %w", pluginCommand, err)
}
return convertOutput(buf), nil
}
func (p *Plugin) Manifest() plugins.Manifest {
typeVersion := "cli/v1"
config := map[string]any{}
if IsDownloader(p) {
typeVersion = "getter/v1"
schemes := make([]string, 0, len(p.Metadata.Downloaders))
for _, d := range p.Metadata.Downloaders {
schemes = append(schemes, d.Protocols...)
}
config["downloader_schemes"] = schemes
}
return plugins.Manifest{
APIVersion: "legacy",
Name: p.Metadata.Name,
Version: p.Metadata.Version,
Description: p.Metadata.Description,
TypeVersion: typeVersion,
RuntimeClass: "subprocess",
Config: config,
}
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package plugin // import "helm.sh/helm/v4/pkg/plugin" package subprocess // import "helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
import ( import (
"fmt" "fmt"

@ -0,0 +1,49 @@
/*
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 schema
import (
"bytes"
"time"
)
// TODO: can we generate these plugin input/outputs?
type GetterOptionsV1 struct {
URL string
Cert []byte
Key []byte
CA []byte
UNTar bool
InsecureSkipVerifyTLS bool
PlainHTTP bool
AcceptHeader string
Username string
Password string
PassCredentialsAll bool
UserAgent string
Version string
Timeout time.Duration
}
type GetterInputV1 struct {
URL string `json:"url"`
Options GetterOptionsV1 `json:"options"`
}
type GetterOutputV1 struct {
Data *bytes.Buffer `json:"data"`
}

@ -31,7 +31,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/plugin" pluginloader "helm.sh/helm/v4/internal/plugins/loader"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
) )
const ( const (
@ -55,7 +56,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return return
} }
found, err := plugin.FindPlugins(settings.PluginsDirectory) found, err := pluginloader.FindPlugins([]string{settings.PluginsDirectory}, cliPluginDescriptor)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
return return
@ -63,15 +64,17 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// Now we create commands for all of these. // Now we create commands for all of these.
for _, plug := range found { for _, plug := range found {
plug := plug
md := plug.Metadata splug := plug.(*subprocess.Plugin)
if md.Usage == "" { md := splug.Metadata
md.Usage = fmt.Sprintf("the %q plugin", md.Name) usage := splug.Metadata.Usage
if usage == "" {
usage = fmt.Sprintf("the %q plugin", md.Name)
} }
c := &cobra.Command{ c := &cobra.Command{
Use: md.Name, Use: md.Name,
Short: md.Usage, Short: usage,
Long: md.Description, Long: md.Description,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
u, err := processParent(cmd, args) u, err := processParent(cmd, args)
@ -82,8 +85,8 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// Call setupEnv before PrepareCommand because // Call setupEnv before PrepareCommand because
// PrepareCommand uses os.ExpandEnv and expects the // PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars. // setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir) subprocess.SetupPluginEnv(settings, md.Name, splug.Dir)
main, argv, prepCmdErr := plug.PrepareCommand(u) main, argv, prepCmdErr := splug.PrepareCommand(u)
if prepCmdErr != nil { if prepCmdErr != nil {
os.Stderr.WriteString(prepCmdErr.Error()) os.Stderr.WriteString(prepCmdErr.Error())
return fmt.Errorf("plugin %q exited with error", md.Name) return fmt.Errorf("plugin %q exited with error", md.Name)
@ -106,7 +109,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
if (err == nil && if (err == nil &&
((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) ||
/* for the tests */ subCmd == baseCmd.Root() { /* for the tests */ subCmd == baseCmd.Root() {
loadCompletionForPlugin(c, plug) loadCompletionForPlugin(c, splug)
} }
} }
} }
@ -201,7 +204,7 @@ type pluginCommand struct {
// loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin
// and add the dynamic completion hook to call the optional plugin.complete // and add the dynamic completion hook to call the optional plugin.complete
func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) { func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *subprocess.Plugin) {
// Parse the yaml file providing the plugin's sub-commands and flags // Parse the yaml file providing the plugin's sub-commands and flags
cmds, err := loadFile(strings.Join( cmds, err := loadFile(strings.Join(
[]string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator)))
@ -223,7 +226,7 @@ func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) {
// addPluginCommands is a recursive method that adds each different level // addPluginCommands is a recursive method that adds each different level
// of sub-commands and flags for the plugins that have provided such information // of sub-commands and flags for the plugins that have provided such information
func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { func addPluginCommands(plugin *subprocess.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) {
if cmds == nil { if cmds == nil {
return return
} }
@ -320,7 +323,7 @@ func loadFile(path string) (*pluginCommand, error) {
// pluginDynamicComp call the plugin.complete script of the plugin (if available) // pluginDynamicComp call the plugin.complete script of the plugin (if available)
// to obtain the dynamic completion choices. It must pass all the flags and sub-commands // to obtain the dynamic completion choices. It must pass all the flags and sub-commands
// specified in the command-line to the plugin.complete executable (except helm's global flags) // specified in the command-line to the plugin.complete executable (except helm's global flags)
func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func pluginDynamicComp(plug *subprocess.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
md := plug.Metadata md := plug.Metadata
u, err := processParent(cmd, args) u, err := processParent(cmd, args)
@ -339,7 +342,7 @@ func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, t
argv = append(argv, u...) argv = append(argv, u...)
argv = append(argv, toComplete) argv = append(argv, toComplete)
} }
plugin.SetupPluginEnv(settings, md.Name, plug.Dir) subprocess.SetupPluginEnv(settings, md.Name, plug.Dir)
cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)

@ -24,7 +24,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/plugin" "helm.sh/helm/v4/internal/plugins"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
) )
const pluginHelp = ` const pluginHelp = `
@ -47,20 +48,20 @@ func newPluginCmd(out io.Writer) *cobra.Command {
} }
// runHook will execute a plugin hook. // runHook will execute a plugin hook.
func runHook(p *plugin.Plugin, event string) error { func runHook(p *subprocess.Plugin, event string) error {
plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir) subprocess.SetupPluginEnv(settings, p.Metadata.Name, p.Dir)
cmds := p.Metadata.PlatformHooks[event] cmds := p.Metadata.PlatformHooks[event]
expandArgs := true expandArgs := true
if len(cmds) == 0 && len(p.Metadata.Hooks) > 0 { if len(cmds) == 0 && len(p.Metadata.Hooks) > 0 {
cmd := p.Metadata.Hooks[event] cmd := p.Metadata.Hooks[event]
if len(cmd) > 0 { if len(cmd) > 0 {
cmds = []plugin.PlatformCommand{{Command: "sh", Args: []string{"-c", cmd}}} cmds = []subprocess.PlatformCommand{{Command: "sh", Args: []string{"-c", cmd}}}
expandArgs = false expandArgs = false
} }
} }
main, argv, err := plugin.PrepareCommands(cmds, expandArgs, []string{}) main, argv, err := subprocess.PrepareCommands(cmds, expandArgs, []string{})
if err != nil { if err != nil {
return nil return nil
} }
@ -79,3 +80,7 @@ func runHook(p *plugin.Plugin, event string) error {
} }
return nil return nil
} }
var cliPluginDescriptor = plugins.PluginDescriptor{
TypeVersion: "cli/v1",
}

@ -22,9 +22,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/plugin"
"helm.sh/helm/v4/pkg/plugin/installer"
) )
type pluginInstallOptions struct { type pluginInstallOptions struct {
@ -80,12 +81,12 @@ func (o *pluginInstallOptions) run(out io.Writer) error {
} }
slog.Debug("loading plugin", "path", i.Path()) slog.Debug("loading plugin", "path", i.Path())
p, err := plugin.LoadDir(i.Path()) p, err := subprocess.LoadDir(i.Path())
if err != nil { if err != nil {
return fmt.Errorf("plugin is installed but unusable: %w", err) return fmt.Errorf("plugin is installed but unusable: %w", err)
} }
if err := runHook(p, plugin.Install); err != nil { if err := runHook(p, subprocess.Install); err != nil {
return err return err
} }

@ -24,7 +24,9 @@ import (
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/plugin" "helm.sh/helm/v4/internal/plugins"
pluginloader "helm.sh/helm/v4/internal/plugins/loader"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
) )
func newPluginListCmd(out io.Writer) *cobra.Command { func newPluginListCmd(out io.Writer) *cobra.Command {
@ -35,7 +37,9 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
ValidArgsFunction: noMoreArgsCompFunc, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
slog.Debug("pluginDirs", "directory", settings.PluginsDirectory) slog.Debug("pluginDirs", "directory", settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := pluginloader.FindPlugins(
[]string{settings.PluginsDirectory},
cliPluginDescriptor)
if err != nil { if err != nil {
return err return err
} }
@ -43,7 +47,8 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
table := uitable.New() table := uitable.New()
table.AddRow("NAME", "VERSION", "DESCRIPTION") table.AddRow("NAME", "VERSION", "DESCRIPTION")
for _, p := range plugins { for _, p := range plugins {
table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description) sp := p.(*subprocess.Plugin)
table.AddRow(sp.Metadata.Name, sp.Metadata.Version, sp.Metadata.Description)
} }
fmt.Fprintln(out, table) fmt.Fprintln(out, table)
return nil return nil
@ -53,17 +58,18 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
} }
// Returns all plugins from plugins, except those with names matching ignoredPluginNames // Returns all plugins from plugins, except those with names matching ignoredPluginNames
func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plugin.Plugin { func filterPlugins(ps []plugins.Plugin, ignoredPluginNames []string) []plugins.Plugin {
// if ignoredPluginNames is nil, just return plugins // if ignoredPluginNames is nil, just return plugins
if ignoredPluginNames == nil { if ignoredPluginNames == nil {
return plugins return ps
} }
var filteredPlugins []*plugin.Plugin filteredPlugins := make([]plugins.Plugin, 0, len(ps))
for _, plugin := range plugins { for _, p := range ps {
found := slices.Contains(ignoredPluginNames, plugin.Metadata.Name) sp := p.(*subprocess.Plugin)
found := slices.Contains(ignoredPluginNames, sp.Metadata.Name)
if !found { if !found {
filteredPlugins = append(filteredPlugins, plugin) filteredPlugins = append(filteredPlugins, sp)
} }
} }
@ -73,11 +79,14 @@ func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plu
// Provide dynamic auto-completion for plugin names // Provide dynamic auto-completion for plugin names
func compListPlugins(_ string, ignoredPluginNames []string) []string { func compListPlugins(_ string, ignoredPluginNames []string) []string {
var pNames []string var pNames []string
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := pluginloader.FindPlugins(
[]string{settings.PluginsDirectory},
cliPluginDescriptor)
if err == nil && len(plugins) > 0 { if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames) filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins { for _, p := range filteredPlugins {
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage)) sp := p.(*subprocess.Plugin)
pNames = append(pNames, fmt.Sprintf("%s\t%s", sp.Metadata.Name, sp.Metadata.Usage))
} }
} }
return pNames return pNames

@ -24,7 +24,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/plugin" "helm.sh/helm/v4/internal/plugins"
pluginloader "helm.sh/helm/v4/internal/plugins/loader"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
) )
type pluginUninstallOptions struct { type pluginUninstallOptions struct {
@ -61,7 +63,9 @@ func (o *pluginUninstallOptions) complete(args []string) error {
func (o *pluginUninstallOptions) run(out io.Writer) error { func (o *pluginUninstallOptions) run(out io.Writer) error {
slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory) slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := pluginloader.FindPlugins(
[]string{settings.PluginsDirectory},
cliPluginDescriptor)
if err != nil { if err != nil {
return err return err
} }
@ -83,16 +87,18 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
return nil return nil
} }
func uninstallPlugin(p *plugin.Plugin) error { func uninstallPlugin(p plugins.Plugin) error {
if err := os.RemoveAll(p.Dir); err != nil { sp := p.(*subprocess.Plugin)
if err := os.RemoveAll(sp.Dir); err != nil {
return err return err
} }
return runHook(p, plugin.Delete) return runHook(sp, subprocess.Delete)
} }
func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin { func findPlugin(ps []plugins.Plugin, name string) plugins.Plugin {
for _, p := range plugins { for _, p := range ps {
if p.Metadata.Name == name { sp := p.(*subprocess.Plugin)
if sp.Metadata.Name == name {
return p return p
} }
} }

@ -24,8 +24,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/plugin" "helm.sh/helm/v4/internal/plugins"
"helm.sh/helm/v4/pkg/plugin/installer" pluginloader "helm.sh/helm/v4/internal/plugins/loader"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess"
"helm.sh/helm/v4/internal/plugins/runtimes/subprocess/installer"
) )
type pluginUpdateOptions struct { type pluginUpdateOptions struct {
@ -63,7 +66,9 @@ func (o *pluginUpdateOptions) complete(args []string) error {
func (o *pluginUpdateOptions) run(out io.Writer) error { func (o *pluginUpdateOptions) run(out io.Writer) error {
installer.Debug = settings.Debug installer.Debug = settings.Debug
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) slog.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := pluginloader.FindPlugins(
[]string{settings.PluginsDirectory},
cliPluginDescriptor)
if err != nil { if err != nil {
return err return err
} }
@ -86,8 +91,10 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
return nil return nil
} }
func updatePlugin(p *plugin.Plugin) error { func updatePlugin(p plugins.Plugin) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir) sp := p.(*subprocess.Plugin)
exactLocation, err := filepath.EvalSymlinks(sp.Dir)
if err != nil { if err != nil {
return err return err
} }
@ -105,10 +112,10 @@ func updatePlugin(p *plugin.Plugin) error {
} }
slog.Debug("loading plugin", "path", i.Path()) slog.Debug("loading plugin", "path", i.Path())
updatedPlugin, err := plugin.LoadDir(i.Path()) updatedPlugin, err := subprocess.LoadDir(i.Path())
if err != nil { if err != nil {
return err return err
} }
return runHook(updatedPlugin, plugin.Update) return runHook(updatedPlugin, subprocess.Update)
} }

@ -27,10 +27,10 @@ import (
"helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/registry"
) )
// options are generic parameters to be provided to the getter during instantiation. // getterOptions are generic parameters to be provided to the getter during instantiation.
// //
// Getters may or may not ignore these parameters as they are passed in. // Getters may or may not ignore these parameters as they are passed in.
type options struct { type getterOptions struct {
url string url string
certFile string certFile string
keyFile string keyFile string
@ -51,54 +51,54 @@ type options struct {
// Option allows specifying various settings configurable by the user for overriding the defaults // Option allows specifying various settings configurable by the user for overriding the defaults
// used when performing Get operations with the Getter. // used when performing Get operations with the Getter.
type Option func(*options) type Option func(*getterOptions)
// WithURL informs the getter the server name that will be used when fetching objects. Used in conjunction with // WithURL informs the getter the server name that will be used when fetching objects. Used in conjunction with
// WithTLSClientConfig to set the TLSClientConfig's server name. // WithTLSClientConfig to set the TLSClientConfig's server name.
func WithURL(url string) Option { func WithURL(url string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.url = url opts.url = url
} }
} }
// WithAcceptHeader sets the request's Accept header as some REST APIs serve multiple content types // WithAcceptHeader sets the request's Accept header as some REST APIs serve multiple content types
func WithAcceptHeader(header string) Option { func WithAcceptHeader(header string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.acceptHeader = header opts.acceptHeader = header
} }
} }
// WithBasicAuth sets the request's Authorization header to use the provided credentials // WithBasicAuth sets the request's Authorization header to use the provided credentials
func WithBasicAuth(username, password string) Option { func WithBasicAuth(username, password string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.username = username opts.username = username
opts.password = password opts.password = password
} }
} }
func WithPassCredentialsAll(pass bool) Option { func WithPassCredentialsAll(pass bool) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.passCredentialsAll = pass opts.passCredentialsAll = pass
} }
} }
// WithUserAgent sets the request's User-Agent header to use the provided agent name. // WithUserAgent sets the request's User-Agent header to use the provided agent name.
func WithUserAgent(userAgent string) Option { func WithUserAgent(userAgent string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.userAgent = userAgent opts.userAgent = userAgent
} }
} }
// WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked // WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked
func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS
} }
} }
// WithTLSClientConfig sets the client auth with the provided credentials. // WithTLSClientConfig sets the client auth with the provided credentials.
func WithTLSClientConfig(certFile, keyFile, caFile string) Option { func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.certFile = certFile opts.certFile = certFile
opts.keyFile = keyFile opts.keyFile = keyFile
opts.caFile = caFile opts.caFile = caFile
@ -106,39 +106,39 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
} }
func WithPlainHTTP(plainHTTP bool) Option { func WithPlainHTTP(plainHTTP bool) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.plainHTTP = plainHTTP opts.plainHTTP = plainHTTP
} }
} }
// WithTimeout sets the timeout for requests // WithTimeout sets the timeout for requests
func WithTimeout(timeout time.Duration) Option { func WithTimeout(timeout time.Duration) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.timeout = timeout opts.timeout = timeout
} }
} }
func WithTagName(tagname string) Option { func WithTagName(tagname string) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.version = tagname opts.version = tagname
} }
} }
func WithRegistryClient(client *registry.Client) Option { func WithRegistryClient(client *registry.Client) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.registryClient = client opts.registryClient = client
} }
} }
func WithUntar() Option { func WithUntar() Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.unTar = true opts.unTar = true
} }
} }
// WithTransport sets the http.Transport to allow overwriting the HTTPGetter default. // WithTransport sets the http.Transport to allow overwriting the HTTPGetter default.
func WithTransport(transport *http.Transport) Option { func WithTransport(transport *http.Transport) Option {
return func(opts *options) { return func(opts *getterOptions) {
opts.transport = transport opts.transport = transport
} }
} }
@ -217,7 +217,7 @@ func Getters(extraOpts ...Option) Providers {
// notations are collected. // notations are collected.
func All(settings *cli.EnvSettings, opts ...Option) Providers { func All(settings *cli.EnvSettings, opts ...Option) Providers {
result := Getters(opts...) result := Getters(opts...)
pluginDownloaders, _ := collectPlugins(settings) pluginDownloaders, _ := collectDownloaderPlugins(settings)
result = append(result, pluginDownloaders...) result = append(result, pluginDownloaders...)
return result return result
} }

@ -30,7 +30,7 @@ import (
// HTTPGetter is the default HTTP(/S) backend handler // HTTPGetter is the default HTTP(/S) backend handler
type HTTPGetter struct { type HTTPGetter struct {
opts options opts getterOptions
transport *http.Transport transport *http.Transport
once sync.Once once sync.Once
} }

@ -32,7 +32,7 @@ import (
// OCIGetter is the default HTTP(/S) backend handler // OCIGetter is the default HTTP(/S) backend handler
type OCIGetter struct { type OCIGetter struct {
opts options opts getterOptions
transport *http.Transport transport *http.Transport
once sync.Once once sync.Once
} }

@ -17,92 +17,136 @@ package getter
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath"
"strings"
"helm.sh/helm/v4/internal/plugins"
pluginloader "helm.sh/helm/v4/internal/plugins/loader"
"helm.sh/helm/v4/internal/plugins/schema"
"helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/plugin"
) )
// collectPlugins scans for getter plugins. // collectDownloaderPlugins scans for getter plugins.
// This will load plugins according to the cli. // This will load plugins according to the cli.
func collectPlugins(settings *cli.EnvSettings) (Providers, error) { func collectDownloaderPlugins(settings *cli.EnvSettings) (Providers, error) {
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
d := plugins.PluginDescriptor{
TypeVersion: "getter/v1",
}
plgs, err := pluginloader.FindPlugins([]string{settings.PluginsDirectory}, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var result Providers
for _, plugin := range plugins { pluginConstructorBuilder := func(plg plugins.Plugin) Constructor {
for _, downloader := range plugin.Metadata.Downloaders { return func(option ...Option) (Getter, error) {
result = append(result, Provider{
Schemes: downloader.Protocols, return &getterPlugin{
New: NewPluginGetter( options: append([]Option{}, option...),
downloader.Command, plg: plg,
settings, }, nil
plugin.Metadata.Name,
plugin.Dir,
),
})
} }
} }
return result, nil
}
// pluginGetter is a generic type to invoke custom downloaders, results := make([]Provider, 0, len(plgs))
// implemented in plugins.
type pluginGetter struct {
command string
settings *cli.EnvSettings
name string
base string
opts options
}
func (p *pluginGetter) setupOptionsEnv(env []string) []string { for _, plg := range plgs {
env = append(env, fmt.Sprintf("HELM_PLUGIN_USERNAME=%s", p.opts.username))
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASSWORD=%s", p.opts.password)) downloaderSchemes, ok := (plg.Manifest().Config["downloader_schemes"]).([]string)
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASS_CREDENTIALS_ALL=%t", p.opts.passCredentialsAll)) if !ok {
return env return nil, fmt.Errorf("plugin %q does not have downloader_schemes defined", plg.Manifest().Name)
}
results = append(results, Provider{
Schemes: downloaderSchemes,
New: pluginConstructorBuilder(plg),
})
}
return results, nil
} }
// Get runs downloader plugin command func convertOptions(globalOptions, options []Option) (schema.GetterOptionsV1, error) {
func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { opts := getterOptions{}
for _, opt := range globalOptions {
opt(&opts)
}
for _, opt := range options { for _, opt := range options {
opt(&p.opts) opt(&opts)
}
result := schema.GetterOptionsV1{
URL: opts.url,
// CertFile string
// KeyFile string
// CAFile string
UNTar: opts.unTar,
InsecureSkipVerifyTLS: opts.insecureSkipVerifyTLS,
PlainHTTP: opts.plainHTTP,
AcceptHeader: opts.acceptHeader,
Username: opts.username,
Password: opts.password,
PassCredentialsAll: opts.passCredentialsAll,
UserAgent: opts.userAgent,
Version: opts.version,
Timeout: opts.timeout,
} }
commands := strings.Split(p.command, " ")
argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href) if opts.caFile != "" {
prog := exec.Command(filepath.Join(p.base, commands[0]), argv...) caData, err := os.ReadFile(opts.caFile)
plugin.SetupPluginEnv(p.settings, p.name, p.base) if err != nil {
prog.Env = p.setupOptionsEnv(os.Environ()) return schema.GetterOptionsV1{}, fmt.Errorf("unable to read CA file: %q: %w", opts.caFile, err)
buf := bytes.NewBuffer(nil)
prog.Stdout = buf
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
return nil, fmt.Errorf("plugin %q exited with error", p.command)
} }
return nil, err result.CA = caData
} }
return buf, nil
}
// NewPluginGetter constructs a valid plugin getter if opts.certFile != "" || opts.keyFile != "" {
func NewPluginGetter(command string, settings *cli.EnvSettings, name, base string) Constructor {
return func(options ...Option) (Getter, error) { certData, err := os.ReadFile(opts.certFile)
result := &pluginGetter{ if err != nil {
command: command, return schema.GetterOptionsV1{}, fmt.Errorf("unable to read cert file: %q: %w", opts.certFile, err)
settings: settings,
name: name,
base: base,
} }
for _, opt := range options {
opt(&result.opts) keyData, err := os.ReadFile(opts.keyFile)
if err != nil {
return schema.GetterOptionsV1{}, fmt.Errorf("unable to read key file: %q: %w", opts.keyFile, err)
} }
return result, nil
result.Cert = certData
result.Key = keyData
} }
return result, nil
}
type getterPlugin struct {
options []Option
plg plugins.Plugin
}
func (g *getterPlugin) Get(url string, options ...Option) (*bytes.Buffer, error) {
opts, err := convertOptions(g.options, options)
if err != nil {
return nil, err
}
input := &plugins.Input{
Message: schema.GetterInputV1{
URL: url,
Options: opts,
},
}
output, err := g.plg.Invoke(context.Background(), input)
if err != nil {
return nil, fmt.Errorf("plugin %q failed to invoke: %w", g.plg, err)
}
outputMessage, ok := output.Message.(schema.GetterOutputV1)
if !ok {
return nil, fmt.Errorf("invalid output message type from plugin %q", g.plg.Manifest().Name)
}
return outputMessage.Data, nil
} }

@ -16,8 +16,6 @@ limitations under the License.
package getter package getter
import ( import (
"runtime"
"strings"
"testing" "testing"
"helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli"
@ -27,7 +25,7 @@ func TestCollectPlugins(t *testing.T) {
env := cli.New() env := cli.New()
env.PluginsDirectory = pluginDir env.PluginsDirectory = pluginDir
p, err := collectPlugins(env) p, err := collectDownloaderPlugins(env)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -49,53 +47,53 @@ func TestCollectPlugins(t *testing.T) {
} }
} }
func TestPluginGetter(t *testing.T) { //func TestPluginGetter(t *testing.T) {
if runtime.GOOS == "windows" { // if runtime.GOOS == "windows" {
t.Skip("TODO: refactor this test to work on windows") // t.Skip("TODO: refactor this test to work on windows")
} // }
//
env := cli.New() // env := cli.New()
env.PluginsDirectory = pluginDir // env.PluginsDirectory = pluginDir
pg := NewPluginGetter("echo", env, "test", ".") // pg := NewPluginGetter("echo", env, "test", ".")
g, err := pg() // g, err := pg()
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
//
data, err := g.Get("test://foo/bar") // data, err := g.Get("test://foo/bar")
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
//
expect := "test://foo/bar" // expect := "test://foo/bar"
got := strings.TrimSpace(data.String()) // got := strings.TrimSpace(data.String())
if got != expect { // if got != expect {
t.Errorf("Expected %q, got %q", expect, got) // t.Errorf("Expected %q, got %q", expect, got)
} // }
} //}
func TestPluginSubCommands(t *testing.T) { //func TestPluginSubCommands(t *testing.T) {
if runtime.GOOS == "windows" { // if runtime.GOOS == "windows" {
t.Skip("TODO: refactor this test to work on windows") // t.Skip("TODO: refactor this test to work on windows")
} // }
//
env := cli.New() // env := cli.New()
env.PluginsDirectory = pluginDir // env.PluginsDirectory = pluginDir
//
pg := NewPluginGetter("echo -n", env, "test", ".") // pg := NewPluginGetter("echo -n", env, "test", ".")
g, err := pg() // g, err := pg()
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
//
data, err := g.Get("test://foo/bar") // data, err := g.Get("test://foo/bar")
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
//
expect := " test://foo/bar" // expect := " test://foo/bar"
got := data.String() // got := data.String()
if got != expect { // if got != expect {
t.Errorf("Expected %q, got %q", expect, got) // t.Errorf("Expected %q, got %q", expect, got)
} // }
} //}

Loading…
Cancel
Save