client: refactor plugin api client functions to define options/results structs

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Austin Vazquez
2025-10-21 19:13:45 -05:00
committed by Sebastiaan van Stijn
parent eddf1a1ad6
commit 909e32b27d
36 changed files with 427 additions and 162 deletions

View File

@@ -9,7 +9,6 @@ import (
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events" "github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/plugin"
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
@@ -145,16 +144,16 @@ type NodeAPIClient interface {
// PluginAPIClient defines API client methods for the plugins // PluginAPIClient defines API client methods for the plugins
type PluginAPIClient interface { type PluginAPIClient interface {
PluginList(ctx context.Context, opts PluginListOptions) (plugin.ListResponse, error) PluginList(ctx context.Context, options PluginListOptions) (PluginListResult, error)
PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) error PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) (PluginRemoveResult, error)
PluginEnable(ctx context.Context, name string, options PluginEnableOptions) error PluginEnable(ctx context.Context, name string, options PluginEnableOptions) (PluginEnableResult, error)
PluginDisable(ctx context.Context, name string, options PluginDisableOptions) error PluginDisable(ctx context.Context, name string, options PluginDisableOptions) (PluginDisableResult, error)
PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (PluginInstallResult, error)
PluginUpgrade(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) PluginUpgrade(ctx context.Context, name string, options PluginUpgradeOptions) (PluginUpgradeResult, error)
PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) PluginPush(ctx context.Context, name string, options PluginPushOptions) (PluginPushResult, error)
PluginSet(ctx context.Context, name string, args []string) error PluginSet(ctx context.Context, name string, options PluginSetOptions) (PluginSetResult, error)
PluginInspect(ctx context.Context, name string, options PluginInspectOptions) (PluginInspectResult, error) PluginInspect(ctx context.Context, name string, options PluginInspectOptions) (PluginInspectResult, error)
PluginCreate(ctx context.Context, createContext io.Reader, options PluginCreateOptions) error PluginCreate(ctx context.Context, createContext io.Reader, options PluginCreateOptions) (PluginCreateResult, error)
} }
// ServiceAPIClient defines API client methods for the services // ServiceAPIClient defines API client methods for the services

View File

@@ -12,8 +12,13 @@ type PluginCreateOptions struct {
RepoName string RepoName string
} }
// PluginCreateResult represents the result of a plugin create operation.
type PluginCreateResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginCreate creates a plugin // PluginCreate creates a plugin
func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions PluginCreateOptions) error { func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions PluginCreateOptions) (PluginCreateResult, error) {
headers := http.Header(make(map[string][]string)) headers := http.Header(make(map[string][]string))
headers.Set("Content-Type", "application/x-tar") headers.Set("Content-Type", "application/x-tar")
@@ -22,5 +27,5 @@ func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, cr
resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginCreateResult{}, err
} }

View File

