Repository: helm/helm
Pull request #7560 (2020-02-08 01:04:22)

feat(tests): Allow to provision memory driver
The memory driver is used for go tests. It can also be used from the
command-line by setting the environment variable HELM_DRIVER=memory.
In the latter case however, there was no way to pre-provision some
releases.

This commit introduces the HELM_MEMORY_DRIVER_DATA variable which
can be used to provide a colon-separated list of yaml files specifying
releases to provision automatically.

An example to try this out:
   HELM_DRIVER=memory \
   HELM_MEMORY_DRIVER_DATA=./testdata/releases.yaml \
   helm list --all-namespaces

Note:

- This is meant to allow writing acceptance tests.  To illustrate new tests have been implemented in the following acceptance-testing PR: https://github.com/helm/acceptance-testing/pull/75

- This PR only allows to *pre-load* releases in the memory driver, it does not persist any changes made by the helm command itself.  It would be possible to simply dump the resulting memory driver content into the same yaml file to create an off-cluster storage mechanism.  However, I purposefully did not do this because it may give users the impression it is a valid way to store releases and since each helm command would parse the entire yaml file of releases and would also dump it, a growing set of releases would cause more and more slowness.  I think the right solution for an off-cluster storage is to use a database (like helm v2 had), not a file (this is beyond the scope of this PR).

marckhouzam: Thanks @hickeyma. I rebased and forced pushed.  In a situation like this, does the previous approval stand (assuming the ci build passes) or do the maintainers have to re-approve? 

Patch:
diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index 0ce40732b..257387547 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -19,6 +19,7 @@ package main // import "helm.sh/helm/v3/cmd/helm"
 import (
 	"flag"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"os"
 	"strings"
@@ -26,6 +27,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"k8s.io/klog"
+	"sigs.k8s.io/yaml"
 
 	// Import to initialize client auth plugins.
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -34,6 +36,9 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/gates"
+	kubefake "helm.sh/helm/v3/pkg/kube/fake"
+	"helm.sh/helm/v3/pkg/release"
+	"helm.sh/helm/v3/pkg/storage/driver"
 )
 
 // FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
@@ -78,9 +83,13 @@ func main() {
 		}
 	}
 
-	if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
+	helmDriver := os.Getenv("HELM_DRIVER")
+	if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil {
 		log.Fatal(err)
 	}
+	if helmDriver == "memory" {
+		loadReleasesInMemory(actionConfig)
+	}
 
 	if err := cmd.Execute(); err != nil {
 		debug("%+v", err)
@@ -106,3 +115,41 @@ func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error {
 		return nil
 	}
 }
+
+// This function loads releases into the memory storage if the
+// environment variable is properly set.
+func loadReleasesInMemory(actionConfig *action.Configuration) {
+	filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":")
+	if len(filePaths) == 0 {
+		return
+	}
+
+	store := actionConfig.Releases
+	mem, ok := store.Driver.(*driver.Memory)
+	if !ok {
+		// For an unexpected reason we are not dealing with the memory storage driver.
+		return
+	}
+
+	actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
+
+	for _, path := range filePaths {
+		b, err := ioutil.ReadFile(path)
+		if err != nil {
+			log.Fatal("Unable to read memory driver data", err)
+		}
+
+		releases := []*release.Release{}
+		if err := yaml.Unmarshal(b, &releases); err != nil {
+			log.Fatal("Unable to unmarshal memory driver data: ", err)
+		}
+
+		for _, rel := range releases {
+			if err := store.Create(rel); err != nil {
+				log.Fatal(err)
+			}
+		}
+	}
+	// Must reset namespace to the proper one
+	mem.SetNamespace(settings.Namespace())
+}
diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go
index f2fb5c01d..358679302 100644
--- a/cmd/helm/load_plugins.go
+++ b/cmd/helm/load_plugins.go
@@ -16,20 +16,31 @@ limitations under the License.
 package main
 
 import (
+	"bytes"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"syscall"
 
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
+	"sigs.k8s.io/yaml"
 
+	"helm.sh/helm/v3/internal/completion"
 	"helm.sh/helm/v3/pkg/plugin"
 )
 
