mirror of https://github.com/docker/cli.git
cli-plugins: add concept of experimental plugin, only enabled in experimental 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>
(cherry picked from commit 6ca8783730
)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
8aebc31806
commit
c5431132d7
|
@ -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") != "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -67,10 +68,13 @@ 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": "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"}`}, 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": ""}`}, 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 CLI"},
|
||||||
// This one should work
|
// 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"}`}},
|
||||||
|
{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 != "" {
|
if tc.err != "" {
|
||||||
assert.ErrorContains(t, err, tc.err)
|
assert.ErrorContains(t, err, tc.err)
|
||||||
} else if tc.invalid != "" {
|
} else if tc.invalid != "" {
|
||||||
|
|
|
@ -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...)
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue