Merge pull request #1898 from tiborvass/plugin_experimental

cli-plugins: add concept of experimental plugin, only enabled in experimental mode
This commit is contained in:
Sebastiaan van Stijn 2019-05-23 21:11:56 +02:00 committed by GitHub
commit ab688a9a79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 38 deletions

View File

@ -101,5 +101,6 @@ func main() {
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
}) })
} }

View File

@ -15,6 +15,7 @@ type fakeCandidate struct {
path string path string
exec bool exec bool
meta string meta string
allowExperimental bool
} }
func (c *fakeCandidate) Path() string { func (c *fakeCandidate) Path() string {
@ -38,6 +39,7 @@ func TestValidateCandidate(t *testing.T) {
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
) )
fakeroot := &cobra.Command{Use: "docker"} fakeroot := &cobra.Command{Use: "docker"}
@ -49,6 +51,7 @@ func TestValidateCandidate(t *testing.T) {
}) })
for _, tc := range []struct { for _, tc := range []struct {
name string
c *fakeCandidate c *fakeCandidate
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
@ -56,21 +59,25 @@ func TestValidateCandidate(t *testing.T) {
invalid string invalid string
}{ }{
/* Each failing one of the tests */ /* Each failing one of the tests */
{c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"}, {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
{c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)}, {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
{c: &fakeCandidate{path: badNamePath}, invalid: "did not match"}, {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
{c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`}, {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
{c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`}, {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
{c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)}, {name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"}, {name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`}, {name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`}, {name: "invalid schemaversion", 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"}, {name: "no vendor", 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"}, {name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
{name: "experimental required", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}, invalid: "requires experimental CLI"},
// This one should work // This one should work
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}}, {name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
{name: "valid + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`, allowExperimental: true}},
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental, allowExperimental: true}},
} { } {
p, err := newPlugin(tc.c, fakeroot) t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
if tc.err != "" { if tc.err != "" {
assert.ErrorContains(t, err, tc.err) assert.ErrorContains(t, err, tc.err)
} else if tc.invalid != "" { } else if tc.invalid != "" {
@ -83,6 +90,7 @@ func TestValidateCandidate(t *testing.T) {
assert.Equal(t, p.SchemaVersion, "0.1.0") assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing") assert.Equal(t, p.Vendor, "e2e-testing")
} }
})
} }
} }

View File

@ -1,6 +1,7 @@
package manager package manager
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
@ -27,10 +28,23 @@ func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e) 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 CLI", string(e))
}
type notFound interface{ NotFound() } type notFound interface{ NotFound() }
// IsNotFound is true if the given error is due to a plugin not being found. // IsNotFound is true if the given error is due to a plugin not being found.
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
if e, ok := err.(*pluginError); ok {
err = e.Cause()
}
_, ok := err.(notFound) _, ok := err.(notFound)
return ok return ok
} }
@ -117,13 +131,15 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
continue continue
} }
c := &candidate{paths[0]} c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd) p, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:] p.ShadowedPaths = paths[1:]
plugins = append(plugins, p) plugins = append(plugins, p)
} }
}
return plugins, nil return plugins, nil
} }
@ -159,12 +175,19 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
} }
c := &candidate{path: path} c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd) plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if plugin.Err != nil { if plugin.Err != nil {
// TODO: why are we not returning plugin.Err? // 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) return nil, errPluginNotFound(name)
} }
cmd := exec.Command(plugin.Path, args...) cmd := exec.Command(plugin.Path, args...)

View File

@ -22,4 +22,7 @@ type Metadata struct {
ShortDescription string `json:",omitempty"` ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage. // URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"` URL string `json:",omitempty"`
// Experimental specifies whether the plugin is experimental.
// Experimental plugins are not displayed on non-experimental CLIs.
Experimental bool `json:",omitempty"`
} }

View File

@ -33,7 +33,9 @@ type Plugin struct {
// is set, and is always a `pluginError`, but the `Plugin` is still // is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a // returned with no error. An error is only returned due to a
// non-recoverable error. // 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() path := c.Path()
if path == "" { if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty") return Plugin{}, errors.New("plugin candidate path cannot be empty")
@ -94,7 +96,10 @@ func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata") p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil return p, nil
} }
if p.Experimental && !allowExperimental {
p.Err = &pluginError{errPluginRequireExperimental(p.Name)}
return p, nil
}
if p.Metadata.SchemaVersion != "0.1.0" { if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion) p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil return p, nil