+const (
+	pluginStaticCompletionFile        = "completion.yaml"
+	pluginDynamicCompletionExecutable = "plugin.complete"
+)
+
 type pluginError struct {
 	error
 	code int
@@ -61,6 +72,13 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
 		return u, nil
 	}
 
+	// If we are dealing with the completion command, we try to load more details about the plugins
+	// if available, so as to allow for command and flag completion
+	if subCmd, _, err := baseCmd.Find(os.Args[1:]); err == nil && subCmd.Name() == "completion" {
+		loadPluginsForCompletion(baseCmd, found)
+		return
+	}
+
 	// Now we create commands for all of these.
 	for _, plug := range found {
 		plug := plug
@@ -69,6 +87,33 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
 			md.Usage = fmt.Sprintf("the %q plugin", md.Name)
 		}
 
+		// This function is used to setup the environment for the plugin and then
+		// call the executable specified by the parameter 'main'
+		callPluginExecutable := func(cmd *cobra.Command, main string, argv []string, out io.Writer) error {
+			env := os.Environ()
+			for k, v := range settings.EnvVars() {
+				env = append(env, fmt.Sprintf("%s=%s", k, v))
+			}
+
+			prog := exec.Command(main, argv...)
+			prog.Env = env
+			prog.Stdin = os.Stdin
+			prog.Stdout = out
+			prog.Stderr = os.Stderr
+			if err := prog.Run(); err != nil {
+				if eerr, ok := err.(*exec.ExitError); ok {
+					os.Stderr.Write(eerr.Stderr)
+					status := eerr.Sys().(syscall.WaitStatus)
+					return pluginError{
+						error: errors.Errorf("plugin %q exited with error", md.Name),
+						code:  status.ExitStatus(),
+					}
+				}
+				return err
+			}
+			return nil
+		}
+
 		c := &cobra.Command{
 			Use:   md.Name,
 			Short: md.Usage,
@@ -89,33 +134,59 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
 					return errors.Errorf("plugin %q exited with error", md.Name)
 				}
 
-				env := os.Environ()
-				for k, v := range settings.EnvVars() {
-					env = append(env, fmt.Sprintf("%s=%s", k, v))
-				}
-
-				prog := exec.Command(main, argv...)
-				prog.Env = env
-				prog.Stdin = os.Stdin
-				prog.Stdout = out
-				prog.Stderr = os.Stderr
-				if err := prog.Run(); err != nil {
-					if eerr, ok := err.(*exec.ExitError); ok {
-						os.Stderr.Write(eerr.Stderr)
-						status := eerr.Sys().(syscall.WaitStatus)
-						return pluginError{
-							error: errors.Errorf("plugin %q exited with error", md.Name),
-							code:  status.ExitStatus(),
-						}
-					}
-					return err
-				}
-				return nil
+				return callPluginExecutable(cmd, main, argv, out)
 			},
 			// This passes all the flags to the subcommand.
 			DisableFlagParsing: true,
 		}
 
+		// Setup dynamic completion for the plugin
+		completion.RegisterValidArgsFunc(c, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
+			u, err := processParent(cmd, args)
+			if err != nil {
+				return nil, completion.BashCompDirectiveError
+			}
+
+			// We will call the dynamic completion script of the plugin
+			main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator))
+
+			argv := []string{}
+			if !md.IgnoreFlags {
+				argv = append(argv, u...)
+				argv = append(argv, toComplete)
+			}
+			plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
+
+			completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv))
+			buf := new(bytes.Buffer)
+			if err := callPluginExecutable(cmd, main, argv, buf); err != nil {
+				return nil, completion.BashCompDirectiveError
+			}
+
+			var completions []string
+			for _, comp := range strings.Split(buf.String(), "\n") {
+				// Remove any empty lines
+				if len(comp) > 0 {
+					completions = append(completions, comp)
+				}
+			}
+
+			// Check if the last line of output is of the form :<integer>, which
+			// indicates the BashCompletionDirective.
+			directive := completion.BashCompDirectiveDefault
+			if len(completions) > 0 {
+				lastLine := completions[len(completions)-1]
+				if len(lastLine) > 1 && lastLine[0] == ':' {
+					if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
+						directive = completion.BashCompDirective(strInt)
+						completions = completions[:len(completions)-1]
+					}
+				}
+			}
+
+			return completions, directive
+		})
+
 		// TODO: Make sure a command with this name does not already exist.
 		baseCmd.AddCommand(c)
 	}
