8000 Client Support for Docker Registry HTTP API V2 by stevvooe · Pull Request #9784 · moby/moby · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Client Support for Docker Registry HTTP API V2 #9784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jan 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
41e20ce
Adds support for v2 registry login
Dec 12, 2014
ac8d964
Add trust key creation on client
dmcgowan Oct 22, 2014
188b56c
Push flow
dmcgowan Oct 1, 2014
bcc0a34
Update manifest format for push
dmcgowan Oct 10, 2014
3e4fd00
Use tarsum dev version to fix mtime issue
dmcgowan Oct 10, 2014
e9b590d
Update push to use mount blob endpoint
dmcgowan Nov 15, 2014
e233625
Update token response handling
Dec 12, 2014
a0f92a2
Registry V2 HTTP route and error code definitions
stevvooe Dec 12, 2014
dbb4b03
Remove dependencies on registry packages
stevvooe Dec 15, 2014
0336b0c
Update push and pull to registry 2.1 specification
dmcgowan Dec 17, 2014
7d61255
Allow private V2 registry endpoints
dmcgowan Dec 19, 2014
d094eb6
Get token on each request
dmcgowan Dec 20, 2014
1b43144
Correctly check and propagate errors in v2 session
stevvooe Dec 22, 2014
7eeda3f
Fix tests
dmcgowan Dec 23, 2014
213e3d1
Add Tarsum Calculation during v2 Pull operation
Dec 23, 2014
25945a4
Refactor from feedback
dmcgowan Jan 2, 2015
8ceb9d2
Update push to sign with the daemon's key when no manifest is given
dmcgowan Jan 7, 2015
1a9cdb1
Fix list tags
dmcgowan Jan 7, 2015
9a38aa0
Fix integration test failures
dmcgowan Jan 12, 2015
ef96c28
Install registry V2 in image
LK4D4 Jan 12, 2015
2fc2862
RegistryV2 datastructure for tests
LK4D4 Jan 12, 2015
f138f7b
Tests for push to registry v2
LK4D4 Jan 12, 2015
dbec231
Add some push test coverage
Jan 13, 2015
92d5eaf
Test pulling image with aliases
Jan 13, 2015
750b41c
Refactor push and pull to move code out of cmd function
dmcgowan Jan 13, 2015
9c6f8e1
Cleanup v2 session to require endpoint
dmcgowan Jan 15, 2015
f11f3f6
Remove session backup
dmcgowan Jan 15, 2015
dd914f9
Add token cache
dmcgowan Jan 15, 2015
f29aacb
Fix failing integration tests
dmcgowan Jan 16, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ RUN set -x \
&& git clone -b v1.2 https://github.com/russross/blackfriday.git /go/src/github.com/russross/blackfriday \
&& go install -v github.com/cpuguy83/go-md2man

# Install registry
COPY pkg/tarsum /go/src/github.com/docker/docker/pkg/tarsum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt this just work? without this line?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try it, Jess. ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but but but je suis le lazyyyy, jk jk i trust you

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I'm not doing my job right. It's always my fault, so you really
shouldn't trust me. ;)

# REGISTRY_COMMIT gives us the repeatability guarantees we need
# (so that we're all testing the same version of the registry)
ENV REGISTRY_COMMIT 21a69f53b5c7986b831f33849d551cd59ec8cbd1
RUN set -x \
&& git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \
&& (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \
&& go get -d github.com/docker/distribution/cmd/registry \
&& go build -o /go/bin/registry-v2 github.com/docker/distribution/cmd/registry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only for tests right or are you using this in the code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could split adding the tests into a different PR from this which might be cool, only if they work on the current registry of course ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is only for tests (we exec the registry-v2 binary so we can push/pull to it).

TBH before this PR there was basically no tests (only 4 written, 3 of them being t.Skip()), so I'm not sure it's worth splitting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok no bigs just wondering


# Wrap all commands in the "docker-in-docker" script to allow nested containers
ENTRYPOINT ["hack/dind"]

Expand Down
23 changes: 22 additions & 1 deletion api/client/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/docker/docker/registry"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/utils"
"github.com/docker/libtrust"
)

