Skip to content

Commit 6a596c0

Browse files
authored
Merge pull request #6269 from thaJeztah/28.x_backport_plugin_manager_unexport
[28.x backport] cli-plugins/manager: various fixes and deprecations
2 parents a8fe4aa + 79e0b1d commit 6a596c0

File tree

11 files changed

+105
-48
lines changed

11 files changed

+105
-48
lines changed

cli-plugins/manager/candidate.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ import (
66
"github.com/docker/cli/cli-plugins/metadata"
77
)
88

9-
// Candidate represents a possible plugin candidate, for mocking purposes
10-
type Candidate interface {
11-
Path() string
12-
Metadata() ([]byte, error)
13-
}
14-
159
type candidate struct {
1610
path string
1711
}

cli-plugins/manager/error.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ func (e *pluginError) Error() string {
2323
return e.cause.Error()
2424
}
2525

26-
// Cause satisfies the errors.causer interface for pluginError.
27-
func (e *pluginError) Cause() error {
28-
return e.cause
29-
}
30-
3126
// Unwrap provides compatibility for Go 1.13 error chains.
3227
func (e *pluginError) Unwrap() error {
3328
return e.cause
@@ -41,14 +36,11 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
4136
// wrapAsPluginError wraps an error in a pluginError with an
4237
// additional message.
4338
func wrapAsPluginError(err error, msg string) error {
44-
if err == nil {
45-
return nil
46-
}
4739
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
4840
}
4941

50-
// NewPluginError creates a new pluginError, analogous to
42+
// newPluginError creates a new pluginError, analogous to
5143
// errors.Errorf.
52-
func NewPluginError(msg string, args ...any) error {
44+
func newPluginError(msg string, args ...any) error {
5345
return &pluginError{cause: fmt.Errorf(msg, args...)}
5446
}

cli-plugins/manager/error_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
func TestPluginError(t *testing.T) {
13-
err := NewPluginError("new error")
13+
err := newPluginError("new error")
1414
assert.Check(t, is.Error(err, "new error"))
1515

1616
inner := errors.New("testing")
@@ -21,4 +21,7 @@ func TestPluginError(t *testing.T) {
2121
actual, err := json.Marshal(err)
2222
assert.Check(t, err)
2323
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
24+
25+
err = wrapAsPluginError(nil, "wrapping")
26+
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
2427
}

cli-plugins/manager/manager.go

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"sync"
1111

12+
"github.com/containerd/errdefs"
1213
"github.com/docker/cli/cli-plugins/metadata"
1314
"github.com/docker/cli/cli/config"
1415
"github.com/docker/cli/cli/config/configfile"
@@ -23,12 +24,6 @@ const (
2324
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
2425
// the plugin to re-execute the original CLI.
2526
ReexecEnvvar = metadata.ReexecEnvvar
26-
27-
// ResourceAttributesEnvvar is the name of the envvar that includes additional
28-
// resource attributes for OTEL.
29-
//
30-
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
31-
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
3227
)
3328

3429
// errPluginNotFound is the error returned when a plugin could not be found.
@@ -40,15 +35,11 @@ func (e errPluginNotFound) Error() string {
4035
return "Error: No such CLI plugin: " + string(e)
4136
}
4237

43-
type notFound interface{ NotFound() }
44-
4538
// IsNotFound is true if the given error is due to a plugin not being found.
39+
//
40+
// Deprecated: use [errdefs.IsNotFound].
4641
func IsNotFound(err error) bool {
47-
if e, ok := err.(*pluginError); ok {
48-
err = e.Cause()
49-
}
50-
_, ok := err.(notFound)
51-
return ok
42+
return errdefs.IsNotFound(err)
5243
}
5344

5445
// getPluginDirs returns the platform-specific locations to search for plugins
@@ -127,7 +118,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
127118
if err != nil {
128119
return nil, err
129120
}
130-
if !IsNotFound(p.Err) {
121+
if !errdefs.IsNotFound(p.Err) {
131122
p.ShadowedPaths = paths[1:]
132123
}
133124
return &p, nil
@@ -164,7 +155,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
164155
if err != nil {
165156
return err
166157
}
167-
if !IsNotFound(p.Err) {
158+
if !errdefs.IsNotFound(p.Err) {
168159
p.ShadowedPaths = paths[1:]
169160
mu.Lock()
170161
defer mu.Unlock()
@@ -185,9 +176,9 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
185176
return plugins, nil
186177
}
187178

188-
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
179+
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
189180
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
190-
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
181+
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow.
191182
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
192183
// This uses the full original args, not the args which may
193184
// have been provided by cobra to our caller. This is because

cli-plugins/manager/manager_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"testing"
77

8+
"github.com/containerd/errdefs"
89
"github.com/docker/cli/cli/config"
910
"github.com/docker/cli/cli/config/configfile"
1011
"github.com/docker/cli/internal/test"
@@ -131,7 +132,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
131132

132133
_, err = GetPlugin("ccc", cli, &cobra.Command{})
133134
assert.Error(t, err, "Error: No such CLI plugin: ccc")
134-
assert.Assert(t, IsNotFound(err))
135+
assert.Assert(t, errdefs.IsNotFound(err))
135136
}
136137

137138
func TestListPluginsIsSorted(t *testing.T) {
@@ -166,8 +167,8 @@ func TestErrPluginNotFound(t *testing.T) {
166167
var err error = errPluginNotFound("test")
167168
err.(errPluginNotFound).NotFound()
168169
assert.Error(t, err, "Error: No such CLI plugin: test")
169-
assert.Assert(t, IsNotFound(err))
170-
assert.Assert(t, !IsNotFound(nil))
170+
assert.Assert(t, errdefs.IsNotFound(err))
171+
assert.Assert(t, !errdefs.IsNotFound(nil))
171172
}
172173

173174
func TestGetPluginDirs(t *testing.T) {

cli-plugins/manager/metadata.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,26 @@ import (
66

77
const (
88
// NamePrefix is the prefix required on all plugin binary names
9+
//
10+
// Deprecated: use [metadata.NamePrefix]. This alias will be removed in a future release.
911
NamePrefix = metadata.NamePrefix
1012

1113
// MetadataSubcommandName is the name of the plugin subcommand
1214
// which must be supported by every plugin and returns the
1315
// plugin metadata.
16+
//
17+
// Deprecated: use [metadata.MetadataSubcommandName]. This alias will be removed in a future release.
1418
MetadataSubcommandName = metadata.MetadataSubcommandName
1519

1620
// HookSubcommandName is the name of the plugin subcommand
1721
// which must be implemented by plugins declaring support
1822
// for hooks in their metadata.
23+
//
24+
// Deprecated: use [metadata.HookSubcommandName]. This alias will be removed in a future release.
1925
HookSubcommandName = metadata.HookSubcommandName
2026
)
2127

2228
// Metadata provided by the plugin.
29+
//
30+
// Deprecated: use [metadata.Metadata]. This alias will be removed in a future release.
2331
type Metadata = metadata.Metadata

cli-plugins/manager/plugin.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package manager
22

33
import (
44
"context"
5+
"encoding"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -31,12 +32,34 @@ type Plugin struct {
3132
ShadowedPaths []string `json:",omitempty"`
3233
}
3334

35+
// MarshalJSON implements [json.Marshaler] to handle marshaling the
36+
// [Plugin.Err] field (Go doesn't marshal errors by default).
37+
func (p *Plugin) MarshalJSON() ([]byte, error) {
38+
type Alias Plugin // avoid recursion
39+
40+
cp := *p // shallow copy to avoid mutating original
41+
42+
if cp.Err != nil {
43+
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
44+
cp.Err = &pluginError{cp.Err}
45+
}
46+
}
47+
48+
return json.Marshal((*Alias)(&cp))
49+
}
50+
51+
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
52+
type pluginCandidate interface {
53+
Path() string
54+
Metadata() ([]byte, error)
55+
}
56+
3457
// newPlugin determines if the given candidate is valid and returns a
3558
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
3659
// is set, and is always a `pluginError`, but the `Plugin` is still
3760
// returned with no error. An error is only returned due to a
3861
// non-recoverable error.
39-
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
62+
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
4063
path := c.Path()
4164
if path == "" {
4265
return Plugin{}, errors.New("plugin candidate path cannot be empty")
@@ -63,7 +86,7 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
6386

6487
// Now apply the candidate tests, so these update p.Err.
6588
if !pluginNameRe.MatchString(p.Name) {
66-
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
89+
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
6790
return p, nil
6891
}
6992

@@ -75,11 +98,11 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
7598
continue
7699
}
77100
if cmd.Name() == p.Name {
78-
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
101+
p.Err = newPluginError("plugin %q duplicates builtin command", p.Name)
79102
return p, nil
80103
}
81104
if cmd.HasAlias(p.Name) {
82-
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
105+
p.Err = newPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
83106
return p, nil
84107
}
85108
}
@@ -96,11 +119,11 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
96119
return p, nil
97120
}
98121
if p.Metadata.SchemaVersion != "0.1.0" {
99-
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
122+
p.Err = newPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
100123
return p, nil
101124
}
102125
if p.Metadata.Vendor == "" {
103-
p.Err = NewPluginError("plugin metadata does not define a vendor")
126+
p.Err = newPluginError("plugin metadata does not define a vendor")
104127
return p, nil
105128
}
106129
return p, nil

cli-plugins/manager/plugin_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package manager
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"testing"
7+
8+
"gotest.tools/v3/assert"
9+
is "gotest.tools/v3/assert/cmp"
10+
)
11+
12+
func TestPluginMarshal(t *testing.T) {
13+
const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}`
14+
const jsonNoError = `{"Name":"some-plugin"}`
15+
16+
tests := []struct {
17+
doc string
18+
error error
19+
expected string
20+
}{
21+
{
22+
doc: "no error",
23+
expected: jsonNoError,
24+
},
25+
{
26+
doc: "regular error",
27+
error: errors.New("something went wrong"),
28+
expected: jsonWithError,
29+
},
30+
{
31+
doc: "custom error",
32+
error: newPluginError("something went wrong"),
33+
expected: jsonWithError,
34+
},
35+
}
36+
for _, tc := range tests {
37+
t.Run(tc.doc, func(t *testing.T) {
38+
actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error})
39+
assert.NilError(t, err)
40+
assert.Check(t, is.Equal(string(actual), tc.expected))
41+
})
42+
}
43+
}

cli/command/system/info_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package system
22

33
import (
44
"encoding/base64"
5+
"errors"
56
"net"
67
"testing"
78
"time"
@@ -220,7 +221,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{
220221
{
221222
Name: "badplugin",
222223
Path: "/path/to/docker-badplugin",
223-
Err: pluginmanager.NewPluginError("something wrong"),
224+
Err: errors.New("something wrong"),
224225
},
225226
}
226227

cmd/docker/builder.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strconv"
99
"strings"
1010

11+
"github.com/containerd/errdefs"
1112
pluginmanager "github.com/docker/cli/cli-plugins/manager"
1213
"github.com/docker/cli/cli-plugins/metadata"
1314
"github.com/docker/cli/cli/command"
@@ -36,7 +37,7 @@ const (
3637
)
3738

3839
func newBuilderError(errorMsg string, pluginLoadErr error) error {
39-
if pluginmanager.IsNotFound(pluginLoadErr) {
40+
if errdefs.IsNotFound(pluginLoadErr) {
4041
return errors.New(errorMsg)
4142
}
4243
if pluginLoadErr != nil {

0 commit comments

Comments
 (0)