@@ -180,3 +251,119 @@ func findPlugins(plugdirs string) ([]*plugin.Plugin, error) {
 	}
 	return found, nil
 }
+
+// pluginCommand represents the optional completion.yaml file of a plugin
+type pluginCommand struct {
+	Name      string          `json:"name"`
+	ValidArgs []string        `json:"validArgs"`
+	Flags     []string        `json:"flags"`
+	Commands  []pluginCommand `json:"commands"`
+}
+
+// loadPluginsForCompletion will load and parse any completion.yaml provided by the plugins
+func loadPluginsForCompletion(baseCmd *cobra.Command, plugins []*plugin.Plugin) {
+	for _, plug := range plugins {
+		// Parse the yaml file providing the plugin's subcmds and flags
+		cmds, err := loadFile(strings.Join(
+			[]string{plug.Dir, pluginStaticCompletionFile}, string(filepath.Separator)))
+
+		if err != nil {
+			// The file could be missing or invalid.  Either way, we at least create the command
+			// for the plugin name.
+			if settings.Debug {
+				log.Output(2, fmt.Sprintf("[info] %s\n", err.Error()))
+			}
+			cmds = &pluginCommand{Name: plug.Metadata.Name}
+		}
+
+		// We know what the plugin name must be.
+		// Let's set it in case the Name field was not specified correctly in the file.
+		// This insures that we will at least get the plugin name to complete, even if
+		// there is a problem with the completion.yaml file
+		cmds.Name = plug.Metadata.Name
+
+		addPluginCommands(baseCmd, cmds)
+	}
+}
+
+// addPluginCommands is a recursive method that adds the different levels
+// of sub-commands and flags for the plugins that provide such information
+func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
+	if cmds == nil {
+		return
+	}
+
+	if len(cmds.Name) == 0 {
+		// Missing name for a command
+		if settings.Debug {
+			log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath()))
+		}
+		return
+	}
+
+	// Create a fake command just so the completion script will include it
+	c := &cobra.Command{
+		Use:       cmds.Name,
+		ValidArgs: cmds.ValidArgs,
+		// A Run is required for it to be a valid command without subcommands
+		Run: func(cmd *cobra.Command, args []string) {},
+	}
+	baseCmd.AddCommand(c)
+
+	// Create fake flags.
+	if len(cmds.Flags) > 0 {
+		// The flags can be created with any type, since we only need them for completion.
+		// pflag does not allow to create short flags without a corresponding long form
+		// so we look for all short flags and match them to any long flag.  This will allow
+		// plugins to provide short flags without a long form.
+		// If there are more short-flags than long ones, we'll create an extra long flag with
+		// the same single letter as the short form.
+		shorts := []string{}
+		longs := []string{}
+		for _, flag := range cmds.Flags {
+			if len(flag) == 1 {
+				shorts = append(shorts, flag)
+			} else {
+				longs = append(longs, flag)
+			}
+		}
+
+		f := c.Flags()
+		if len(longs) >= len(shorts) {
+			for i := range longs {
+				if i < len(shorts) {
+					f.BoolP(longs[i], shorts[i], false, "")
+				} else {
+					f.Bool(longs[i], false, "")
+				}
+			}
+		} else {
+			for i := range shorts {
+				if i < len(longs) {
+					f.BoolP(longs[i], shorts[i], false, "")
+				} else {
+					// Create a long flag with the same name as the short flag.
+					// Not a perfect solution, but its better than ignoring the extra short flags.
+					f.BoolP(shorts[i], shorts[i], false, "")
+				}
+			}
+		}
+	}
+
+	// Recursively add any sub-commands
+	for _, cmd := range cmds.Commands {
+		addPluginCommands(c, &cmd)
+	}
+}
+
+// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object
+func loadFile(path string) (*pluginCommand, error) {
+	cmds := new(pluginCommand)
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path))
+	}
+
+	err = yaml.Unmarshal(b, cmds)
+	return cmds, err
+}
diff --git a/internal/completion/complete.go b/internal/completion/complete.go
index aa0d134b9..c8f78868a 100644
--- a/internal/completion/complete.go
+++ b/internal/completion/complete.go
@@ -188,29 +188,47 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
 		Run: func(cmd *cobra.Command, args []string) {
 			CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))
 
