Skip to content

Commit 10b899b

Browse files
author
Tibor Vass
committed
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>
1 parent 57aa773 commit 10b899b

File tree

5 files changed

+45
-10
lines changed

5 files changed

+45
-10
lines changed

cli-plugins/examples/helloworld/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,6 @@ func main() {
101101
SchemaVersion: "0.1.0",
102102
Vendor: "Docker Inc.",
103103
Version: "testing",
104+
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
104105
})
105106
}

cli-plugins/manager/candidate_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import (
1212
)
1313

1414
type fakeCandidate struct {
15-
path string
16-
exec bool
17-
meta string
15+
path string
16+
exec bool
17+
meta string
18+
allowExperimental bool
1819
}
1920

2021
func (c *fakeCandidate) Path() string {
@@ -67,10 +68,13 @@ func TestValidateCandidate(t *testing.T) {
6768
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
6869
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
6970
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
71+
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`}, invalid: "requires experimental enabled"},
7072
// This one should work
7173
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
74+
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`, allowExperimental: true}},
75+
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`, allowExperimental: true}},
7276
} {
73-
p, err := newPlugin(tc.c, fakeroot)
77+
p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
7478
if tc.err != "" {
7579
assert.ErrorContains(t, err, tc.err)
7680
} else if tc.invalid != "" {

cli-plugins/manager/manager.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package manager
22

33
import (
4+
"fmt"
45
"io/ioutil"
56
"os"
67
"os/exec"
@@ -27,10 +28,23 @@ func (e errPluginNotFound) Error() string {
2728
return "Error: No such CLI plugin: " + string(e)
2829
}
2930

31+
type errPluginRequireExperimental string
32+
33+
// Note: errPluginRequireExperimental implements notFound so that the plugin
34+
// is skipped when listing the plugins.
35+
func (e errPluginRequireExperimental) NotFound() {}
36+
37+
func (e errPluginRequireExperimental) Error() string {
38+
return fmt.Sprintf("plugin candidate %q: requires experimental enabled", string(e))
39+
}
40+
3041
type notFound interface{ NotFound() }
3142

3243
// IsNotFound is true if the given error is due to a plugin not being found.
3344
func IsNotFound(err error) bool {
45+
if e, ok := err.(*pluginError); ok {
46+
err = e.Cause()
47+
}
3448
_, ok := err.(notFound)
3549
return ok
3650
}
@@ -117,12 +131,14 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
117131
continue
118132
}
119133
c := &candidate{paths[0]}
120-
p, err := newPlugin(c, rootcmd)
134+
p, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
121135
if err != nil {
122136
return nil, err
123137
}
124-
p.ShadowedPaths = paths[1:]
125-
plugins = append(plugins, p)
138+
if !IsNotFound(p.Err) {
139+
p.ShadowedPaths = paths[1:]
140+
plugins = append(plugins, p)
141+
}
126142
}
127143

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

161177
c := &candidate{path: path}
162-
plugin, err := newPlugin(c, rootcmd)
178+
plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
163179
if err != nil {
164180
return nil, err
165181
}
166182
if plugin.Err != nil {
167183
// TODO: why are we not returning plugin.Err?
184+
185+
err := plugin.Err.(*pluginError).Cause()
186+
// if an experimental plugin was invoked directly while experimental mode is off
187+
// provide a more useful error message than "not found".
188+
if err, ok := err.(errPluginRequireExperimental); ok {
189+
return nil, err
190+
}
168191
return nil, errPluginNotFound(name)
169192
}
170193
cmd := exec.Command(plugin.Path, args...)

cli-plugins/manager/metadata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ type Metadata struct {
2222
ShortDescription string `json:",omitempty"`
2323
// URL is a pointer to the plugin's homepage.
2424
URL string `json:",omitempty"`
25+
// Experimental specifies whether the plugin is experimental
26+
Experimental bool `json:",omitempty"`
2527
}

cli-plugins/manager/plugin.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ type Plugin struct {
3333
// is set, and is always a `pluginError`, but the `Plugin` is still
3434
// returned with no error. An error is only returned due to a
3535
// non-recoverable error.
36-
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
36+
//
37+
// nolint: gocyclo
38+
func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plugin, error) {
3739
path := c.Path()
3840
if path == "" {
3941
return Plugin{}, errors.New("plugin candidate path cannot be empty")
@@ -94,7 +96,10 @@ func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
9496
p.Err = wrapAsPluginError(err, "invalid metadata")
9597
return p, nil
9698
}
97-
99+
if p.Experimental && !allowExperimental {
100+
p.Err = &pluginError{errPluginRequireExperimental(p.Name)}
101+
return p, nil
102+
}
98103
if p.Metadata.SchemaVersion != "0.1.0" {
99104
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
100105
return p, nil

0 commit comments

Comments
 (0)