@@ -10,11 +10,16 @@ type PluginDisableOptions struct {
Force bool Force bool
} }
// PluginDisableResult represents the result of a plugin disable operation.
type PluginDisableResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginDisable disables a plugin // PluginDisable disables a plugin
func (cli *Client) PluginDisable(ctx context.Context, name string, options PluginDisableOptions) error { func (cli *Client) PluginDisable(ctx context.Context, name string, options PluginDisableOptions) (PluginDisableResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginDisableResult{}, err
} }
query := url.Values{} query := url.Values{}
if options.Force { if options.Force {
@@ -22,5 +27,5 @@ func (cli *Client) PluginDisable(ctx context.Context, name string, options Plugi
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginDisableResult{}, err
} }

View File

@@ -16,14 +16,14 @@ func TestPluginDisableError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginDisable(context.Background(), "plugin_name", PluginDisableOptions{}) _, err = client.PluginDisable(context.Background(), "plugin_name", PluginDisableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.PluginDisable(context.Background(), "", PluginDisableOptions{}) _, err = client.PluginDisable(context.Background(), "", PluginDisableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.PluginDisable(context.Background(), " ", PluginDisableOptions{}) _, err = client.PluginDisable(context.Background(), " ", PluginDisableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -42,6 +42,6 @@ func TestPluginDisable(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginDisable(context.Background(), "plugin_name", PluginDisableOptions{}) _, err = client.PluginDisable(context.Background(), "plugin_name", PluginDisableOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -11,16 +11,21 @@ type PluginEnableOptions struct {
Timeout int Timeout int
} }
// PluginEnableResult represents the result of a plugin enable operation.
type PluginEnableResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginEnable enables a plugin // PluginEnable enables a plugin
func (cli *Client) PluginEnable(ctx context.Context, name string, options PluginEnableOptions) error { func (cli *Client) PluginEnable(ctx context.Context, name string, options PluginEnableOptions) (PluginEnableResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginEnableResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("timeout", strconv.Itoa(options.Timeout)) query.Set("timeout", strconv.Itoa(options.Timeout))
resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginEnableResult{}, err
} }

View File

@@ -16,14 +16,14 @@ func TestPluginEnableError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginEnable(context.Background(), "plugin_name", PluginEnableOptions{}) _, err = client.PluginEnable(context.Background(), "plugin_name", PluginEnableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.PluginEnable(context.Background(), "", PluginEnableOptions{}) _, err = client.PluginEnable(context.Background(), "", PluginEnableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.PluginEnable(context.Background(), " ", PluginEnableOptions{}) _, err = client.PluginEnable(context.Background(), " ", PluginEnableOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -42,6 +42,6 @@ func TestPluginEnable(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginEnable(context.Background(), "plugin_name", PluginEnableOptions{}) _, err = client.PluginEnable(context.Background(), "plugin_name", PluginEnableOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -33,17 +33,23 @@ type PluginInstallOptions struct {
Args []string Args []string
} }
// PluginInstallResult holds the result of a plugin install operation.
// It is an io.ReadCloser from which the caller can read installation progress or result.
type PluginInstallResult struct {
io.ReadCloser
}
// PluginInstall installs a plugin // PluginInstall installs a plugin
func (cli *Client) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (_ io.ReadCloser, retErr error) { func (cli *Client) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (_ PluginInstallResult, retErr error) {
query := url.Values{} query := url.Values{}
if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil {
return nil, fmt.Errorf("invalid remote reference: %w", err) return PluginInstallResult{}, fmt.Errorf("invalid remote reference: %w", err)
} }
query.Set("remote", options.RemoteRef) query.Set("remote", options.RemoteRef)
privileges, err := cli.checkPluginPermissions(ctx, query, options) privileges, err := cli.checkPluginPermissions(ctx, query, &options)
if err != nil { if err != nil {
return nil, err return PluginInstallResult{}, err
} }
// set name for plugin pull, if empty should default to remote reference // set name for plugin pull, if empty should default to remote reference
@@ -51,7 +57,7 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
if err != nil { if err != nil {
return nil, err return PluginInstallResult{}, err
} }
name = resp.Header.Get("Docker-Plugin-Name") name = resp.Header.Get("Docker-Plugin-Name")
@@ -70,7 +76,7 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
} }
}() }()
if len(options.Args) > 0 { if len(options.Args) > 0 {
if err := cli.PluginSet(ctx, name, options.Args); err != nil { if _, err := cli.PluginSet(ctx, name, PluginSetOptions{Args: options.Args}); err != nil {
_ = pw.CloseWithError(err) _ = pw.CloseWithError(err)
return return
} }
@@ -81,10 +87,10 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
return return
} }
enableErr := cli.PluginEnable(ctx, name, PluginEnableOptions{Timeout: 0}) _, enableErr := cli.PluginEnable(ctx, name, PluginEnableOptions{Timeout: 0})
_ = pw.CloseWithError(enableErr) _ = pw.CloseWithError(enableErr)
}() }()
return pr, nil return PluginInstallResult{pr}, nil
} }
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) { func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
@@ -99,17 +105,17 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
}) })
} }
func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options PluginInstallOptions) (plugin.Privileges, error) { func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options pluginOptions) (plugin.Privileges, error) {
resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) resp, err := cli.tryPluginPrivileges(ctx, query, options.getRegistryAuth())
if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { if cerrdefs.IsUnauthorized(err) && options.getPrivilegeFunc() != nil {
// TODO: do inspect before to check existing name before checking privileges // TODO: do inspect before to check existing name before checking privileges
newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) newAuthHeader, privilegeErr := options.getPrivilegeFunc()(ctx)
if privilegeErr != nil { if privilegeErr != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return nil, privilegeErr return nil, privilegeErr
} }
options.RegistryAuth = newAuthHeader options.setRegistryAuth(newAuthHeader)
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) resp, err = cli.tryPluginPrivileges(ctx, query, options.getRegistryAuth())
} }
if err != nil { if err != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
@@ -123,14 +129,47 @@ func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values,
} }
ensureReaderClosed(resp) ensureReaderClosed(resp)
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { if !options.getAcceptAllPermissions() && options.getAcceptPermissionsFunc() != nil && len(privileges) > 0 {
accept, err := options.AcceptPermissionsFunc(ctx, privileges) accept, err := options.getAcceptPermissionsFunc()(ctx, privileges)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !accept { if !accept {
return nil, errors.New("permission denied while installing plugin " + options.RemoteRef) return nil, errors.New("permission denied while installing plugin " + options.getRemoteRef())
} }
} }
return privileges, nil return privileges, nil
} }
type pluginOptions interface {
getRegistryAuth() string
setRegistryAuth(string)
getPrivilegeFunc() func(context.Context) (string, error)
getAcceptAllPermissions() bool
getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error)
getRemoteRef() string
}
func (o *PluginInstallOptions) getRegistryAuth() string {
return o.RegistryAuth
}
func (o *PluginInstallOptions) setRegistryAuth(auth string) {
o.RegistryAuth = auth
}
func (o *PluginInstallOptions) getPrivilegeFunc() func(context.Context) (string, error) {
return o.PrivilegeFunc
}
func (o *PluginInstallOptions) getAcceptAllPermissions() bool {
return o.AcceptAllPermissions
}
func (o *PluginInstallOptions) getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error) {
return o.AcceptPermissionsFunc
}
func (o *PluginInstallOptions) getRemoteRef() string {
return o.RemoteRef
}

View File

@@ -13,18 +13,23 @@ type PluginListOptions struct {
Filters Filters Filters Filters
} }
// PluginListResult represents the result of a plugin list operation.
type PluginListResult struct {
Items []*plugin.Plugin
}
// PluginList returns the installed plugins // PluginList returns the installed plugins
func (cli *Client) PluginList(ctx context.Context, opts PluginListOptions) (plugin.ListResponse, error) { func (cli *Client) PluginList(ctx context.Context, options PluginListOptions) (PluginListResult, error) {
var plugins plugin.ListResponse
query := url.Values{} query := url.Values{}
opts.Filters.updateURLValues(query) options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/plugins", query, nil) resp, err := cli.get(ctx, "/plugins", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return plugins, err return PluginListResult{}, err
} }
var plugins plugin.ListResponse
err = json.NewDecoder(resp.Body).Decode(&plugins) err = json.NewDecoder(resp.Body).Decode(&plugins)
return plugins, err return PluginListResult{Items: plugins}, err
} }