-			flag, trimmedArgs, toComplete, err := checkIfFlagCompletion(cmd.Root(), args[:len(args)-1], args[len(args)-1])
-			if err != nil {
-				// Error while attempting to parse flags
-				CompErrorln(err.Error())
-				return
-			}
+			// The last argument, which is not complete, should not be part of the list of arguments
+			toComplete := args[len(args)-1]
+			trimmedArgs := args[:len(args)-1]
+
 			// Find the real command for which completion must be performed
 			finalCmd, finalArgs, err := cmd.Root().Find(trimmedArgs)
 			if err != nil {
 				// Unable to find the real command. E.g., helm invalidCmd <TAB>
+				CompDebugln(fmt.Sprintf("Unable to find a command for arguments: %v", trimmedArgs))
 				return
 			}
 
 			CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs))
 
+			var flag *pflag.Flag
+			if !finalCmd.DisableFlagParsing {
+				// We only do flag completion if we are allowed to parse flags
+				// This is important for helm plugins which need to do their own flag completion.
+				flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
+				if err != nil {
+					// Error while attempting to parse flags
+					CompErrorln(err.Error())
+					return
+				}
+			}
+
 			// Parse the flags and extract the arguments to prepare for calling the completion function
 			if err = finalCmd.ParseFlags(finalArgs); err != nil {
 				CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error()))
 				return
 			}
-			argsWoFlags := finalCmd.Flags().Args()
-			CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", argsWoFlags, len(argsWoFlags)))
 
+			// We only remove the flags from the arguments if DisableFlagParsing is not set.
+			// This is important for helm plugins, which need to receive all flags.
+			// The plugin completion code will do its own flag parsing.
+			if !finalCmd.DisableFlagParsing {
+				finalArgs = finalCmd.Flags().Args()
+				CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", finalArgs, len(finalArgs)))
+			}
+
+			// Find completion function for the flag or command
 			var key interface{}
 			var keyStr string
 			if flag != nil {
@@ -220,21 +238,23 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
 				key = finalCmd
 				keyStr = finalCmd.Name()
 			}
-
-			// Find completion function for the flag or command
 			completionFn, ok := validArgsFunctions[key]
 			if !ok {
 				CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr))
 				return
 			}
 
-			CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), argsWoFlags, toComplete))
-			completions, directive := completionFn(finalCmd, argsWoFlags, toComplete)
+			CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), finalArgs, toComplete))
+			completions, directive := completionFn(finalCmd, finalArgs, toComplete)
 			for _, comp := range completions {
 				// Print each possible completion to stdout for the completion script to consume.
 				fmt.Fprintln(out, comp)
 			}
 
+			if directive > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
+				directive = BashCompDirectiveDefault
+			}
+
 			// As the last printout, print the completion directive for the
 			// completion script to parse.
 			// The directive integer must be that last character following a single :
@@ -252,7 +272,7 @@ func isFlag(arg string) bool {
 	return len(arg) > 0 && arg[0] == '-'
 }
 
-func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
+func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
 	var flagName string
 	trimmedArgs := args
 	flagWithEqual := false
@@ -287,19 +307,10 @@ func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string
 		return nil, trimmedArgs, lastArg, nil
 	}
 