const (
Expand Down Expand Up @@ -1215,6 +1216,26 @@ func (cli *DockerCli) CmdPush(args ...string) error {

v := url.Values{}
v.Set("tag", tag)

body, _, err := readBody(cli.call("GET", "/images/"+remote+"/manifest?"+v.Encode(), nil, false))
if err != nil {
return err
}

js, err := libtrust.NewJSONSignature(body)
if err != nil {
return err
}
err = js.Sign(cli.key)
if err != nil {
return err
}

signedBody, err := js.PrettySignature("signatures")
if err != nil {
return err
}

push := func(authConfig registry.AuthConfig) error {
buf, err := json.Marshal(authConfig)
if err != nil {
Expand All @@ -1224,7 +1245,7 @@ func (cli *DockerCli) CmdPush(args ...string) error {
base64.URLEncoding.EncodeToString(buf),
}

return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, map[string][]string{
return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), bytes.NewReader(signedBody), cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
}
Expand Down
19 changes: 19 additions & 0 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,18 @@ func getImagesSearch(eng *engine.Engine, version version.Version, w http.Respons
return job.Run()
}

func getImageManifest(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}

job := eng.Job("image_manifest", vars["name"])
job.Setenv("tag", r.Form.Get("tag"))
job.Stdout.Add(utils.NewWriteFlusher(w))

return job.Run()
}

func postImagesPush(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if vars == nil {
return fmt.Errorf("Missing parameter")
Expand Down Expand Up @@ -639,9 +651,15 @@ func postImagesPush(eng *engine.Engine, version version.Version, w http.Response
}
}

manifest, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}

job := eng.Job("push", vars["name"])
job.SetenvJson("metaHeaders", metaHeaders)
job.SetenvJson("authConfig", authConfig)
job.Setenv("manifest", string(manifest))
job.Setenv("tag", r.Form.Get("tag"))
if version.GreaterThan("1.0") {
job.SetenvBool("json", true)
Expand Down Expand Up @@ -1294,6 +1312,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
"/images/viz": getImagesViz,
"/images/search": getImagesSearch,
"/images/get": getImagesGet,
"/images/{name:.*}/manifest": getImageManifest,
"/images/{name:.*}/get": getImagesGet,
"/images/{name:.*}/history": getImagesHistory,
"/images/{name:.*}/json": getImagesByName,
Expand Down
12 changes: 6 additions & 6 deletions daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,8 +895,13 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
return nil, err
}

trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath)
if err != nil {
return nil, err
}

log.Debugf("Creating repository list")
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, trustKey)
if err != nil {
return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
}
Expand Down Expand Up @@ -961,11 +966,6 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
return nil, err
}

trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath)
if err != nil {
return nil, err
}

daemon := &Daemon{
ID: trustKey.PublicKey().KeyID(),
repository: daemonRepo,
Expand Down
9 changes: 7 additions & 2 deletions docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func main() {
}
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

F438 trustKey, err := api.LoadOrCreateTrustKey(*flTrustKey)
if err != nil {
log.Fatal(err)
}

var (
cli *client.DockerCli
tlsConfig tls.Config
Expand Down Expand Up @@ -118,9 +123,9 @@ func main() {
}

if *flTls || *flTlsVerify {
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
} else {
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil)
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], nil)
}

if err := cli.Cmd(flag.Args()...); err != nil {
Expand Down
196 changes: 196 additions & 0 deletions graph/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package graph

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"path"

log "github.com/Sirupsen/logrus"
"github.com/docker/docker/engine"
"github.com/docker/docker/pkg/tarsum"
"github.com/docker/docker/registry"
"github.com/docker/docker/runconfig"
"github.com/docker/libtrust"
)

func (s *TagStore) CmdManifest(job *engine.Job) engine.Status {
if len(job.Args) != 1 {
return job.Errorf("usage: %s NAME", job.Name)
}
name := job.Args[0]
tag := job.Getenv("tag")
if tag == "" {
tag = "latest"
}

// Resolve the Repository name from fqn to endpoint + name
repoInfo, err := registry.ParseRepositoryInfo(name)
if err != nil {
return job.Error(err)
}

manifestBytes, err := s.newManifest(name, repoInfo.RemoteName, tag)
if err != nil {
return job.Error(err)
}

_, err = job.Stdout.Write(manifestBytes)
if err != nil {
return job.Error(err)
}

return engine.StatusOK
}

