Repository: puppetlabs/wash
Pull request #314 (2019-06-04 15:07:34)

Support plugin-specific config and limiting AWS profiles
Support plugin-specific config (#289) and use it to allow configuring which AWS profiles are available (#317).

MikaelSmith: This needs a better way of handling config. It lets you restrict which profiles are loaded so you don't have to enter MFA tokens a bunch or mount profiles don't want to use.
MikaelSmith: What I would like this to look like is
```
aws:
  profiles:
    - eso
```

where each plugin has a top-level key automatically associated with it, and the contents of that key are passed to the plugin `Init()` function.
MikaelSmith: I'll move to review once I've added external plugin support.
ekinanp: May as well mention something about adding plugin config in the PR title

Patch:
diff --git a/cmd/internal/server/core.go b/cmd/internal/server/core.go
index c90d1a3..0152271 100644
--- a/cmd/internal/server/core.go
+++ b/cmd/internal/server/core.go
@@ -13,20 +13,18 @@ import (
 	"github.com/puppetlabs/wash/api"
 	"github.com/puppetlabs/wash/fuse"
 	"github.com/puppetlabs/wash/plugin"
-	"github.com/puppetlabs/wash/plugin/aws"
-	"github.com/puppetlabs/wash/plugin/docker"
-	"github.com/puppetlabs/wash/plugin/kubernetes"
 
 	log "github.com/sirupsen/logrus"
 )
 
 // Opts exposes additional configuration for server operation.
 type Opts struct {
-	CPUProfilePath      string
-	ExternalPlugins     []plugin.ExternalPluginSpec
-	LogFile             string
+	CPUProfilePath  string
+	ExternalPlugins []plugin.ExternalPluginSpec
+	LogFile         string
 	// LogLevel can be "warn", "info", "debug", or "trace".
-	LogLevel            string
+	LogLevel     string
+	PluginConfig map[string]map[string]interface{}
 }
 
 type controlChannels struct {
@@ -42,6 +40,7 @@ type Server struct {
 	logFH      *os.File
 	api        controlChannels
 	fuse       controlChannels
+	plugins    map[string]plugin.Root
 }
 
 var levelMap = map[string]log.Level{
@@ -51,9 +50,9 @@ var levelMap = map[string]log.Level{
 	"trace": log.TraceLevel,
 }
 
-// New creates a new Server.
-func New(mountpoint string, socket string, opts Opts) *Server {
-	return &Server{mountpoint: mountpoint, socket: socket, opts: opts}
+// New creates a new Server. Accepts a list of core plugins to load.
+func New(mountpoint string, socket string, plugins map[string]plugin.Root, opts Opts) *Server {
+	return &Server{mountpoint: mountpoint, socket: socket, plugins: plugins, opts: opts}
 }
 
 // Start starts the server. It returns once the server is ready.
@@ -79,9 +78,9 @@ func (s *Server) Start() error {
 	}
 
 	registry := plugin.NewRegistry()
-	loadInternalPlugins(registry)
+	s.loadInternalPlugins(registry)
 	if len(s.opts.ExternalPlugins) > 0 {
-		loadExternalPlugins(registry, s.opts.ExternalPlugins)
+		s.loadExternalPlugins(registry, s.opts.ExternalPlugins)
 	}
 	if len(registry.Plugins()) == 0 {
 		return fmt.Errorf("No plugins loaded")
@@ -166,27 +165,27 @@ func (s *Server) Stop() {
 	s.shutdown()
 }
 
-func loadPlugin(registry *plugin.Registry, name string, root plugin.Root) {
+func (s *Server) loadPlugin(registry *plugin.Registry, name string, root plugin.Root) {
 	log.Infof("Loading %v", name)
-	if err := registry.RegisterPlugin(root); err != nil {
+	if err := registry.RegisterPlugin(root, s.opts.PluginConfig[name]); err != nil {
 		// %+v is a convention used by some errors to print additional context such as a stack trace
 		log.Warnf("%v failed to load: %+v", name, err)
 	}
 }
 
-func loadInternalPlugins(registry *plugin.Registry) {
+func (s *Server) loadInternalPlugins(registry *plugin.Registry) {
 	log.Debug("Loading internal plugins")
-	loadPlugin(registry, "aws", &aws.Root{})
-	loadPlugin(registry, "docker", &docker.Root{})
-	loadPlugin(registry, "kubernetes", &kubernetes.Root{})
+	for name, root := range s.plugins {
+		s.loadPlugin(registry, name, root)
+	}
 	log.Debug("Finished loading internal plugins")
 }
 
-func loadExternalPlugins(registry *plugin.Registry, externalPlugins []plugin.ExternalPluginSpec) {
+func (s *Server) loadExternalPlugins(registry *plugin.Registry, externalPlugins []plugin.ExternalPluginSpec) {
 	log.Infof("Loading external plugins")
 	for _, p := range externalPlugins {
 		log.Infof("Loading %v", p.Script)
-		if err := registry.RegisterExternalPlugin(p); err != nil {
+		if err := registry.RegisterExternalPlugin(p, s.opts.PluginConfig[p.Name()]); err != nil {
 			// %+v is a convention used by some errors to print additional context such as a stack trace
 			log.Warnf("%v failed to load: %+v", p.Script, err)
 		}
diff --git a/cmd/rootMain.go b/cmd/rootMain.go
index fb6ead9..5b89b1a 100644
--- a/cmd/rootMain.go
+++ b/cmd/rootMain.go
@@ -157,7 +157,7 @@ func rootMain(cmd *cobra.Command, args []string) exitCode {
 		return exitCode{1}
 	}
 	socketpath := filepath.Join(rundir, "api.sock")
-	srv := server.New(mountpath, socketpath, serverOpts)
+	srv := server.New(mountpath, socketpath, internalPlugins, serverOpts)
 	if err := srv.Start(); err != nil {
 		cmdutil.ErrPrintf("Unable to start server: %v\n", err)
 		return exitCode{1}
diff --git a/cmd/server.go b/cmd/server.go
index f9755ba..543a24d 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -13,6 +13,9 @@ import (
 	"github.com/puppetlabs/wash/cmd/internal/server"
 	cmdutil "github.com/puppetlabs/wash/cmd/util"
 	"github.com/puppetlabs/wash/plugin"
+	"github.com/puppetlabs/wash/plugin/aws"
+	"github.com/puppetlabs/wash/plugin/docker"
+	"github.com/puppetlabs/wash/plugin/kubernetes"
 
 	log "github.com/sirupsen/logrus"
 
@@ -22,6 +25,12 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
+var internalPlugins = map[string]plugin.Root{
+	"aws":        &aws.Root{},
+	"docker":     &docker.Root{},
+	"kubernetes": &kubernetes.Root{},
+}
+
 func serverCommand() *cobra.Command {
 	serverCmd := &cobra.Command{
 		Use:   "server <mountpoint>",
@@ -60,7 +69,7 @@ func serverMain(cmd *cobra.Command, args []string) exitCode {
 		cmdutil.ErrPrintf("%v\n", err)
 		return exitCode{1}
 	}
-	srv := server.New(mountpoint, config.Socket, serverOpts)
+	srv := server.New(mountpoint, config.Socket, internalPlugins, serverOpts)
 	if err := srv.Start(); err != nil {
 		log.Warn(err)
 		return exitCode{1}
@@ -109,11 +118,21 @@ func serverOptsFor(cmd *cobra.Command) (server.Opts, error) {
 		}
 	}
 
+	config := make(map[string]map[string]interface{})
+	for name := range internalPlugins {
+		config[name] = viper.GetStringMap(name)
+	}
+	for _, spec := range externalPlugins {
+		name := spec.Name()
+		config[name] = viper.GetStringMap(name)
+	}
+
 	// Return the options
 	return server.Opts{
 		CPUProfilePath:  viper.GetString("cpuprofile"),
 		LogFile:         viper.GetString("logfile"),
 		LogLevel:        viper.GetString("loglevel"),
+		PluginConfig:    config,
 		ExternalPlugins: externalPlugins,
 	}, nil
 }
diff --git a/docs/docs/external_plugins/index.html b/docs/docs/external_plugins/index.html
index 8614608..190aa2a 100644
--- a/docs/docs/external_plugins/index.html
+++ b/docs/docs/external_plugins/index.html
@@ -48,7 +48,7 @@
 <ol>
 <li><p>Write the <a href="#plugin-script">plugin script</a>. This is the script that Wash will shell out to whenever it needs to invoke a method on a specific entry within your plugin.</p></li>
 
-<li><p>Add the plugin to your <code>wash.yaml</code> file under the <code>external-plugins</code> key, and specify the path to the plugin script. The plugin&rsquo;s name will be determined by invoking the script with the <code>init</code> method. An example <code>wash.yaml</code> config is shown below:</p>
+<li><p>Add the plugin to your <code>wash.yaml</code> file under the <code>external-plugins</code> key, and specify the path to the plugin script. <em>The plugin&rsquo;s name is the basename of the script without the extension.</em> An example <code>wash.yaml</code> config adding the <code>sshfs</code> plugin is shown below:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
@@ -56,7 +56,7 @@
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">external-plugins:
-    - script: &#39;/Users/enis.inan/GitHub/wash/website/static/docs/external_plugins/examples/sshfs.sh&#39;</pre></td></tr></table>
+    - script: &#39;/path/to/wash/website/static/docs/external_plugins/examples/sshfs.sh&#39;</pre></td></tr></table>
 </div>
 </div></li>
 
@@ -91,7 +91,7 @@
 
 <h2 id="init">init</h2>
 
-<p>The <code>init</code> method is special. It is invoked as <code>&lt;plugin_script&gt; init</code>, and it is invoked only once, when the external plugin is loaded. When <code>init</code> is invoked, the script must output a JSON object representing the plugin root. Here&rsquo;s an example:</p>
+<p>The <code>init</code> method is special. It is invoked as <code>&lt;plugin_script&gt; init &lt;config&gt;</code>, and it is invoked only once, when the external plugin is loaded. <code>&lt;config&gt;</code> is JSON containing any config supplied to Wash under the plugin&rsquo;s key. Given a Wash config file (<code>wash.yaml</code>)</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
@@ -102,19 +102,29 @@
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
-  &#34;name&#34;: &#34;aws&#34;,
-  &#34;methods&#34;: [
-    &#34;list&#34;
-  ]
-}</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">external-plugins:
+  - script: &#39;/path/to/myplugin.rb&#39;
+myplugin:
+  profiles:
+    - profile_a
+    - profile_b</pre></td></tr></table>
+</div>
+</div>
+<p>the <code>init</code> method for a plugin named <code>myplugin</code> will be invoked with</p>
+<div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
+<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
+</span></pre></td>
+<td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; init &#39;{&#34;profiles&#34;:[&#34;profile_a&#34;,&#34;profile_b&#34;]}&#39;</pre></td></tr></table>
 </div>
 </div>
-<p>This example shows the <em>minimum</em> amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct <em>any</em> external plugin entry. This information consists of the entry&rsquo;s name and its implemented methods.</p>
+<p>When <code>init</code> is invoked, the script must output a JSON object representing the plugin root. The <em>minimum</em> amount of information required for Wash to construct the plugin root is an empty object, <code>{}</code>.</p>
 
 <p>You can include additional (optional) keys in the printed JSON object. These keys are:</p>
 
 <ul>
+<li><code>methods</code>. This is an array specifying the list of methods, enumerated below, that can be called directly on the plugin entry. The plugin root must always include and implement the <code>list</code> method.</li>
 <li><code>cache_ttls</code>. This specifies how many seconds each method&rsquo;s result should be cached (<code>ttl</code> is short for time to live). Currently, Wash caches the result of <code>list</code>, <code>read</code>, and <code>metadata</code>.</li>
 <li><code>attributes</code>. This represents the entry&rsquo;s attributes (see the <a href="/wash/docs#attributes-metadata"><code>Attributes/Metadata</code></a> section). Time attributes are specified in Unix seconds. Octal modes must be prefixed with the <code>0</code> delimiter (e.g. like <code>0777</code>). Hexadecimal modes must be prefixed with the <code>0x</code> delimiter (e.g. like <code>0xabcd</code>).</li>
 <li><code>slash_replacer</code>. This overrides the default slash replacer <code>#</code>.</li>
@@ -139,16 +149,10 @@
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
-</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
-</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
-</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
-  &#34;name&#34;: &#34;some_entry&#34;,
-  &#34;methods&#34;: [
-    &#34;list&#34;
-  ],
+  &#34;methods&#34;: [&#34;list&#34;]
   &#34;cache_ttls&#34;: {
     &#34;list&#34;: 30
   },
@@ -164,13 +168,34 @@
 }</pre></td></tr></table>
 </div>
 </div>
-<p>We see from <code>cache_ttls</code> that the result of <code>some_entry</code>&rsquo;s <code>list</code> method will be cached for 30 seconds. We see from <code>attributes</code> that <code>some_entry</code> has an <code>mtime</code> attribute, and that it also includes the <code>meta</code> attribute. We see from <code>slash_replacer</code> that any <code>/</code>&lsquo;es in the entry&rsquo;s returned name will be replaced by a <code>:</code> instead of a <code>#</code>. Finally, we see from <code>state</code> that <code>some_entry</code> has some state that Wash will pass-back in via the <code>&lt;state&gt;</code> parameter whenever it invokes one of its methods. In this case, only <code>list</code> is implemented, and <code>&lt;state&gt;</code> is a stringified JSON object containing the entry&rsquo;s class (<code>SSHFS::Directory</code>) in whatever language the plugin script was written in.</p>
+<p>We see from <code>cache_ttls</code> that the result of <code>some_entry</code>&rsquo;s <code>list</code> method will be cached for 30 seconds. We see from <code>attributes</code> that <code>some_entry</code> has an <code>mtime</code> attribute, and that it also includes the <code>meta</code> attribute. We see from <code>slash_replacer</code> that any <code>/</code>&lsquo;es in the entry&rsquo;s returned name will be replaced by a <code>:</code> instead of a <code>#</code>. Finally, we see from <code>state</code> that <code>some_entry</code> has some state that Wash will pass-back in via the <code>&lt;state&gt;</code> parameter whenever it invokes one of its methods. In this case, <code>&lt;state&gt;</code> is a stringified JSON object containing the entry&rsquo;s class (<code>SSHFS::Directory</code>) in whatever language the plugin script was written in.</p>
 
 <p><code>init</code> adopts the standard error conventions described in the <a href="#errors">Errors</a> section.</p>
 
 <h2 id="list">list</h2>
 
-<p><code>list</code> is invoked as <code>&lt;plugin_script&gt; list &lt;path&gt; &lt;state&gt;</code>. When <code>list</code> is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the <code>init</code> section (hence, <code>list</code> outputs an array of entries). Below is an example of valid <code>list</code> output:</p>
+<p><code>list</code> is invoked as <code>&lt;plugin_script&gt; list &lt;path&gt; &lt;state&gt;</code>. When <code>list</code> is invoked, the script must output an array of JSON objects. The <em>minimum</em> information required is each entry&rsquo;s name and its implemented methods</p>
+<div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
+<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
+</span></pre></td>
+<td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
+  &#34;name&#34;: &#34;mydirectory&#34;,
+  &#34;methods&#34;: [
+    &#34;list&#34;
+  ]
+}</pre></td></tr></table>
+</div>
+</div>
+<p>Each entry may additionally return any keys described in <a href="#init">init</a>.</p>
+
+<p>Below is an example of valid <code>list</code> output:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
diff --git a/docs/docs/index.html b/docs/docs/index.html
index 70c5f64..78acdff 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -156,13 +156,25 @@ to display running processes on all listed nodes. Errors on paths that don&rsquo
 
 <ul>
 <li>EC2 and S3</li>
-<li>uses <code>AWS_SHARED_CREDENTIALS_FILE</code> environment variable or <code>$HOME/.aws/credentials</code> and <code>AWS_CONFIG_FILE</code> environment variable or <code>$HOME/.aws/config</code> to find profiles and configure the SDK.</li>
 <li>IAM roles are supported when configured as described here. Note that currently region will also need to be specified with the profile.</li>
 <li>if using MFA, Wash will prompt for it on standard input. Credentials are valid for 1 hour. They are cached under <code>wash/aws-credentials</code> in your user cache directory so they can be re-used across server restarts. Wash may have to re-prompt for a new MFA token in response to navigating the Wash environment to authorize a new session.</li>
 <li>supports streaming, and remote command execution via ssh</li>
 <li>supports full metadata for S3 content</li>
 </ul>
 
+<p>The AWS plugin reads the <code>AWS_SHARED_CREDENTIALS_FILE</code> environment variable or <code>$HOME/.aws/credentials</code> and <code>AWS_CONFIG_FILE</code> environment variable or <code>$HOME/.aws/config</code> to find profiles and configure the SDK. The profiles it lists can be limited by adding</p>
+<div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
+<table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
+</span></pre></td>
+<td style="vertical-align:top;padding:0;margin:0;border:0;">
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">aws:
+  profiles: [profile_1, profile_2]</pre></td></tr></table>
+</div>
+</div>
+<p>to Wash&rsquo;s <a href="#config">config file</a>.</p>
+
 <h3 id="docker">Docker</h3>
 
 <ul>
diff --git a/docs/index.html b/docs/index.html
index b59fbd5..53201e0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
 <head>
-	<meta name="generator" content="Hugo 0.55.5" />
+	<meta name="generator" content="Hugo 0.55.6" />
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
 
diff --git a/plugin/aws/root.go b/plugin/aws/root.go
index 1d0d7b0..b63776e 100644
--- a/plugin/aws/root.go
+++ b/plugin/aws/root.go
@@ -20,6 +20,7 @@ import (
 // Root of the AWS plugin
 type Root struct {
 	plugin.EntryBase
+	profs []string
 }
 
 func awsCredentialsFile() (string, error) {
@@ -60,11 +61,26 @@ func exists(path string) error {
 }
 
 // Init for root
-func (r *Root) Init() error {
+func (r *Root) Init(cfg map[string]interface{}) error {
 	r.EntryBase = plugin.NewEntryBase()
 	r.SetName("aws")
 	r.SetTTLOf(plugin.ListOp, 1*time.Minute)
 
+	if profsI, ok := cfg["profiles"]; ok {
+		profs, ok := profsI.([]interface{})
+		if !ok {
+			return fmt.Errorf("aws.profiles config must be an array of strings, not %s", profs)
+		}
+		r.profs = make([]string, len(profs))
+		for i, elem := range profs {
+			prof, ok := elem.(string)
+			if !ok {
+				return fmt.Errorf("aws.profiles config must be an array of strings, not %s", profs)
+			}
+			r.profs[i] = prof
+		}
+	}
+
 	// Force authorizing profiles on startup
 	_, err := r.List(context.Background())
 	return err
@@ -118,12 +134,21 @@ func (r *Root) List(ctx context.Context) ([]plugin.Entry, error) {
 		names[strings.TrimPrefix(section.Name(), "profile ")] = struct{}{}
 	}
 
+	profs := make(map[string]struct{})
+	for _, p := range r.profs {
+		profs[p] = struct{}{}
+	}
+
 	var profiles []plugin.Entry
 	for name := range names {
 		if name == "DEFAULT" {
 			continue
 		}
 
+		if _, ok := profs[name]; len(profs) > 0 && !ok {
+			continue
+		}
+
 		profile, err := newProfile(ctx, name)
 		if err != nil {
 			activity.Record(ctx, err.Error())
diff --git a/plugin/docker/root.go b/plugin/docker/root.go
index 5e9431b..89ec721 100644
--- a/plugin/docker/root.go
+++ b/plugin/docker/root.go
@@ -20,7 +20,7 @@ type Root struct {
 }
 
 // Init for root
-func (r *Root) Init() error {
+func (r *Root) Init(map[string]interface{}) error {
 	dockerCli, err := client.NewClientWithOpts(client.FromEnv)
 	if err != nil {
 		return err
diff --git a/plugin/externalPluginRoot.go b/plugin/externalPluginRoot.go
index 07c7bd8..9702c24 100644
--- a/plugin/externalPluginRoot.go
+++ b/plugin/externalPluginRoot.go
@@ -14,19 +14,27 @@ type externalPluginRoot struct {
 
 // newExternalPluginRoot returns a new external plugin root given
 // the plugin script
-func newExternalPluginRoot(script string) *externalPluginRoot {
-	return &externalPluginRoot{&externalPluginEntry{
-		script: externalPluginScriptImpl{path: script},
+func newExternalPluginRoot(name, script string) *externalPluginRoot {
+	root := &externalPluginRoot{&externalPluginEntry{
+		EntryBase: NewEntryBase(),
+		script:    externalPluginScriptImpl{path: script},
 	}}
+	root.SetName(name)
+	return root
 }
 
 // Init initializes the external plugin root
-func (r *externalPluginRoot) Init() error {
+func (r *externalPluginRoot) Init(cfg map[string]interface{}) error {
+	cfgJSON, err := json.Marshal(cfg)
+	if err != nil {
+		return fmt.Errorf("could not marshal plugin config %v into JSON: %v", cfg, err)
+	}
+
 	// Give external plugins about five-seconds to finish their
 	// initialization
-	ctx, cancelFunc := context.WithTimeout(context.Background(), 5 * time.Second)
+	ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancelFunc()
-	stdout, err := r.script.InvokeAndWait(ctx, "init", nil)
+	stdout, err := r.script.InvokeAndWait(ctx, "init", nil, string(cfgJSON))
 	if err != nil {
 		select {
 		case <-ctx.Done():
@@ -42,18 +50,29 @@ func (r *externalPluginRoot) Init() error {
 			"the plugin root",
 			err,
 			stdout,
-			"{\"name\":\"plugin_name\",\"methods\":[\"list\"]}",
+			"{}",
 		)
 	}
+
+	// Fill in required fields with data we already know.
+	if decodedRoot.Name == "" {
+		decodedRoot.Name = r.Name()
+	} else if decodedRoot.Name != r.Name() {
+		panic(fmt.Sprintf(`plugin root's name must match the basename (without extension) of %s
+it's safe to omit name from the response to 'init'`, r.script.Path()))
+	}
+	if decodedRoot.Methods == nil {
+		decodedRoot.Methods = []string{"list"}
+	}
 	entry, err := decodedRoot.toExternalPluginEntry()
 	if err != nil {
 		return err
 	}
 	if !ListAction().IsSupportedOn(entry) {
-		return fmt.Errorf("plugin roots must implement 'list'")
+		panic(fmt.Sprintf("plugin root for %s must implement 'list'", r.script.Path()))
 	}
 	script := r.script
 	r.externalPluginEntry = entry
 	r.externalPluginEntry.script = script
 	return nil
-}
\ No newline at end of file
+}
diff --git a/plugin/externalPluginScript.go b/plugin/externalPluginScript.go
index 7befed3..dcaf7e5 100644
--- a/plugin/externalPluginScript.go
+++ b/plugin/externalPluginScript.go
@@ -60,7 +60,7 @@ func (s externalPluginScriptImpl) NewInvocation(
 	args ...string,
 ) *internal.Command {
 	if method == "init" {
-		return internal.NewCommand(ctx, s.Path(), "init")
+		return internal.NewCommand(ctx, s.Path(), append([]string{"init"}, args...)...)
 	}
 	if entry == nil {
 		msg := fmt.Sprintf("s.NewInvocation called with method '%v' and entry == nil", method)
@@ -68,7 +68,7 @@ func (s externalPluginScriptImpl) NewInvocation(
 	}
 	return internal.NewCommand(
 		ctx,
-		s.Path(), 
+		s.Path(),
 		append([]string{method, entry.id(), entry.state}, args...)...,
 	)
-}
\ No newline at end of file
+}
diff --git a/plugin/kubernetes/root.go b/plugin/kubernetes/root.go
index 2794e36..a0c52df 100644
--- a/plugin/kubernetes/root.go
+++ b/plugin/kubernetes/root.go
@@ -40,7 +40,7 @@ func createContext(raw clientcmdapi.Config, name string, access clientcmd.Config
 }
 
 // Init for root
-func (r *Root) Init() error {
+func (r *Root) Init(map[string]interface{}) error {
 	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
 	config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
 	raw, err := config.RawConfig()
diff --git a/plugin/registry.go b/plugin/registry.go
index 1c5cf6b..b1b8453 100644
--- a/plugin/registry.go
+++ b/plugin/registry.go
@@ -4,7 +4,9 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"path/filepath"
 	"regexp"
+	"strings"
 )
 
 // Registry represents the plugin registry. It is also Wash's root.
@@ -37,8 +39,8 @@ var pluginNameRegex = regexp.MustCompile("^[0-9a-zA-Z_-]+$")
 
 // RegisterPlugin initializes the given plugin and adds it to the registry if
 // initialization was successful.
-func (r *Registry) RegisterPlugin(root Root) error {
-	if err := root.Init(); err != nil {
+func (r *Registry) RegisterPlugin(root Root, config map[string]interface{}) error {
+	if err := root.Init(config); err != nil {
 		return err
 	}
 
@@ -63,9 +65,15 @@ type ExternalPluginSpec struct {
 	Script string
 }
 
+// Name returns the plugin name, which is the basename of the script with extension removed.
+func (s ExternalPluginSpec) Name() string {
+	basename := filepath.Base(s.Script)
+	return strings.TrimSuffix(basename, filepath.Ext(basename))
+}
+
 // RegisterExternalPlugin initializes an external plugin from its spec and
 // passes it to RegisterPlugin.
-func (r *Registry) RegisterExternalPlugin(spec ExternalPluginSpec) error {
+func (r *Registry) RegisterExternalPlugin(spec ExternalPluginSpec, cfg map[string]interface{}) error {
 	fi, err := os.Stat(spec.Script)
 	if err != nil {
 		return err
@@ -75,8 +83,8 @@ func (r *Registry) RegisterExternalPlugin(spec ExternalPluginSpec) error {
 		return fmt.Errorf("script %v is not executable", spec.Script)
 	}
 
-	root := newExternalPluginRoot(spec.Script)
-	return r.RegisterPlugin(root)
+	root := newExternalPluginRoot(spec.Name(), spec.Script)
+	return r.RegisterPlugin(root, cfg)
 }
 
 // ChildSchemas returns the child schemas of the plugin registry
diff --git a/plugin/types.go b/plugin/types.go
index 5834369..d3bfd6b 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -57,10 +57,11 @@ type Parent interface {
 	List(context.Context) ([]Entry, error)
 }
 
-// Root represents the plugin root
+// Root represents the plugin root. The Init function is passed a config map representing
+// plugin-specific configuration.
 type Root interface {
 	Parent
-	Init() error
+	Init(map[string]interface{}) error
 }
 
 // ExecOptions is a struct we can add new features to that must be serializable to JSON.
diff --git a/website/content/docs/_index.md b/website/content/docs/_index.md
index 7c80340..a4ff795 100644
--- a/website/content/docs/_index.md
+++ b/website/content/docs/_index.md
@@ -101,12 +101,18 @@ NOTE: Do not override `socket` in a config file. Instead, override it via the `W
 ### AWS
 
 - EC2 and S3
-- uses `AWS_SHARED_CREDENTIALS_FILE` environment variable or `$HOME/.aws/credentials` and `AWS_CONFIG_FILE` environment variable or `$HOME/.aws/config` to find profiles and configure the SDK.
 - IAM roles are supported when configured as described here. Note that currently region will also need to be specified with the profile.
 - if using MFA, Wash will prompt for it on standard input. Credentials are valid for 1 hour. They are cached under `wash/aws-credentials` in your user cache directory so they can be re-used across server restarts. Wash may have to re-prompt for a new MFA token in response to navigating the Wash environment to authorize a new session.
 - supports streaming, and remote command execution via ssh
 - supports full metadata for S3 content
 
+The AWS plugin reads the `AWS_SHARED_CREDENTIALS_FILE` environment variable or `$HOME/.aws/credentials` and `AWS_CONFIG_FILE` environment variable or `$HOME/.aws/config` to find profiles and configure the SDK. The profiles it lists can be limited by adding
+```
+aws:
+  profiles: [profile_1, profile_2]
+```
+to Wash's [config file](#config).
+
 ### Docker
 
 - containers and volumes
diff --git a/website/content/docs/external_plugins/_index.md b/website/content/docs/external_plugins/_index.md
index e487208..22cbcf3 100644
--- a/website/content/docs/external_plugins/_index.md
+++ b/website/content/docs/external_plugins/_index.md
@@ -6,12 +6,13 @@ External plugins let Wash talk to other things outside of the built-in plugins.
 
 1. Write the [plugin script](#plugin-script). This is the script that Wash will shell out to whenever it needs to invoke a method on a specific entry within your plugin.
 
-1. Add the plugin to your `wash.yaml` file under the `external-plugins` key, and specify the path to the plugin script. The plugin's name will be determined by invoking the script with the `init` method. An example `wash.yaml` config is shown below:
+1. Add the plugin to your `wash.yaml` file under the `external-plugins` key, and specify the path to the plugin script. *The plugin's name is the basename of the script without the extension.* An example `wash.yaml` config adding the `sshfs` plugin is shown below:
 
     ```
     external-plugins:
-        - script: '/Users/enis.inan/GitHub/wash/website/static/docs/external_plugins/examples/sshfs.sh'
+        - script: '/path/to/wash/website/static/docs/external_plugins/examples/sshfs.sh'
     ```
+
 1. Start the Wash shell to see your plugin in action.
 
 ## Plugin Script
@@ -36,21 +37,28 @@ The remaining sections describe all the possible Wash methods that can be passed
 NOTE: Plugin script invocations run in their own process group (pgrp). Wash will send a SIGTERM signal to the pgrp on a cancelled API/filesystem request. If after five seconds the invocation process has not terminated, then Wash will send a SIGKILL signal.
 
 ## init
-The `init` method is special. It is invoked as `<plugin_script> init`, and it is invoked only once, when the external plugin is loaded. When `init` is invoked, the script must output a JSON object representing the plugin root. Here's an example:
+The `init` method is special. It is invoked as `<plugin_script> init <config>`, and it is invoked only once, when the external plugin is loaded. `<config>` is JSON containing any config supplied to Wash under the plugin's key. Given a Wash config file (`wash.yaml`)
 
 ```
-{
-  "name": "aws",
-  "methods": [
-    "list"
-  ]
-}
+external-plugins:
+  - script: '/path/to/myplugin.rb'
+myplugin:
+  profiles:
+    - profile_a
+    - profile_b
 ```
 
-This example shows the *minimum* amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct *any* external plugin entry. This information consists of the entry's name and its implemented methods.
+the `init` method for a plugin named `myplugin` will be invoked with
+
+```
+<plugin_script> init '{"profiles":["profile_a","profile_b"]}'
+```
+
+When `init` is invoked, the script must output a JSON object representing the plugin root. The *minimum* amount of information required for Wash to construct the plugin root is an empty object, `{}`.
 
 You can include additional (optional) keys in the printed JSON object. These keys are:
 
+* `methods`. This is an array specifying the list of methods, enumerated below, that can be called directly on the plugin entry. The plugin root must always include and implement the `list` method.
 * `cache_ttls`. This specifies how many seconds each method's result should be cached (`ttl` is short for time to live). Currently, Wash caches the result of `list`, `read`, and `metadata`.
 * `attributes`. This represents the entry's attributes (see the [`Attributes/Metadata`](/wash/docs#attributes-metadata) section). Time attributes are specified in Unix seconds. Octal modes must be prefixed with the `0` delimiter (e.g. like `0777`). Hexadecimal modes must be prefixed with the `0x` delimiter (e.g. like `0xabcd`).
 * `slash_replacer`. This overrides the default slash replacer `#`.
@@ -60,10 +68,7 @@ Below is an example JSON object showcasing all possible keys at once.
 
 ```
 {
-  "name": "some_entry",
-  "methods": [
-    "list"
-  ],
+  "methods": ["list"]
   "cache_ttls": {
     "list": 30
   },
@@ -79,12 +84,25 @@ Below is an example JSON object showcasing all possible keys at once.
 }
 ```
 
-We see from `cache_ttls` that the result of `some_entry`'s `list` method will be cached for 30 seconds. We see from `attributes` that `some_entry` has an `mtime` attribute, and that it also includes the `meta` attribute. We see from `slash_replacer` that any `/`'es in the entry's returned name will be replaced by a `:` instead of a `#`. Finally, we see from `state` that `some_entry` has some state that Wash will pass-back in via the `<state>` parameter whenever it invokes one of its methods. In this case, only `list` is implemented, and `<state>` is a stringified JSON object containing the entry's class (`SSHFS::Directory`) in whatever language the plugin script was written in.
+We see from `cache_ttls` that the result of `some_entry`'s `list` method will be cached for 30 seconds. We see from `attributes` that `some_entry` has an `mtime` attribute, and that it also includes the `meta` attribute. We see from `slash_replacer` that any `/`'es in the entry's returned name will be replaced by a `:` instead of a `#`. Finally, we see from `state` that `some_entry` has some state that Wash will pass-back in via the `<state>` parameter whenever it invokes one of its methods. In this case, `<state>` is a stringified JSON object containing the entry's class (`SSHFS::Directory`) in whatever language the plugin script was written in.
 
 `init` adopts the standard error conventions described in the [Errors](#errors) section.
 
 ## list
-`list` is invoked as `<plugin_script> list <path> <state>`. When `list` is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the `init` section (hence, `list` outputs an array of entries). Below is an example of valid `list` output:
+`list` is invoked as `<plugin_script> list <path> <state>`. When `list` is invoked, the script must output an array of JSON objects. The *minimum* information required is each entry's name and its implemented methods
+
+```
+{
+  "name": "mydirectory",
+  "methods": [
+    "list"
+  ]
+}
+```
+
+Each entry may additionally return any keys described in [init](#init).
+
+Below is an example of valid `list` output:
 
 ```
 [


Test patch:
diff --git a/api/helpers_test.go b/api/helpers_test.go
index 7278495..0ae5b36 100644
--- a/api/helpers_test.go
+++ b/api/helpers_test.go
@@ -89,12 +89,12 @@ func (suite *HelpersTestSuite) TestFindEntry() {
 	duplicateFoo.SetName("foo#bar")
 	parent.entries = append(parent.entries, &duplicateFoo)
 	expectedErr := plugin.DuplicateCNameErr{
-		ParentID:                        plugin.ID(parent),
-		FirstChildName:                  foo.Name(),
-		FirstChildSlashReplacer:         '#',
-		SecondChildName:                 duplicateFoo.Name(),
-		SecondChildSlashReplacer:        '#',
-		CName:                           "foo#bar",
+		ParentID:                 plugin.ID(parent),
+		FirstChildName:           foo.Name(),
+		FirstChildSlashReplacer:  '#',
+		SecondChildName:          duplicateFoo.Name(),
+		SecondChildSlashReplacer: '#',
+		CName:                    "foo#bar",
 	}
 	runTestCase(
 		parent,
@@ -107,7 +107,7 @@ type mockRoot struct {
 	mock.Mock
 }
 
-func (m *mockRoot) Init() error {
+func (m *mockRoot) Init(map[string]interface{}) error {
 	return nil
 }
 
@@ -133,7 +133,7 @@ func (suite *HelpersTestSuite) TestGetEntryFromPath() {
 	plug := &mockRoot{EntryBase: plugin.NewEntryBase()}
 	plug.SetName("mine")
 	plug.SetTestID("/mine")
-	suite.NoError(reg.RegisterPlugin(plug))
+	suite.NoError(reg.RegisterPlugin(plug, map[string]interface{}{}))
 	ctx := context.WithValue(context.Background(), pluginRegistryKey, reg)
 
 	mountpoint := "/mountpoint"
diff --git a/plugin/externalPluginRoot_test.go b/plugin/externalPluginRoot_test.go
index 6f3da3c..bd92b9a 100644
--- a/plugin/externalPluginRoot_test.go
+++ b/plugin/externalPluginRoot_test.go
@@ -19,43 +19,39 @@ func (suite *ExternalPluginRootTestSuite) TestInit() {
 		EntryBase: NewEntryBase(),
 		script:    mockScript,
 	}}
+	root.SetName("foo")
 
 	mockInvokeAndWait := func(stdout []byte, err error) {
 		mockScript.OnInvokeAndWait(
 			mock.Anything,
 			"init",
 			nil,
+			"null",
 		).Return(stdout, err).Once()
 	}
 
 	// Test that if InvokeAndWait errors, then Init returns its error
 	mockErr := fmt.Errorf("execution error")
 	mockInvokeAndWait([]byte{}, mockErr)
-	err := root.Init()
+	err := root.Init(nil)
 	suite.EqualError(mockErr, err.Error())
 
 	// Test that Init returns an error if stdout does not have the right
 	// output format
 	mockInvokeAndWait([]byte("bad format"), nil)
-	err = root.Init()
+	err = root.Init(nil)
 	suite.Regexp(regexp.MustCompile("stdout"), err)
 
-	// Test that Init returns an error if the root does not implement
-	// "list"
-	mockInvokeAndWait([]byte("{\"name\":\"root\",\"methods\":[]}"), nil)
-	err = root.Init()
-	suite.Regexp("implement.*list", err)
-
 	// Test that Init properly decodes the root from stdout
-	stdout := "{\"name\":\"foo\",\"methods\":[\"list\"]}"
+	stdout := "{}"
 	mockInvokeAndWait([]byte(stdout), nil)
-	err = root.Init()
+	err = root.Init(nil)
 	if suite.NoError(err) {
 		expectedRoot := &externalPluginRoot{
 			externalPluginEntry: &externalPluginEntry{
-				EntryBase:        NewEntryBase(),
-				methods:          []string{"list"},
-				script:           root.script,
+				EntryBase: NewEntryBase(),
+				methods:   []string{"list"},
+				script:    root.script,
 			},
 		}
 		expectedRoot.SetName("foo")
@@ -64,6 +60,24 @@ func (suite *ExternalPluginRootTestSuite) TestInit() {
 	}
 }
 
+func (suite *ExternalPluginRootTestSuite) TestInitWithConfig() {
+	mockScript := &mockExternalPluginScript{path: "plugin_script"}
+	root := &externalPluginRoot{&externalPluginEntry{
+		EntryBase: NewEntryBase(),
+		script:    mockScript,
+	}}
+	root.SetName("foo")
+
+	mockScript.OnInvokeAndWait(
+		mock.Anything,
+		"init",
+		nil,
+		`{"key":["value"]}`,
+	).Return([]byte("{}"), nil).Once()
+
+	suite.NoError(root.Init(map[string]interface{}{"key": []string{"value"}}))
+}
+
 func TestExternalPluginRoot(t *testing.T) {
 	suite.Run(t, new(ExternalPluginRootTestSuite))
 }
diff --git a/plugin/registry_test.go b/plugin/registry_test.go
index d3f4c24..d9fd81a 100644
--- a/plugin/registry_test.go
+++ b/plugin/registry_test.go
@@ -23,8 +23,8 @@ type mockRoot struct {
 	mock.Mock
 }
 
-func (m *mockRoot) Init() error {
-	args := m.Called()
+func (m *mockRoot) Init(cfg map[string]interface{}) error {
+	args := m.Called(cfg)
 	return args.Error(0)
 }
 
@@ -59,9 +59,22 @@ func (suite *RegistryTestSuite) TestRegisterPlugin() {
 	reg := NewRegistry()
 	m := &mockRoot{EntryBase: NewEntryBase()}
 	m.SetName("mine")
-	m.On("Init").Return(nil)
+	cfg := map[string]interface{}{}
+	m.On("Init", cfg).Return(nil)
 
-	suite.NoError(reg.RegisterPlugin(m))
+	suite.NoError(reg.RegisterPlugin(m, cfg))
+	m.AssertExpectations(suite.T())
+	suite.Contains(reg.Plugins(), "mine")
+}
+
+func (suite *RegistryTestSuite) TestRegisterPluginWithConfig() {
+	reg := NewRegistry()
+	m := &mockRoot{EntryBase: NewEntryBase()}
+	m.SetName("mine")
+	cfg := map[string]interface{}{"key": "value"}
+	m.On("Init", cfg).Return(nil)
+
+	suite.NoError(reg.RegisterPlugin(m, cfg))
 	m.AssertExpectations(suite.T())
 	suite.Contains(reg.Plugins(), "mine")
 }
@@ -70,9 +83,9 @@ func (suite *RegistryTestSuite) TestRegisterPluginInitError() {
 	reg := NewRegistry()
 	m := &mockRoot{EntryBase: NewEntryBase()}
 	m.SetName("mine")
-	m.On("Init").Return(errors.New("failed"))
+	m.On("Init", map[string]interface{}(nil)).Return(errors.New("failed"))
 
-	suite.EqualError(reg.RegisterPlugin(m), "failed")
+	suite.EqualError(reg.RegisterPlugin(m, nil), "failed")
 	m.AssertExpectations(suite.T())
 	suite.NotContains(reg.Plugins(), "mine")
 }
@@ -82,7 +95,7 @@ func (suite *RegistryTestSuite) TestRegisterPluginInvalidPluginName() {
 		reg := NewRegistry()
 		m := &mockRoot{EntryBase: NewEntryBase()}
 		m.SetName("b@dname")
-		_ = reg.RegisterPlugin(m)
+		_ = reg.RegisterPlugin(m, map[string]interface{}{})
 	}
 
 	suite.Panics(
@@ -96,8 +109,8 @@ func (suite *RegistryTestSuite) TestRegisterPluginRegisteredPlugin() {
 		reg := NewRegistry()
 		m1 := &mockRoot{EntryBase: NewEntryBase()}
 		m1.SetName("mine")
-		_ = reg.RegisterPlugin(m1)
-		_ = reg.RegisterPlugin(m1)
+		_ = reg.RegisterPlugin(m1, map[string]interface{}{})
+		_ = reg.RegisterPlugin(m1, map[string]interface{}{})
 	}
 
 	suite.Panics(panicFunc, "r.RegisterPlugin: the mine plugin's already been registered")
@@ -107,32 +120,40 @@ func (suite *RegistryTestSuite) TestRegisterExternalPlugin() {
 	reg := NewRegistry()
 	spec := ExternalPluginSpec{Script: "testdata/external.sh"}
 
-	suite.NoError(reg.RegisterExternalPlugin(spec))
-	suite.Contains(reg.Plugins(), "test")
+	suite.NoError(reg.RegisterExternalPlugin(spec, map[string]interface{}{}))
+	suite.Contains(reg.Plugins(), "external")
+}
+
+func (suite *RegistryTestSuite) TestRegisterExternalPluginWithConfig() {
+	reg := NewRegistry()
+	spec := ExternalPluginSpec{Script: "testdata/external.sh"}
+
+	suite.NoError(reg.RegisterExternalPlugin(spec, map[string]interface{}{"key": "value"}))
+	suite.Contains(reg.Plugins(), "external")
 }
 
 func (suite *RegistryTestSuite) TestRegisterExternalPluginNoExec() {
 	reg := NewRegistry()
 	spec := ExternalPluginSpec{Script: "testdata/noexec"}
 
-	suite.EqualError(reg.RegisterExternalPlugin(spec), "script testdata/noexec is not executable")
-	suite.NotContains(reg.Plugins(), "test")
+	suite.EqualError(reg.RegisterExternalPlugin(spec, map[string]interface{}{}), "script testdata/noexec is not executable")
+	suite.NotContains(reg.Plugins(), "external")
 }
 
 func (suite *RegistryTestSuite) TestRegisterExternalPluginNoExist() {
 	reg := NewRegistry()
 	spec := ExternalPluginSpec{Script: "testdata/noexist"}
 
-	suite.EqualError(reg.RegisterExternalPlugin(spec), "stat testdata/noexist: no such file or directory")
-	suite.NotContains(reg.Plugins(), "test")
+	suite.EqualError(reg.RegisterExternalPlugin(spec, map[string]interface{}{}), "stat testdata/noexist: no such file or directory")
+	suite.NotContains(reg.Plugins(), "external")
 }
 
 func (suite *RegistryTestSuite) TestRegisterExternalPluginNotFile() {
 	reg := NewRegistry()
 	spec := ExternalPluginSpec{Script: "testdata/notfile"}
 
-	suite.EqualError(reg.RegisterExternalPlugin(spec), "script testdata/notfile is not a file")
-	suite.NotContains(reg.Plugins(), "test")
+	suite.EqualError(reg.RegisterExternalPlugin(spec, map[string]interface{}{}), "script testdata/notfile is not a file")
+	suite.NotContains(reg.Plugins(), "external")
 }
 
 func TestRegistry(t *testing.T) {
diff --git a/plugin/testdata/external.sh b/plugin/testdata/external.sh
index effe8fe..3480488 100755
--- a/plugin/testdata/external.sh
+++ b/plugin/testdata/external.sh
@@ -1,7 +1,2 @@
 #!/bin/sh
-echo '{
-  "name": "test",
-  "methods": [
-    "list"
-  ]
-}'
\ No newline at end of file
+echo '{}'
\ No newline at end of file