-	// Find the real command for which completion must be performed
-	finalCmd, _, err := rootCmd.Find(trimmedArgs)
-	if err != nil {
-		// Unable to find the real command. E.g., helm invalidCmd <TAB>
-		return nil, nil, "", errors.New("Unable to find final command for completion")
-	}
-
-	CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found final command '%s'", finalCmd.Name()))
-
 	flag := findFlag(finalCmd, flagName)
 	if flag == nil {
 		// Flag not supported by this command, nothing to complete
-		err = fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
+		err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
 		return nil, nil, "", err
 	}
 
diff --git a/pkg/action/action.go b/pkg/action/action.go
index 1af5b3d9a..e4db942c8 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -241,7 +241,18 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac
 		d.Log = log
 		store = storage.Init(d)
 	case "memory":
-		d := driver.NewMemory()
+		var d *driver.Memory
+		if c.Releases != nil {
+			if mem, ok := c.Releases.Driver.(*driver.Memory); ok {
+				// This function can be called more than once (e.g., helm list --all-namespaces).
+				// If a memory driver was already initialized, re-use it but set the possibly new namespace.
+				// We re-use it in case some releases where already created in the existing memory driver.
+				d = mem
+			}
+		}
+		if d == nil {
+			d = driver.NewMemory()
+		}
 		d.SetNamespace(namespace)
 		store = storage.Init(d)
 	default:


Test patch:
diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go
index 3fd3a4197..e43f277a5 100644
--- a/cmd/helm/plugin_test.go
+++ b/cmd/helm/plugin_test.go
@@ -19,10 +19,14 @@ import (
 	"bytes"
 	"os"
 	"runtime"
+	"sort"
 	"strings"
 	"testing"
 
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+
+	"helm.sh/helm/v3/pkg/release"
 )
 
 func TestManuallyProcessArgs(t *testing.T) {
@@ -151,6 +155,134 @@ func TestLoadPlugins(t *testing.T) {
 	}
 }
 