View File

@@ -85,10 +85,10 @@ func TestPluginList(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
plugins, err := client.PluginList(context.Background(), PluginListOptions{ list, err := client.PluginList(context.Background(), PluginListOptions{
Filters: listCase.filters, Filters: listCase.filters,
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Len(plugins, 2)) assert.Check(t, is.Len(list.Items, 2))
} }
} }

View File

@@ -8,17 +8,27 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
// PluginPushOptions holds parameters to push a plugin.
type PluginPushOptions struct {
RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry
}
// PluginPushResult is the result of a plugin push operation
type PluginPushResult struct {
io.ReadCloser
}
// PluginPush pushes a plugin to a registry // PluginPush pushes a plugin to a registry
func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) { func (cli *Client) PluginPush(ctx context.Context, name string, options PluginPushOptions) (PluginPushResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return nil, err return PluginPushResult{}, err
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{ resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {options.RegistryAuth},
}) })
if err != nil { if err != nil {
return nil, err return PluginPushResult{}, err
} }
return resp.Body, nil return PluginPushResult{resp.Body}, nil
} }

View File

@@ -18,14 +18,14 @@ func TestPluginPushError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.PluginPush(context.Background(), "plugin_name", "") _, err = client.PluginPush(context.Background(), "plugin_name", PluginPushOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.PluginPush(context.Background(), "", "") _, err = client.PluginPush(context.Background(), "", PluginPushOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.PluginPush(context.Background(), " ", "") _, err = client.PluginPush(context.Background(), " ", PluginPushOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -48,6 +48,6 @@ func TestPluginPush(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.PluginPush(context.Background(), "plugin_name", "authtoken") _, err = client.PluginPush(context.Background(), "plugin_name", PluginPushOptions{RegistryAuth: "authtoken"})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -10,11 +10,16 @@ type PluginRemoveOptions struct {
Force bool Force bool
} }
// PluginRemoveResult represents the result of a plugin removal.
type PluginRemoveResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginRemove removes a plugin // PluginRemove removes a plugin
func (cli *Client) PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) error { func (cli *Client) PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) (PluginRemoveResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginRemoveResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -24,5 +29,5 @@ func (cli *Client) PluginRemove(ctx context.Context, name string, options Plugin
resp, err := cli.delete(ctx, "/plugins/"+name, query, nil) resp, err := cli.delete(ctx, "/plugins/"+name, query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginRemoveResult{}, err
} }

View File

@@ -16,14 +16,14 @@ func TestPluginRemoveError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginRemove(context.Background(), "plugin_name", PluginRemoveOptions{}) _, err = client.PluginRemove(context.Background(), "plugin_name", PluginRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.PluginRemove(context.Background(), "", PluginRemoveOptions{}) _, err = client.PluginRemove(context.Background(), "", PluginRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.PluginRemove(context.Background(), " ", PluginRemoveOptions{}) _, err = client.PluginRemove(context.Background(), " ", PluginRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -42,6 +42,6 @@ func TestPluginRemove(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginRemove(context.Background(), "plugin_name", PluginRemoveOptions{}) _, err = client.PluginRemove(context.Background(), "plugin_name", PluginRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -4,14 +4,24 @@ import (
"context" "context"
) )
// PluginSetOptions defines options for modifying a plugin's settings.
type PluginSetOptions struct {
Args []string
}
// PluginSetResult represents the result of a plugin set operation.
type PluginSetResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginSet modifies settings for an existing plugin // PluginSet modifies settings for an existing plugin
func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error { func (cli *Client) PluginSet(ctx context.Context, name string, options PluginSetOptions) (PluginSetResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginSetResult{}, err
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, options.Args, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginSetResult{}, err
} }

View File

@@ -16,14 +16,14 @@ func TestPluginSetError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginSet(context.Background(), "plugin_name", []string{}) _, err = client.PluginSet(context.Background(), "plugin_name", PluginSetOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.PluginSet(context.Background(), "", []string{}) _, err = client.PluginSet(context.Background(), "", PluginSetOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.PluginSet(context.Background(), " ", []string{}) _, err = client.PluginSet(context.Background(), " ", PluginSetOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -42,6 +42,6 @@ func TestPluginSet(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
err = client.PluginSet(context.Background(), "plugin_name", []string{"arg1"}) _, err = client.PluginSet(context.Background(), "plugin_name", PluginSetOptions{Args: []string{"arg1"}})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -12,8 +12,29 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
// PluginUpgradeOptions holds parameters to upgrade a plugin.
type PluginUpgradeOptions struct {
Disabled bool
AcceptAllPermissions bool
RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry
RemoteRef string // RemoteRef is the plugin name on the registry
// PrivilegeFunc is a function that clients can supply to retry operations
// after getting an authorization error. This function returns the registry
// authentication header value in base64 encoded format, or an error if the
// privilege request fails.
//
// For details, refer to [github.com/moby/moby/api/types/registry.RequestAuthConfig].
PrivilegeFunc func(context.Context) (string, error)
AcceptPermissionsFunc func(context.Context, plugin.Privileges) (bool, error)
Args []string
}
// PluginUpgradeResult holds the result of a plugin upgrade operation.
type PluginUpgradeResult io.ReadCloser
// PluginUpgrade upgrades a plugin // PluginUpgrade upgrades a plugin
func (cli *Client) PluginUpgrade(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) { func (cli *Client) PluginUpgrade(ctx context.Context, name string, options PluginUpgradeOptions) (PluginUpgradeResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -25,7 +46,7 @@ func (cli *Client) PluginUpgrade(ctx context.Context, name string, options Plugi
} }
query.Set("remote", options.RemoteRef) query.Set("remote", options.RemoteRef)
privileges, err := cli.checkPluginPermissions(ctx, query, options) privileges, err := cli.checkPluginPermissions(ctx, query, &options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -42,3 +63,27 @@ func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privi
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })
} }
func (o *PluginUpgradeOptions) getRegistryAuth() string {
return o.RegistryAuth
}
func (o *PluginUpgradeOptions) setRegistryAuth(auth string) {
o.RegistryAuth = auth
}
func (o *PluginUpgradeOptions) getPrivilegeFunc() func(context.Context) (string, error) {
return o.PrivilegeFunc
}
func (o *PluginUpgradeOptions) getAcceptAllPermissions() bool {
return o.AcceptAllPermissions
}
func (o *PluginUpgradeOptions) getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error) {
return o.AcceptPermissionsFunc
}
func (o *PluginUpgradeOptions) getRemoteRef() string {
return o.RemoteRef
}

View File

@@ -71,7 +71,7 @@ func TestAuthZPluginV2Disable(t *testing.T) {
assert.ErrorContains(t, err, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) assert.ErrorContains(t, err, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))
// disable the plugin // disable the plugin
err = c.PluginDisable(ctx, authzPluginNameWithTag, client.PluginDisableOptions{}) _, err = c.PluginDisable(ctx, authzPluginNameWithTag, client.PluginDisableOptions{})
assert.NilError(t, err) assert.NilError(t, err)
// now test to see if the docker api works. // now test to see if the docker api works.

View File

@@ -135,14 +135,14 @@ func TestPluginInstall(t *testing.T) {
err := plugin.Create(ctx, apiclient, repo) err := plugin.Create(ctx, apiclient, repo)
assert.NilError(t, err) assert.NilError(t, err)
rdr, err := apiclient.PluginPush(ctx, repo, "") pushResult, err := apiclient.PluginPush(ctx, repo, client.PluginPushOptions{})
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer pushResult.Close()
buf := &strings.Builder{} buf := &strings.Builder{}
assert.NilError(t, err) assert.NilError(t, err)
var digest string var digest string
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, func(j jsonmessage.JSONMessage) { assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(pushResult, buf, 0, false, func(j jsonmessage.JSONMessage) {
if j.Aux != nil { if j.Aux != nil {
var r types.PushResult var r types.PushResult
assert.NilError(t, json.Unmarshal(*j.Aux, &r)) assert.NilError(t, json.Unmarshal(*j.Aux, &r))
@@ -150,17 +150,17 @@ func TestPluginInstall(t *testing.T) {
} }
}), buf) }), buf)
err = apiclient.PluginRemove(ctx, repo, client.PluginRemoveOptions{Force: true}) _, err = apiclient.PluginRemove(ctx, repo, client.PluginRemoveOptions{Force: true})
assert.NilError(t, err) assert.NilError(t, err)
rdr, err = apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{ installResult, err := apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{
Disabled: true, Disabled: true,
RemoteRef: repo + "@" + digest, RemoteRef: repo + "@" + digest,
}) })
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer installResult.Close()
_, err = io.Copy(io.Discard, rdr) _, err = io.Copy(io.Discard, installResult)
assert.NilError(t, err) assert.NilError(t, err)
_, err = apiclient.PluginInspect(ctx, repo, client.PluginInspectOptions{}) _, err = apiclient.PluginInspect(ctx, repo, client.PluginInspectOptions{})
@@ -268,9 +268,12 @@ func TestPluginsWithRuntimes(t *testing.T) {
apiclient := d.NewClientT(t) apiclient := d.NewClientT(t)
assert.NilError(t, plugin.Create(ctx, apiclient, "test:latest")) assert.NilError(t, plugin.Create(ctx, apiclient, "test:latest"))
defer apiclient.PluginRemove(ctx, "test:latest", client.PluginRemoveOptions{Force: true}) defer func() {
_, _ = apiclient.PluginRemove(ctx, "test:latest", client.PluginRemoveOptions{Force: true})
}()
assert.NilError(t, apiclient.PluginEnable(ctx, "test:latest", client.PluginEnableOptions{Timeout: 30})) _, err = apiclient.PluginEnable(ctx, "test:latest", client.PluginEnableOptions{Timeout: 30})
assert.NilError(t, err)
p := filepath.Join(dir, "myrt") p := filepath.Join(dir, "myrt")
script := fmt.Sprintf(`#!/bin/sh script := fmt.Sprintf(`#!/bin/sh
@@ -331,12 +334,12 @@ func TestPluginBackCompatMediaTypes(t *testing.T) {
assert.NilError(t, plugin.Create(ctx, apiclient, repo)) assert.NilError(t, plugin.Create(ctx, apiclient, repo))
rdr, err := apiclient.PluginPush(ctx, repo, "") res, err := apiclient.PluginPush(ctx, repo, client.PluginPushOptions{})
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer res.Close()
buf := &strings.Builder{} buf := &strings.Builder{}
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, nil), buf) assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(res, buf, 0, false, nil), buf)
// Use custom header here because older versions of the registry do not // Use custom header here because older versions of the registry do not
// parse the accept header correctly and does not like the accept header // parse the accept header correctly and does not like the accept header
@@ -356,7 +359,7 @@ func TestPluginBackCompatMediaTypes(t *testing.T) {
fetcher, err := resolver.Fetcher(ctx, n) fetcher, err := resolver.Fetcher(ctx, n)
assert.NilError(t, err) assert.NilError(t, err)
rdr, err = fetcher.Fetch(ctx, desc) rdr, err := fetcher.Fetch(ctx, desc)
assert.NilError(t, err) assert.NilError(t, err)
defer rdr.Close() defer rdr.Close()

View File

@@ -31,9 +31,12 @@ func TestContinueAfterPluginCrash(t *testing.T) {
ctxT, cancel := context.WithTimeout(ctx, 60*time.Second) ctxT, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
assert.Assert(t, apiclient.PluginEnable(ctxT, "test", client.PluginEnableOptions{Timeout: 30})) _, err := apiclient.PluginEnable(ctxT, "test", client.PluginEnableOptions{Timeout: 30})
assert.NilError(t, err)
cancel() cancel()
defer apiclient.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true}) defer func() {
_, _ = apiclient.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true})
}()
ctxT, cancel = context.WithTimeout(ctx, 60*time.Second) ctxT, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()

View File

@@ -34,7 +34,7 @@ func TestReadPluginNoRead(t *testing.T) {
assert.Assert(t, err) assert.Assert(t, err)
createPlugin(ctx, t, apiclient, "test", "discard", asLogDriver) createPlugin(ctx, t, apiclient, "test", "discard", asLogDriver)
err = apiclient.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30}) _, err = apiclient.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30})
assert.Check(t, err) assert.Check(t, err)
d.Stop(t) d.Stop(t)

