From e78f02c4dbc3cada909c114fef6b6643969ab912 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 15:17:34 -0500 Subject: [PATCH] Implement docker pull with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 2 ++ api/client/create.go | 7 ++----- api/client/lib/image_create.go | 9 +++++--- api/client/lib/image_pull.go | 34 ++++++++++++++++++++++++++++++ api/client/lib/privileged.go | 9 ++++++++ api/client/pull.go | 38 +++++++++++++++++++++++++++------- api/client/trust.go | 13 ++++-------- api/client/utils.go | 15 ++++++++++++++ api/types/client.go | 8 +++++++ cliconfig/config.go | 9 ++++++++ 10 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 api/client/lib/image_pull.go create mode 100644 api/client/lib/privileged.go diff --git a/api/client/client.go b/api/client/client.go index d3c502ac1e..c71617ac2a 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -7,6 +7,7 @@ package client import ( "io" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/types" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/parsers/filters" @@ -48,6 +49,7 @@ type apiClient interface { ImageImport(options types.ImageImportOptions) (io.ReadCloser, error) ImageList(options types.ImageListOptions) ([]types.Image, error) ImageLoad(input io.Reader) (io.ReadCloser, error) + ImagePull(options types.ImagePullOptions, privilegeFunc lib.RequestPrivilegeFunc) (io.ReadCloser, error) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) ImageSave(imageIDs []string) (io.ReadCloser, error) ImageTag(options types.ImageTagOptions) error diff --git a/api/client/create.go b/api/client/create.go index a5430eb3cb..857ef90aac 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -1,8 +1,6 @@ package client import ( - "encoding/base64" - "encoding/json" "fmt" "io" "os" @@ -45,8 +43,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { } // Resolve the Auth config relevant for this server - authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) - buf, err := json.Marshal(authConfig) + encodedAuth, err := cli.encodeRegistryAuth(repoInfo.Index) if err != nil { return err } @@ -54,7 +51,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { options := types.ImageCreateOptions{ Parent: ref.Name(), Tag: tag, - RegistryAuth: base64.URLEncoding.EncodeToString(buf), + RegistryAuth: encodedAuth, } responseBody, err := cli.client.ImageCreate(options) diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index 3d4e70e01c..75d4b75e4a 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -13,11 +13,14 @@ func (cli *Client) ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, query := url.Values{} query.Set("fromImage", options.Parent) query.Set("tag", options.Tag) - - headers := map[string][]string{"X-Registry-Auth": {options.RegistryAuth}} - resp, err := cli.post("/images/create", query, nil, headers) + resp, err := cli.tryImageCreate(query, options.RegistryAuth) if err != nil { return nil, err } return resp.body, nil } + +func (cli *Client) tryImageCreate(query url.Values, registryAuth string) (*serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post("/images/create", query, nil, headers) +} diff --git a/api/client/lib/image_pull.go b/api/client/lib/image_pull.go new file mode 100644 index 0000000000..ef5f1ce6e6 --- /dev/null +++ b/api/client/lib/image_pull.go @@ -0,0 +1,34 @@ +package lib + +import ( + "io" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImagePull request the docker host to pull an image from a remote registry. +// It executes the privileged function if the operation is unauthorized +// and it tries one more time. +// It's up to the caller to handle the io.ReadCloser and close it properly. +func (cli *Client) ImagePull(options types.ImagePullOptions, privilegeFunc RequestPrivilegeFunc) (io.ReadCloser, error) { + query := url.Values{} + query.Set("fromImage", options.ImageID) + if options.Tag != "" { + query.Set("tag", options.Tag) + } + + resp, err := cli.tryImageCreate(query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized { + newAuthHeader, privilegeErr := privilegeFunc() + if privilegeErr != nil { + return nil, privilegeErr + } + resp, err = cli.tryImageCreate(query, newAuthHeader) + } + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/api/client/lib/privileged.go b/api/client/lib/privileged.go new file mode 100644 index 0000000000..cb3da51ea8 --- /dev/null +++ b/api/client/lib/privileged.go @@ -0,0 +1,9 @@ +package lib + +// RequestPrivilegeFunc is a function interface that +// clients can supply to retry operations after +// getting an authorization error. +// This function returns the registry authentication +// header value in base 64 format, or an error +// if the privilege request fails. +type RequestPrivilegeFunc func() (string, error) diff --git a/api/client/pull.go b/api/client/pull.go index e585a12081..88879308a3 100644 --- a/api/client/pull.go +++ b/api/client/pull.go @@ -3,10 +3,13 @@ package client import ( "errors" "fmt" - "net/url" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/jsonmessage" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" tagpkg "github.com/docker/docker/tag" @@ -62,15 +65,34 @@ func (cli *DockerCli) CmdPull(args ...string) error { return err } + authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + if isTrusted() && !ref.HasDigest() { // Check if tag is digest - authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) - return cli.trustedPull(repoInfo, ref, authConfig) + return cli.trustedPull(repoInfo, ref, authConfig, requestPrivilege) } - v := url.Values{} - v.Set("fromImage", distributionRef.String()) - - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") - return err + return cli.imagePullPrivileged(authConfig, distributionRef.String(), "", requestPrivilege) +} + +func (cli *DockerCli) imagePullPrivileged(authConfig cliconfig.AuthConfig, imageID, tag string, requestPrivilege lib.RequestPrivilegeFunc) error { + + encodedAuth, err := authConfig.EncodeToBase64() + if err != nil { + return err + } + options := types.ImagePullOptions{ + ImageID: imageID, + Tag: tag, + RegistryAuth: encodedAuth, + } + + responseBody, err := cli.client.ImagePull(options, requestPrivilege) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) } diff --git a/api/client/trust.go b/api/client/trust.go index 4a550a381a..1bc4f8fe1f 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -22,6 +22,7 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/types" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/ansiescape" @@ -278,11 +279,8 @@ func notaryError(err error) error { return err } -func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig) error { - var ( - v = url.Values{} - refs = []target{} - ) +func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig, requestPrivilege lib.RequestPrivilegeFunc) error { + var refs []target notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) if err != nil { @@ -317,17 +315,14 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr refs = append(refs, r) } - v.Set("fromImage", repoInfo.LocalName.Name()) for i, r := range refs { displayTag := r.reference.String() if displayTag != "" { displayTag = ":" + displayTag } fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.LocalName, displayTag, r.digest) - v.Set("tag", r.digest.String()) - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") - if err != nil { + if err := cli.imagePullPrivileged(authConfig, repoInfo.LocalName.Name(), r.digest.String(), requestPrivilege); err != nil { return err } diff --git a/api/client/utils.go b/api/client/utils.go index afa0353b09..095ae8ee63 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -160,6 +160,21 @@ func (cli *DockerCli) cmdAttempt(authConfig cliconfig.AuthConfig, method, path s return serverResp.body, serverResp.statusCode, err } +func (cli *DockerCli) encodeRegistryAuth(index *registry.IndexInfo) (string, error) { + authConfig := registry.ResolveAuthConfig(cli.configFile, index) + return authConfig.EncodeToBase64() +} + +func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registry.IndexInfo, cmdName string) lib.RequestPrivilegeFunc { + return func() (string, error) { + fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) + if err := cli.CmdLogin(index.GetAuthConfigKey()); err != nil { + return "", err + } + return cli.encodeRegistryAuth(index) + } +} + func (cli *DockerCli) clientRequestAttemptLogin(method, path string, in io.Reader, out io.Writer, index *registry.IndexInfo, cmdName string) (io.ReadCloser, int, error) { // Resolve the Auth config relevant for this server diff --git a/api/types/client.go b/api/types/client.go index 8ce653d590..10f40a8cd8 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -179,6 +179,14 @@ type ImageListOptions struct { Filters filters.Args } +// ImagePullOptions holds information to pull images. +type ImagePullOptions struct { + ImageID string + Tag string + // RegistryAuth is the base64 encoded credentials for this server + RegistryAuth string +} + // ImageRemoveOptions holds parameters to remove images. type ImageRemoveOptions struct { ImageID string diff --git a/cliconfig/config.go b/cliconfig/config.go index b28d6e5bcf..0bcf87d87d 100644 --- a/cliconfig/config.go +++ b/cliconfig/config.go @@ -54,6 +54,15 @@ type AuthConfig struct { RegistryToken string `json:"registrytoken,omitempty"` } +// EncodeToBase64 serializes the auth configuration as JSON base64 payload +func (a AuthConfig) EncodeToBase64() (string, error) { + buf, err := json.Marshal(a) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buf), nil +} + // ConfigFile ~/.docker/config.json file info type ConfigFile struct { AuthConfigs map[string]AuthConfig `json:"auths"`