1package client // import "github.com/docker/docker/client"
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"net/url"
8
9	"github.com/docker/distribution/reference"
10	"github.com/docker/docker/api/types"
11	"github.com/docker/docker/errdefs"
12	"github.com/pkg/errors"
13)
14
15// PluginInstall installs a plugin
16func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
17	query := url.Values{}
18	if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil {
19		return nil, errors.Wrap(err, "invalid remote reference")
20	}
21	query.Set("remote", options.RemoteRef)
22
23	privileges, err := cli.checkPluginPermissions(ctx, query, options)
24	if err != nil {
25		return nil, err
26	}
27
28	// set name for plugin pull, if empty should default to remote reference
29	query.Set("name", name)
30
31	resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
32	if err != nil {
33		return nil, err
34	}
35
36	name = resp.header.Get("Docker-Plugin-Name")
37
38	pr, pw := io.Pipe()
39	go func() { // todo: the client should probably be designed more around the actual api
40		_, err := io.Copy(pw, resp.body)
41		if err != nil {
42			pw.CloseWithError(err)
43			return
44		}
45		defer func() {
46			if err != nil {
47				delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil)
48				ensureReaderClosed(delResp)
49			}
50		}()
51		if len(options.Args) > 0 {
52			if err := cli.PluginSet(ctx, name, options.Args); err != nil {
53				pw.CloseWithError(err)
54				return
55			}
56		}
57
58		if options.Disabled {
59			pw.Close()
60			return
61		}
62
63		enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
64		pw.CloseWithError(enableErr)
65	}()
66	return pr, nil
67}
68
69func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
70	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
71	return cli.get(ctx, "/plugins/privileges", query, headers)
72}
73
74func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (serverResponse, error) {
75	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
76	return cli.post(ctx, "/plugins/pull", query, privileges, headers)
77}
78
79func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
80	resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
81	if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
82		// todo: do inspect before to check existing name before checking privileges
83		newAuthHeader, privilegeErr := options.PrivilegeFunc()
84		if privilegeErr != nil {
85			ensureReaderClosed(resp)
86			return nil, privilegeErr
87		}
88		options.RegistryAuth = newAuthHeader
89		resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
90	}
91	if err != nil {
92		ensureReaderClosed(resp)
93		return nil, err
94	}
95
96	var privileges types.PluginPrivileges
97	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
98		ensureReaderClosed(resp)
99		return nil, err
100	}
101	ensureReaderClosed(resp)
102
103	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
104		accept, err := options.AcceptPermissionsFunc(privileges)
105		if err != nil {
106			return nil, err
107		}
108		if !accept {
109			return nil, pluginPermissionDenied{options.RemoteRef}
110		}
111	}
112	return privileges, nil
113}
114