View File

@@ -29,9 +29,11 @@ func TestDaemonStartWithLogOpt(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
createPlugin(ctx, t, c, "test", "dummy", asLogDriver) createPlugin(ctx, t, c, "test", "dummy", asLogDriver)
err := c.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30}) _, err := c.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30})
assert.Check(t, err) assert.Check(t, err)
defer c.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true}) defer func() {
_, _ = c.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true})
}()
d.Stop(t) d.Stop(t)
d.Start(t, "--iptables=false", "--ip6tables=false", "--log-driver=test", "--log-opt=foo=bar") d.Start(t, "--iptables=false", "--ip6tables=false", "--log-driver=test", "--log-opt=foo=bar")

View File

@@ -47,10 +47,10 @@ func TestPluginWithDevMounts(t *testing.T) {
c.IpcHost = true c.IpcHost = true
}) })
err = c.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30}) _, err = c.PluginEnable(ctx, "test", client.PluginEnableOptions{Timeout: 30})
assert.NilError(t, err) assert.NilError(t, err)
defer func() { defer func() {
err := c.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true}) _, err := c.PluginRemove(ctx, "test", client.PluginRemoveOptions{Force: true})
assert.Check(t, err) assert.Check(t, err)
}() }()

View File

@@ -35,19 +35,19 @@ func TestServicePlugin(t *testing.T) {
apiclient := d.NewClientT(t) apiclient := d.NewClientT(t)
err := plugin.Create(ctx, apiclient, repo) err := plugin.Create(ctx, apiclient, repo)
assert.NilError(t, err) assert.NilError(t, err)
r, err := apiclient.PluginPush(ctx, repo, "") r, err := apiclient.PluginPush(ctx, repo, client.PluginPushOptions{})
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.Copy(io.Discard, r) _, err = io.Copy(io.Discard, r)
assert.NilError(t, err) assert.NilError(t, err)
err = apiclient.PluginRemove(ctx, repo, client.PluginRemoveOptions{}) _, err = apiclient.PluginRemove(ctx, repo, client.PluginRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
err = plugin.Create(ctx, apiclient, repo2) err = plugin.Create(ctx, apiclient, repo2)
assert.NilError(t, err) assert.NilError(t, err)
r, err = apiclient.PluginPush(ctx, repo2, "") r, err = apiclient.PluginPush(ctx, repo2, client.PluginPushOptions{})
assert.NilError(t, err) assert.NilError(t, err)
_, err = io.Copy(io.Discard, r) _, err = io.Copy(io.Discard, r)
assert.NilError(t, err) assert.NilError(t, err)
err = apiclient.PluginRemove(ctx, repo2, client.PluginRemoveOptions{}) _, err = apiclient.PluginRemove(ctx, repo2, client.PluginRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
d.Stop(t) d.Stop(t)

View File

@@ -164,18 +164,18 @@ func deleteAllNetworks(ctx context.Context, t testing.TB, c client.NetworkAPICli
func deleteAllPlugins(ctx context.Context, t testing.TB, c client.PluginAPIClient, protectedPlugins map[string]struct{}) { func deleteAllPlugins(ctx context.Context, t testing.TB, c client.PluginAPIClient, protectedPlugins map[string]struct{}) {
t.Helper() t.Helper()
plugins, err := c.PluginList(ctx, client.PluginListOptions{}) res, err := c.PluginList(ctx, client.PluginListOptions{})
// Docker EE does not allow cluster-wide plugin management. // Docker EE does not allow cluster-wide plugin management.
if cerrdefs.IsNotImplemented(err) { if cerrdefs.IsNotImplemented(err) {
return return
} }
assert.Check(t, err, "failed to list plugins") assert.Check(t, err, "failed to list plugins")
for _, p := range plugins { for _, p := range res.Items {
if _, ok := protectedPlugins[p.Name]; ok { if _, ok := protectedPlugins[p.Name]; ok {
continue continue
} }
err := c.PluginRemove(ctx, p.Name, client.PluginRemoveOptions{Force: true}) _, err := c.PluginRemove(ctx, p.Name, client.PluginRemoveOptions{Force: true})
assert.Check(t, err, "failed to remove plugin %s", p.ID) assert.Check(t, err, "failed to remove plugin %s", p.ID)
} }
} }

View File

@@ -194,7 +194,7 @@ func ProtectPlugins(ctx context.Context, t testing.TB, testEnv *Execution) {
func getExistingPlugins(ctx context.Context, t testing.TB, testEnv *Execution) []string { func getExistingPlugins(ctx context.Context, t testing.TB, testEnv *Execution) []string {
t.Helper() t.Helper()
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
pluginList, err := apiClient.PluginList(ctx, client.PluginListOptions{}) res, err := apiClient.PluginList(ctx, client.PluginListOptions{})
// Docker EE does not allow cluster-wide plugin management. // Docker EE does not allow cluster-wide plugin management.
if cerrdefs.IsNotImplemented(err) { if cerrdefs.IsNotImplemented(err) {
return []string{} return []string{}
@@ -202,7 +202,7 @@ func getExistingPlugins(ctx context.Context, t testing.TB, testEnv *Execution) [
assert.NilError(t, err, "failed to list plugins") assert.NilError(t, err, "failed to list plugins")
var plugins []string var plugins []string
for _, plugin := range pluginList { for _, plugin := range res.Items {
plugins = append(plugins, plugin.Name) plugins = append(plugins, plugin.Name)
} }
return plugins return plugins

View File

@@ -51,7 +51,7 @@ func WithBinary(bin string) CreateOpt {
// CreateClient is the interface used for `BuildPlugin` to interact with the // CreateClient is the interface used for `BuildPlugin` to interact with the
// daemon. // daemon.
type CreateClient interface { type CreateClient interface {
PluginCreate(context.Context, io.Reader, client.PluginCreateOptions) error PluginCreate(context.Context, io.Reader, client.PluginCreateOptions) (client.PluginCreateResult, error)
} }
// Create creates a new plugin with the specified name // Create creates a new plugin with the specified name
@@ -71,7 +71,8 @@ func Create(ctx context.Context, c CreateClient, name string, opts ...CreateOpt)
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
return c.PluginCreate(ctx, tar, client.PluginCreateOptions{RepoName: name}) _, err = c.PluginCreate(ctx, tar, client.PluginCreateOptions{RepoName: name})
return err
} }
// CreateInRegistry makes a plugin (locally) and pushes it to a registry. // CreateInRegistry makes a plugin (locally) and pushes it to a registry.

View File

@@ -9,7 +9,6 @@ import (
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events" "github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/plugin"
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
@@ -145,16 +144,16 @@ type NodeAPIClient interface {
// PluginAPIClient defines API client methods for the plugins // PluginAPIClient defines API client methods for the plugins
type PluginAPIClient interface { type PluginAPIClient interface {
PluginList(ctx context.Context, opts PluginListOptions) (plugin.ListResponse, error) PluginList(ctx context.Context, options PluginListOptions) (PluginListResult, error)
PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) error PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) (PluginRemoveResult, error)
PluginEnable(ctx context.Context, name string, options PluginEnableOptions) error PluginEnable(ctx context.Context, name string, options PluginEnableOptions) (PluginEnableResult, error)
PluginDisable(ctx context.Context, name string, options PluginDisableOptions) error PluginDisable(ctx context.Context, name string, options PluginDisableOptions) (PluginDisableResult, error)
PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (PluginInstallResult, error)
PluginUpgrade(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) PluginUpgrade(ctx context.Context, name string, options PluginUpgradeOptions) (PluginUpgradeResult, error)
PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) PluginPush(ctx context.Context, name string, options PluginPushOptions) (PluginPushResult, error)
PluginSet(ctx context.Context, name string, args []string) error PluginSet(ctx context.Context, name string, options PluginSetOptions) (PluginSetResult, error)
PluginInspect(ctx context.Context, name string, options PluginInspectOptions) (PluginInspectResult, error) PluginInspect(ctx context.Context, name string, options PluginInspectOptions) (PluginInspectResult, error)
PluginCreate(ctx context.Context, createContext io.Reader, options PluginCreateOptions) error PluginCreate(ctx context.Context, createContext io.Reader, options PluginCreateOptions) (PluginCreateResult, error)
} }
// ServiceAPIClient defines API client methods for the services // ServiceAPIClient defines API client methods for the services

View File

@@ -12,8 +12,13 @@ type PluginCreateOptions struct {
RepoName string RepoName string
} }
// PluginCreateResult represents the result of a plugin create operation.
type PluginCreateResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginCreate creates a plugin // PluginCreate creates a plugin
func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions PluginCreateOptions) error { func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions PluginCreateOptions) (PluginCreateResult, error) {
headers := http.Header(make(map[string][]string)) headers := http.Header(make(map[string][]string))
headers.Set("Content-Type", "application/x-tar") headers.Set("Content-Type", "application/x-tar")
@@ -22,5 +27,5 @@ func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, cr
resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginCreateResult{}, err
} }

View File

@@ -10,11 +10,16 @@ type PluginDisableOptions struct {
Force bool Force bool
} }
// PluginDisableResult represents the result of a plugin disable operation.
type PluginDisableResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginDisable disables a plugin // PluginDisable disables a plugin
func (cli *Client) PluginDisable(ctx context.Context, name string, options PluginDisableOptions) error { func (cli *Client) PluginDisable(ctx context.Context, name string, options PluginDisableOptions) (PluginDisableResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginDisableResult{}, err
} }
query := url.Values{} query := url.Values{}
if options.Force { if options.Force {
@@ -22,5 +27,5 @@ func (cli *Client) PluginDisable(ctx context.Context, name string, options Plugi
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginDisableResult{}, err
} }

View File

@@ -11,16 +11,21 @@ type PluginEnableOptions struct {
Timeout int Timeout int
} }
// PluginEnableResult represents the result of a plugin enable operation.
type PluginEnableResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginEnable enables a plugin // PluginEnable enables a plugin
func (cli *Client) PluginEnable(ctx context.Context, name string, options PluginEnableOptions) error { func (cli *Client) PluginEnable(ctx context.Context, name string, options PluginEnableOptions) (PluginEnableResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginEnableResult{}, err
} }
query := url.Values{} query := url.Values{}
query.Set("timeout", strconv.Itoa(options.Timeout)) query.Set("timeout", strconv.Itoa(options.Timeout))
resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginEnableResult{}, err
} }

View File

@@ -33,17 +33,23 @@ type PluginInstallOptions struct {
Args []string Args []string
} }
// PluginInstallResult holds the result of a plugin install operation.
// It is an io.ReadCloser from which the caller can read installation progress or result.
type PluginInstallResult struct {
io.ReadCloser
}
// PluginInstall installs a plugin // PluginInstall installs a plugin
func (cli *Client) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (_ io.ReadCloser, retErr error) { func (cli *Client) PluginInstall(ctx context.Context, name string, options PluginInstallOptions) (_ PluginInstallResult, retErr error) {
query := url.Values{} query := url.Values{}
if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil {
return nil, fmt.Errorf("invalid remote reference: %w", err) return PluginInstallResult{}, fmt.Errorf("invalid remote reference: %w", err)
} }
query.Set("remote", options.RemoteRef) query.Set("remote", options.RemoteRef)
privileges, err := cli.checkPluginPermissions(ctx, query, options) privileges, err := cli.checkPluginPermissions(ctx, query, &options)
if err != nil { if err != nil {
return nil, err return PluginInstallResult{}, err
} }
// set name for plugin pull, if empty should default to remote reference // set name for plugin pull, if empty should default to remote reference
@@ -51,7 +57,7 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
if err != nil { if err != nil {
return nil, err return PluginInstallResult{}, err
} }
name = resp.Header.Get("Docker-Plugin-Name") name = resp.Header.Get("Docker-Plugin-Name")
@@ -70,7 +76,7 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
} }
}() }()
if len(options.Args) > 0 { if len(options.Args) > 0 {
if err := cli.PluginSet(ctx, name, options.Args); err != nil { if _, err := cli.PluginSet(ctx, name, PluginSetOptions{Args: options.Args}); err != nil {
_ = pw.CloseWithError(err) _ = pw.CloseWithError(err)
return return
} }
@@ -81,10 +87,10 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options Plugi
return return
} }
enableErr := cli.PluginEnable(ctx, name, PluginEnableOptions{Timeout: 0}) _, enableErr := cli.PluginEnable(ctx, name, PluginEnableOptions{Timeout: 0})
_ = pw.CloseWithError(enableErr) _ = pw.CloseWithError(enableErr)
}() }()
return pr, nil return PluginInstallResult{pr}, nil
} }
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) { func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
@@ -99,17 +105,17 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
}) })
} }
func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options PluginInstallOptions) (plugin.Privileges, error) { func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options pluginOptions) (plugin.Privileges, error) {
resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) resp, err := cli.tryPluginPrivileges(ctx, query, options.getRegistryAuth())
if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { if cerrdefs.IsUnauthorized(err) && options.getPrivilegeFunc() != nil {
// TODO: do inspect before to check existing name before checking privileges // TODO: do inspect before to check existing name before checking privileges
newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) newAuthHeader, privilegeErr := options.getPrivilegeFunc()(ctx)
if privilegeErr != nil { if privilegeErr != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return nil, privilegeErr return nil, privilegeErr
} }
options.RegistryAuth = newAuthHeader options.setRegistryAuth(newAuthHeader)
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) resp, err = cli.tryPluginPrivileges(ctx, query, options.getRegistryAuth())
} }
if err != nil { if err != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
@@ -123,14 +129,47 @@ func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values,
} }
ensureReaderClosed(resp) ensureReaderClosed(resp)
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { if !options.getAcceptAllPermissions() && options.getAcceptPermissionsFunc() != nil && len(privileges) > 0 {
accept, err := options.AcceptPermissionsFunc(ctx, privileges) accept, err := options.getAcceptPermissionsFunc()(ctx, privileges)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !accept { if !accept {
return nil, errors.New("permission denied while installing plugin " + options.RemoteRef) return nil, errors.New("permission denied while installing plugin " + options.getRemoteRef())
} }
} }
return privileges, nil return privileges, nil
} }
type pluginOptions interface {
getRegistryAuth() string
setRegistryAuth(string)
getPrivilegeFunc() func(context.Context) (string, error)
getAcceptAllPermissions() bool
getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error)
getRemoteRef() string
}
func (o *PluginInstallOptions) getRegistryAuth() string {
return o.RegistryAuth
}
func (o *PluginInstallOptions) setRegistryAuth(auth string) {
o.RegistryAuth = auth
}
func (o *PluginInstallOptions) getPrivilegeFunc() func(context.Context) (string, error) {
return o.PrivilegeFunc
}
func (o *PluginInstallOptions) getAcceptAllPermissions() bool {
return o.AcceptAllPermissions
}
func (o *PluginInstallOptions) getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error) {
return o.AcceptPermissionsFunc
}
func (o *PluginInstallOptions) getRemoteRef() string {
return o.RemoteRef
}