+type staticCompletionDetails struct {
+	use       string
+	validArgs []string
+	flags     []string
+	next      []staticCompletionDetails
+}
+
+func TestLoadPluginsForCompletion(t *testing.T) {
+	settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
+
+	var out bytes.Buffer
+
+	cmd := &cobra.Command{
+		Use: "completion",
+	}
+
+	loadPlugins(cmd, &out)
+
+	tests := []staticCompletionDetails{
+		{"args", []string{}, []string{}, []staticCompletionDetails{}},
+		{"echo", []string{}, []string{}, []staticCompletionDetails{}},
+		{"env", []string{}, []string{"global"}, []staticCompletionDetails{
+			{"list", []string{}, []string{"a", "all", "log"}, []staticCompletionDetails{}},
+			{"remove", []string{"all", "one"}, []string{}, []staticCompletionDetails{}},
+		}},
+		{"exitwith", []string{}, []string{}, []staticCompletionDetails{
+			{"code", []string{}, []string{"a", "b"}, []staticCompletionDetails{}},
+		}},
+		{"fullenv", []string{}, []string{"q", "z"}, []staticCompletionDetails{
+			{"empty", []string{}, []string{}, []staticCompletionDetails{}},
+			{"full", []string{}, []string{}, []staticCompletionDetails{
+				{"less", []string{}, []string{"a", "all"}, []staticCompletionDetails{}},
+				{"more", []string{"one", "two"}, []string{"b", "ball"}, []staticCompletionDetails{}},
+			}},
+		}},
+	}
+	checkCommand(t, cmd.Commands(), tests)
+}
+
+func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) {
+	if len(plugins) != len(tests) {
+		t.Fatalf("Expected commands %v, got %v", tests, plugins)
+	}
+
+	for i := 0; i < len(plugins); i++ {
+		pp := plugins[i]
+		tt := tests[i]
+		if pp.Use != tt.use {
+			t.Errorf("%s: Expected Use=%q, got %q", pp.Name(), tt.use, pp.Use)
+		}
+
+		targs := tt.validArgs
+		pargs := pp.ValidArgs
+		if len(targs) != len(pargs) {
+			t.Fatalf("%s: expected args %v, got %v", pp.Name(), targs, pargs)
+		}
+
+		sort.Strings(targs)
+		sort.Strings(pargs)
+		for j := range targs {
+			if targs[j] != pargs[j] {
+				t.Errorf("%s: expected validArg=%q, got %q", pp.Name(), targs[j], pargs[j])
+			}
+		}
+
+		tflags := tt.flags
+		var pflags []string
+		pp.LocalFlags().VisitAll(func(flag *pflag.Flag) {
+			pflags = append(pflags, flag.Name)
+			if len(flag.Shorthand) > 0 && flag.Shorthand != flag.Name {
+				pflags = append(pflags, flag.Shorthand)
+			}
+		})
+		if len(tflags) != len(pflags) {
+			t.Fatalf("%s: expected flags %v, got %v", pp.Name(), tflags, pflags)
+		}
+
+		sort.Strings(tflags)
+		sort.Strings(pflags)
+		for j := range tflags {
+			if tflags[j] != pflags[j] {
+				t.Errorf("%s: expected flag=%q, got %q", pp.Name(), tflags[j], pflags[j])
+			}
+		}
+		// Check the next level
+		checkCommand(t, pp.Commands(), tt.next)
+	}
+}
+
+func TestPluginDynamicCompletion(t *testing.T) {
+
+	tests := []cmdTestCase{{
+		name:   "completion for plugin",
+		cmd:    "__complete args ''",
+		golden: "output/plugin_args_comp.txt",
+		rels:   []*release.Release{},
+	}, {
+		name:   "completion for plugin with flag",
+		cmd:    "__complete args --myflag ''",
+		golden: "output/plugin_args_flag_comp.txt",
+		rels:   []*release.Release{},
+	}, {
+		name:   "completion for plugin with global flag",
+		cmd:    "__complete args --namespace mynamespace ''",
+		golden: "output/plugin_args_ns_comp.txt",
+		rels:   []*release.Release{},
+	}, {
+		name:   "completion for plugin with multiple args",
+		cmd:    "__complete args --myflag --namespace mynamespace start",
+		golden: "output/plugin_args_many_args_comp.txt",
+		rels:   []*release.Release{},
+	}, {
+		name:   "completion for plugin no directive",
+		cmd:    "__complete echo -n mynamespace ''",
+		golden: "output/plugin_echo_no_directive.txt",
+		rels:   []*release.Release{},
+	}, {
+		name:   "completion for plugin bad directive",
+		cmd:    "__complete echo ''",
+		golden: "output/plugin_echo_bad_directive.txt",
+		rels:   []*release.Release{},
+	}}
+	for _, test := range tests {
+		settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
+		runTestCmd(t, []cmdTestCase{test})
+	}
+}
+
 func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
 	settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
 	settings.RepositoryConfig = "testdata/helmhome/helm/repository"
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete
new file mode 100755
index 000000000..2b00c2281
--- /dev/null
+++ b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete
@@ -0,0 +1,13 @@
+#!/usr/bin/env sh
+
+echo "plugin.complete was called"
+echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
+echo "Num args received: ${#}"
+echo "Args received: ${@}"
+
+# Final printout is the optional completion directive of the form :<directive>
+if [ "$HELM_NAMESPACE" = "default" ]; then
+    echo ":4"
+else
+    echo ":2"
+fi
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete
new file mode 100755
index 000000000..6bc73d130
--- /dev/null
+++ b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete
@@ -0,0 +1,14 @@
+#!/usr/bin/env sh
+
+echo "echo plugin.complete was called"
+echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
+echo "Num args received: ${#}"
+echo "Args received: ${@}"
+
+# Final printout is the optional completion directive of the form :<directive>
+if [ "$HELM_NAMESPACE" = "default" ]; then
+    # Output an invalid directive, which should be ignored
+    echo ":2222"
+# else
+    # Don't include the directive, to test it is really optional
+fi
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml
new file mode 100644
index 000000000..e479a0503
--- /dev/null
+++ b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml
@@ -0,0 +1,13 @@
+name: env
+commands:
+  - name: list
+    flags:
+    - a
+    - all
+    - log
+  - name: remove
+    validArgs:
+    - all
+    - one
+flags:
+- global
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml
new file mode 100644
index 000000000..e5bf440f6
--- /dev/null
+++ b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml
@@ -0,0 +1,5 @@
+commands:
+  - name: code
+    flags:
+    - a
+    - b
diff --git a/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml
new file mode 100644
index 000000000..e0b161c69
--- /dev/null
+++ b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml
@@ -0,0 +1,19 @@
+name: wrongname
+commands:
+  - name: empty
+  - name: full
+    commands:
+    - name: more
+      validArgs:
+      - one
+      - two
+      flags:
+      - b
+      - ball
+    - name: less
+      flags:
+      - a
+      - all
+flags:
+- z
+- q
diff --git a/cmd/helm/testdata/output/plugin_args_comp.txt b/cmd/helm/testdata/output/plugin_args_comp.txt
new file mode 100644
index 000000000..8fb01cc23
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_args_comp.txt
@@ -0,0 +1,5 @@
+plugin.complete was called
+Namespace: default
+Num args received: 1
+Args received: 
+:4
diff --git a/cmd/helm/testdata/output/plugin_args_flag_comp.txt b/cmd/helm/testdata/output/plugin_args_flag_comp.txt
new file mode 100644
index 000000000..92f0e58a8
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_args_flag_comp.txt
@@ -0,0 +1,5 @@
+plugin.complete was called
+Namespace: default
+Num args received: 2
+Args received: --myflag 
+:4
diff --git a/cmd/helm/testdata/output/plugin_args_many_args_comp.txt b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt
new file mode 100644
index 000000000..86fa768bb
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt
@@ -0,0 +1,5 @@
+plugin.complete was called
+Namespace: mynamespace
+Num args received: 2
+Args received: --myflag start
+:2
diff --git a/cmd/helm/testdata/output/plugin_args_ns_comp.txt b/cmd/helm/testdata/output/plugin_args_ns_comp.txt
new file mode 100644
index 000000000..e12867daa
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_args_ns_comp.txt
@@ -0,0 +1,5 @@
+plugin.complete was called
+Namespace: mynamespace
+Num args received: 1
+Args received: 
+:2
diff --git a/cmd/helm/testdata/output/plugin_echo_bad_directive.txt b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt
new file mode 100644
index 000000000..f4b86cd47
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt
@@ -0,0 +1,5 @@
+echo plugin.complete was called
+Namespace: default
+Num args received: 1
+Args received: 
+:0
diff --git a/cmd/helm/testdata/output/plugin_echo_no_directive.txt b/cmd/helm/testdata/output/plugin_echo_no_directive.txt
new file mode 100644
index 000000000..6266dd4d9
--- /dev/null
+++ b/cmd/helm/testdata/output/plugin_echo_no_directive.txt
@@ -0,0 +1,5 @@
+echo plugin.complete was called
+Namespace: mynamespace
+Num args received: 1
+Args received: 
+:0
diff --git a/testdata/releases.yaml b/testdata/releases.yaml
new file mode 100644
index 000000000..fef79f424
--- /dev/null
+++ b/testdata/releases.yaml
@@ -0,0 +1,43 @@
+# This file can be used as input to create test releases:
+# HELM_MEMORY_DRIVER_DATA=./testdata/releases.yaml HELM_DRIVER=memory helm list --all-namespaces
+- name: athos
+  version: 1
+  namespace: default
+  info:
+    status: deployed
+  chart:
+    metadata:
+      name: athos-chart
+      version: 1.0.0
+      appversion: 1.1.0
+- name: porthos
+  version: 2
+  namespace: default
+  info:
+    status: deployed
+  chart:
+    metadata:
+      name: prothos-chart
+      version: 0.2.0
+      appversion: 0.2.2
+- name: aramis
+  version: 3
+  namespace: default
+  info:
+    status: deployed
+  chart:
+    metadata:
+      name: aramis-chart
+      version: 0.0.3
+      appversion: 3.0.3
+- name: dartagnan
+  version: 4
+  namespace: gascony
+  info:
+    status: deployed
+  chart:
+    metadata:
+      name: dartagnan-chart
+      version: 0.4.4
+      appversion: 4.4.4
+