func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) {
manifest := &registry.ManifestData{
Name: remoteName,
Tag: tag,
SchemaVersion: 1,
}
localRepo, err := s.Get(localName)
if err != nil {
return nil, err
}
if localRepo == nil {
return nil, fmt.Errorf("Repo does not exist: %s", localName)
}

// Get the top-most layer id which the tag points to
layerId, exists := localRepo[tag]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explicit what layerId is here? Is that the "top-most" layer for the image?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be the top-most layer which the tag is referring directly to. I will add a comment.

if !exists {
return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I just noticed an extra space between name and tag

}
layersSeen := make(map[string]bool)

layer, err := s.graph.Get(layerId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's "history", but I'm really confused by all this :-( We're passing a layerId to something which returns an Image and potentially emits errors about "missing image".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the "tag" store and "layer" store (called graph) are separate the in the current implementation. These will be unified in a single "image" store which shouldn't allow for confusion and inconsistencies in the interface. Josh has started some work on the "image" store, but it is out of scope for this PR.

if err != nil {
return nil, err
}
if layer.Config == nil {
return nil, errors.New("Missing layer configuration")
}
manifest.Architecture = layer.Architecture
manifest.FSLayers = make([]*registry.FSLayer, 0, 4)
manifest.History = make([]*registry.ManifestHistory, 0, 4)
var metadata runconfig.Config
metadata = *layer.Config

for ; layer != nil; layer, err = layer.GetParent() {
if err != nil {
return nil, err
}

if layersSeen[layer.ID] {
break
}
if layer.Config != nil && metadata.Image != layer.ID {
err = runconfig.Merge(&metadata, layer.Config)
if err != nil {
return nil, err
}
}

archive, err := layer.TarLayer()
if err != nil {
return nil, err
}

tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, we need to calculate the tarSum for each layer at manifest creation time. What I'm not sure to understand is that a manifest refers to content-addressable layers, so why would we need to compute this for an already known layer?

if err != nil {
return nil, err
}
if _, err := io.Copy(ioutil.Discard, tarSum); err != nil {
return nil, err
}

tarId := tarSum.Sum(nil)

manifest.FSLayers = append(manifest.FSLayers, &registry.FSLayer{BlobSum: tarId})

layersSeen[layer.ID] = true
jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json"))
if err != nil {
return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)
}
manifest.History = append(manifest.History, &registry.ManifestHistory{V1Compatibility: string(jsonData)})
}

manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return nil, err
}

return manifestBytes, nil
}

func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) {
sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
if err != nil {
return nil, false, fmt.Errorf("error parsing payload: %s", err)
}

keys, err := sig.Verify()
if err != nil {
return nil, false, fmt.Errorf("error verifying payload: %s", err)
}

payload, err := sig.Payload()
if err != nil {
return nil, false, fmt.Errorf("error retrieving payload: %s", err)
}

var manifest registry.ManifestData
if err := json.Unmarshal(payload, &manifest); err != nil {
return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
}
if manifest.SchemaVersion != 1 {
return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
}

var verified bool
for _, key := range keys {
job := eng.Job("trust_key_check")
b, err := key.MarshalJSON()
if err != nil {
return nil, false, fmt.Errorf("error marshalling public key: %s", err)
}
namespace := manifest.Name
if namespace[0] != '/' {
namespace = "/" + namespace
}
stdoutBuffer := bytes.NewBuffer(nil)

job.Args = append(job.Args, namespace)
job.Setenv("PublicKey", string(b))
// Check key has read/write permission (0x03)
job.SetenvInt("Permission", 0x03)
job.Stdout.Add(stdoutBuffer)
if err = job.Run(); err != nil {
return nil, false, fmt.Errorf("error running key check: %s", err)
}
result := engine.Tail(stdoutBuffer, 1)
log.Debugf("Key check result: %q", result)
if result == "verified" {
verified = true
}
}

return &manifest, verified, nil
}

func checkValidManifest(manifest *registry.ManifestData) error {
if len(manifest.FSLayers) != len(manifest.History) {
return fmt.Errorf("length of history not equal to number of layers")
}

if len(manifest.FSLayers) == 0 {
return fmt.Errorf("no FSLayers in manifest")
}

return nil
}
Loading
0