View File

@@ -13,18 +13,23 @@ type PluginListOptions struct {
Filters Filters Filters Filters
} }
// PluginListResult represents the result of a plugin list operation.
type PluginListResult struct {
Items []*plugin.Plugin
}
// PluginList returns the installed plugins // PluginList returns the installed plugins
func (cli *Client) PluginList(ctx context.Context, opts PluginListOptions) (plugin.ListResponse, error) { func (cli *Client) PluginList(ctx context.Context, options PluginListOptions) (PluginListResult, error) {
var plugins plugin.ListResponse
query := url.Values{} query := url.Values{}
opts.Filters.updateURLValues(query) options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/plugins", query, nil) resp, err := cli.get(ctx, "/plugins", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return plugins, err return PluginListResult{}, err
} }
var plugins plugin.ListResponse
err = json.NewDecoder(resp.Body).Decode(&plugins) err = json.NewDecoder(resp.Body).Decode(&plugins)
return plugins, err return PluginListResult{Items: plugins}, err
} }

View File

@@ -8,17 +8,27 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
// PluginPushOptions holds parameters to push a plugin.
type PluginPushOptions struct {
RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry
}
// PluginPushResult is the result of a plugin push operation
type PluginPushResult struct {
io.ReadCloser
}
// PluginPush pushes a plugin to a registry // PluginPush pushes a plugin to a registry
func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) { func (cli *Client) PluginPush(ctx context.Context, name string, options PluginPushOptions) (PluginPushResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return nil, err return PluginPushResult{}, err
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{ resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {options.RegistryAuth},
}) })
if err != nil { if err != nil {
return nil, err return PluginPushResult{}, err
} }
return resp.Body, nil return PluginPushResult{resp.Body}, nil
} }

