Skip to content

Commit

Permalink
cli-plugins: add concept of experimental plugin, only enabled in expe…
Browse files Browse the repository at this point in the history
…rimental mode

To test, add $(pwd)/build/plugins-linux-amd64 to "cliPluginsExtraDirs" config and run:
make plugins
make binary
HELLO_EXPERIMENTAL=1 docker helloworld

To show it enabled:
HELLO_EXPERIMENTAL=1 DOCKER_CLI_EXPERIMENTAL=enabled docker helloworld

Signed-off-by: Tibor Vass <tibor@docker.com>
  • Loading branch information
Tibor Vass committed May 21, 2019
1 parent 57aa773 commit c483c63
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 12 deletions.
1 change: 1 addition & 0 deletions cli-plugins/examples/helloworld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,6 @@ func main() {
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: "testing",
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
})
}
16 changes: 10 additions & 6 deletions cli-plugins/manager/candidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
)

type fakeCandidate struct {
path string
exec bool
meta string
path string
exec bool
meta string
allowExperimental bool
}

func (c *fakeCandidate) Path() string {
Expand Down Expand Up @@ -48,7 +49,7 @@ func TestValidateCandidate(t *testing.T) {
},
})

for _, tc := range []struct {
for i, tc := range []struct {
c *fakeCandidate

// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
Expand All @@ -67,15 +68,18 @@ func TestValidateCandidate(t *testing.T) {
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`}, invalid: "requires experimental enabled"},
// This one should work
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`, allowExperimental: true}},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`, allowExperimental: true}},
} {
p, err := newPlugin(tc.c, fakeroot)
p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
if tc.err != "" {
assert.ErrorContains(t, err, tc.err)
} else if tc.invalid != "" {
assert.NilError(t, err)
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})), i)
assert.ErrorContains(t, p.Err, tc.invalid)
} else {
assert.NilError(t, err)
Expand Down
31 changes: 27 additions & 4 deletions cli-plugins/manager/manager.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package manager

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
Expand All @@ -27,10 +28,23 @@ func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
}

type errPluginRequireExperimental string

// Note: errPluginRequireExperimental implements notFound so that the plugin
// is skipped when listing the plugins.
func (e errPluginRequireExperimental) NotFound() {}

func (e errPluginRequireExperimental) Error() string {
return fmt.Sprintf("plugin candidate %q: requires experimental enabled", string(e))
}

type notFound interface{ NotFound() }

// IsNotFound is true if the given error is due to a plugin not being found.
func IsNotFound(err error) bool {
if e, ok := err.(*pluginError); ok {
err = e.Cause()
}
_, ok := err.(notFound)
return ok
}
Expand Down Expand Up @@ -117,12 +131,14 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
continue
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd)
p, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil {
return nil, err
}
p.ShadowedPaths = paths[1:]
plugins = append(plugins, p)
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
plugins = append(plugins, p)
}
}

return plugins, nil
Expand Down Expand Up @@ -159,12 +175,19 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
}

c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd)
plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil {
return nil, err
}
if plugin.Err != nil {
// TODO: why are we not returning plugin.Err?

err := plugin.Err.(*pluginError).Cause()
// if an experimental plugin was invoked directly while experimental mode is off
// provide a more useful error message than "not found".
if err, ok := err.(errPluginRequireExperimental); ok {
return nil, err
}
return nil, errPluginNotFound(name)
}
cmd := exec.Command(plugin.Path, args...)
Expand Down
2 changes: 2 additions & 0 deletions cli-plugins/manager/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ type Metadata struct {
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Experimental specifies whether the plugin is experimental
Experimental bool `json:",omitempty"`
}
9 changes: 7 additions & 2 deletions cli-plugins/manager/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ type Plugin struct {
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
//
// nolint: gocyclo
func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
Expand Down Expand Up @@ -94,7 +96,10 @@ func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}

if p.Experimental && !allowExperimental {
p.Err = &pluginError{errPluginRequireExperimental(p.Name)}
return p, nil
}
if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil
Expand Down

0 comments on commit c483c63

Please sign in to comment.