View File

@@ -10,11 +10,16 @@ type PluginRemoveOptions struct {
Force bool Force bool
} }
// PluginRemoveResult represents the result of a plugin removal.
type PluginRemoveResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginRemove removes a plugin // PluginRemove removes a plugin
func (cli *Client) PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) error { func (cli *Client) PluginRemove(ctx context.Context, name string, options PluginRemoveOptions) (PluginRemoveResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginRemoveResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -24,5 +29,5 @@ func (cli *Client) PluginRemove(ctx context.Context, name string, options Plugin
resp, err := cli.delete(ctx, "/plugins/"+name, query, nil) resp, err := cli.delete(ctx, "/plugins/"+name, query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginRemoveResult{}, err
} }

View File

@@ -4,14 +4,24 @@ import (
"context" "context"
) )
// PluginSetOptions defines options for modifying a plugin's settings.
type PluginSetOptions struct {
Args []string
}
// PluginSetResult represents the result of a plugin set operation.
type PluginSetResult struct {
// Currently empty; can be extended in the future if needed.
}
// PluginSet modifies settings for an existing plugin // PluginSet modifies settings for an existing plugin
func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error { func (cli *Client) PluginSet(ctx context.Context, name string, options PluginSetOptions) (PluginSetResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return err return PluginSetResult{}, err
} }
resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, options.Args, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return PluginSetResult{}, err
} }

View File

@@ -12,8 +12,29 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
// PluginUpgradeOptions holds parameters to upgrade a plugin.
type PluginUpgradeOptions struct {
Disabled bool
AcceptAllPermissions bool
RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry
RemoteRef string // RemoteRef is the plugin name on the registry
// PrivilegeFunc is a function that clients can supply to retry operations
// after getting an authorization error. This function returns the registry
// authentication header value in base64 encoded format, or an error if the
// privilege request fails.
//
// For details, refer to [github.com/moby/moby/api/types/registry.RequestAuthConfig].
PrivilegeFunc func(context.Context) (string, error)
AcceptPermissionsFunc func(context.Context, plugin.Privileges) (bool, error)
Args []string
}
// PluginUpgradeResult holds the result of a plugin upgrade operation.
type PluginUpgradeResult io.ReadCloser
// PluginUpgrade upgrades a plugin // PluginUpgrade upgrades a plugin
func (cli *Client) PluginUpgrade(ctx context.Context, name string, options PluginInstallOptions) (io.ReadCloser, error) { func (cli *Client) PluginUpgrade(ctx context.Context, name string, options PluginUpgradeOptions) (PluginUpgradeResult, error) {
name, err := trimID("plugin", name) name, err := trimID("plugin", name)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -25,7 +46,7 @@ func (cli *Client) PluginUpgrade(ctx context.Context, name string, options Plugi
} }
query.Set("remote", options.RemoteRef) query.Set("remote", options.RemoteRef)
privileges, err := cli.checkPluginPermissions(ctx, query, options) privileges, err := cli.checkPluginPermissions(ctx, query, &options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -42,3 +63,27 @@ func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privi
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })
} }
func (o *PluginUpgradeOptions) getRegistryAuth() string {
return o.RegistryAuth
}
func (o *PluginUpgradeOptions) setRegistryAuth(auth string) {
o.RegistryAuth = auth
}
func (o *PluginUpgradeOptions) getPrivilegeFunc() func(context.Context) (string, error) {
return o.PrivilegeFunc
}
func (o *PluginUpgradeOptions) getAcceptAllPermissions() bool {
return o.AcceptAllPermissions
}
func (o *PluginUpgradeOptions) getAcceptPermissionsFunc() func(context.Context, plugin.Privileges) (bool, error) {
return o.AcceptPermissionsFunc
}
func (o *PluginUpgradeOptions) getRemoteRef() string {
return o.RemoteRef
}