Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
| ... | ... |
@@ -43,7 +43,7 @@ clone git github.com/hashicorp/consul v0.5.2 |
| 43 | 43 |
clone git github.com/boltdb/bolt v1.1.0 |
| 44 | 44 |
|
| 45 | 45 |
# get graph and distribution packages |
| 46 |
-clone git github.com/docker/distribution 568bf038af6d65b376165d02886b1c7fcaef1f61 |
|
| 46 |
+clone git github.com/docker/distribution a7ae88da459b98b481a245e5b1750134724ac67d |
|
| 47 | 47 |
clone git github.com/vbatts/tar-split v0.9.11 |
| 48 | 48 |
|
| 49 | 49 |
# get desired notary commit, might also need to be updated in Dockerfile |
| ... | ... |
@@ -5,3 +5,10 @@ Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com> |
| 5 | 5 |
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu> |
| 6 | 6 |
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com> |
| 7 | 7 |
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com> |
| 8 |
+Andrew Meredith <andymeredith@gmail.com> Andrew Meredith <kendru@users.noreply.github.com> |
|
| 9 |
+harche <p.harshal@gmail.com> harche <harche@users.noreply.github.com> |
|
| 10 |
+Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com> |
|
| 11 |
+Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com> |
|
| 12 |
+Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com> |
|
| 13 |
+Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr> |
|
| 14 |
+davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com> |
|
| 8 | 15 |
\ No newline at end of file |
| ... | ... |
@@ -5,13 +5,16 @@ Adrian Mouat <adrian.mouat@gmail.com> |
| 5 | 5 |
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com> |
| 6 | 6 |
Alex Chan <alex.chan@metaswitch.com> |
| 7 | 7 |
Alex Elman <aelman@indeed.com> |
| 8 |
+amitshukla <ashukla73@hotmail.com> |
|
| 8 | 9 |
Amy Lindburg <amy.lindburg@docker.com> |
| 10 |
+Andrew Meredith <andymeredith@gmail.com> |
|
| 9 | 11 |
Andrey Kostov <kostov.andrey@gmail.com> |
| 10 | 12 |
Andy Goldstein <agoldste@redhat.com> |
| 11 | 13 |
Anton Tiurin <noxiouz@yandex.ru> |
| 12 | 14 |
Antonio Mercado <amercado@thinknode.com> |
| 13 | 15 |
Arnaud Porterie <arnaud.porterie@docker.com> |
| 14 | 16 |
Arthur Baars <arthur@semmle.com> |
| 17 |
+Avi Miller <avi.miller@oracle.com> |
|
| 15 | 18 |
Ayose Cazorla <ayosec@gmail.com> |
| 16 | 19 |
BadZen <dave.trombley@gmail.com> |
| 17 | 20 |
Ben Firshman <ben@firshman.co.uk> |
| ... | ... |
@@ -32,9 +35,10 @@ Derek McGowan <derek@mcgstyle.net> |
| 32 | 32 |
Diogo Mónica <diogo.monica@gmail.com> |
| 33 | 33 |
Donald Huang <don.hcd@gmail.com> |
| 34 | 34 |
Doug Davis <dug@us.ibm.com> |
| 35 |
+farmerworking <farmerworking@gmail.com> |
|
| 35 | 36 |
Florentin Raud <florentin.raud@gmail.com> |
| 36 | 37 |
Frederick F. Kautz IV <fkautz@alumni.cmu.edu> |
| 37 |
-harche <harche@users.noreply.github.com> |
|
| 38 |
+harche <p.harshal@gmail.com> |
|
| 38 | 39 |
Henri Gomez <henri.gomez@gmail.com> |
| 39 | 40 |
Hu Keping <hukeping@huawei.com> |
| 40 | 41 |
Hua Wang <wanghua.humble@gmail.com> |
| ... | ... |
@@ -42,9 +46,10 @@ Ian Babrou <ibobrik@gmail.com> |
| 42 | 42 |
Jack Griffin <jackpg14@gmail.com> |
| 43 | 43 |
Jason Freidman <jason.freidman@gmail.com> |
| 44 | 44 |
Jeff Nickoloff <jeff@allingeek.com> |
| 45 |
-Jessie Frazelle <jfrazelle@users.noreply.github.com> |
|
| 45 |
+Jessie Frazelle <jessie@docker.com> |
|
| 46 | 46 |
Jianqing Wang <tsing@jianqing.org> |
| 47 | 47 |
Jon Poler <jonathan.poler@apcera.com> |
| 48 |
+Jonathan Boulle <jonathanboulle@gmail.com> |
|
| 48 | 49 |
Jordan Liggitt <jliggitt@redhat.com> |
| 49 | 50 |
Josh Hawn <josh.hawn@docker.com> |
| 50 | 51 |
Julien Fernandez <julien.fernandez@gmail.com> |
| ... | ... |
@@ -59,6 +64,7 @@ Matt Moore <mattmoor@google.com> |
| 59 | 59 |
Matt Robenolt <matt@ydekproductions.com> |
| 60 | 60 |
Michael Prokop <mika@grml.org> |
| 61 | 61 |
Miquel Sabaté <msabate@suse.com> |
| 62 |
+Morgan Bauer <mbauer@us.ibm.com> |
|
| 62 | 63 |
moxiegirl <mary@docker.com> |
| 63 | 64 |
Nathan Sullivan <nathan@nightsys.net> |
| 64 | 65 |
nevermosby <robolwq@qq.com> |
| ... | ... |
@@ -70,8 +76,8 @@ Olivier Jacques <olivier.jacques@hp.com> |
| 70 | 70 |
Patrick Devine <patrick.devine@docker.com> |
| 71 | 71 |
Philip Misiowiec <philip@atlashealth.com> |
| 72 | 72 |
Richard Scothern <richard.scothern@docker.com> |
| 73 |
+Rusty Conover <rusty@luckydinosaur.com> |
|
| 73 | 74 |
Sebastiaan van Stijn <github@gone.nl> |
| 74 |
-Sharif Nassar <mrwacky42@users.noreply.github.com> |
|
| 75 | 75 |
Sharif Nassar <sharif@mrwacky.com> |
| 76 | 76 |
Shawn Falkner-Horine <dreadpirateshawn@gmail.com> |
| 77 | 77 |
Shreyas Karnik <karnik.shreyas@gmail.com> |
| ... | ... |
@@ -81,15 +87,16 @@ Stephen J Day <stephen.day@docker.com> |
| 81 | 81 |
Sungho Moon <sungho.moon@navercorp.com> |
| 82 | 82 |
Sven Dowideit <SvenDowideit@home.org.au> |
| 83 | 83 |
Sylvain Baubeau <sbaubeau@redhat.com> |
| 84 |
+Ted Reed <ted.reed@gmail.com> |
|
| 84 | 85 |
tgic <farmer1992@gmail.com> |
| 85 | 86 |
Thomas Sjögren <konstruktoid@users.noreply.github.com> |
| 86 | 87 |
Tianon Gravi <admwiggin@gmail.com> |
| 87 | 88 |
Tibor Vass <teabee89@gmail.com> |
| 89 |
+Tonis Tiigi <tonistiigi@gmail.com> |
|
| 88 | 90 |
Troels Thomsen <troels@thomsen.io> |
| 89 | 91 |
Vincent Batts <vbatts@redhat.com> |
| 90 | 92 |
Vincent Demeester <vincent@sbr.pm> |
| 91 | 93 |
Vincent Giersch <vincent.giersch@ovh.net> |
| 92 |
-Vincent Giersch <vincent@giersch.fr> |
|
| 93 | 94 |
W. Trevor King <wking@tremily.us> |
| 94 | 95 |
xg.song <xg.song@venusource.com> |
| 95 | 96 |
xiekeyang <xiekeyang@huawei.com> |
| ... | ... |
@@ -1,8 +1,58 @@ |
| 1 |
-Solomon Hykes <solomon@docker.com> (@shykes) |
|
| 2 |
-Olivier Gambier <olivier@docker.com> (@dmp42) |
|
| 3 |
-Stephen Day <stephen.day@docker.com> (@stevvooe) |
|
| 4 |
-Derek McGowan <derek@mcgstyle.net> (@dmcgowan) |
|
| 5 |
-Richard Scothern <richard.scothern@gmail.com> (@richardscothern) |
|
| 6 |
-Aaron Lehmann <aaron.lehmann@docker.com> (@aaronlehmann) |
|
| 1 |
+# Distribution maintainers file |
|
| 2 |
+# |
|
| 3 |
+# This file describes who runs the docker/distribution project and how. |
|
| 4 |
+# This is a living document - if you see something out of date or missing, speak up! |
|
| 5 |
+# |
|
| 6 |
+# It is structured to be consumable by both humans and programs. |
|
| 7 |
+# To extract its contents programmatically, use any TOML-compliant parser. |
|
| 8 |
+# |
|
| 9 |
+# This file is compiled into the MAINTAINERS file in docker/opensource. |
|
| 10 |
+# |
|
| 11 |
+[Org] |
|
| 12 |
+ [Org."Core maintainers"] |
|
| 13 |
+ people = [ |
|
| 14 |
+ "aaronlehmann", |
|
| 15 |
+ "dmcgowan", |
|
| 16 |
+ "dmp42", |
|
| 17 |
+ "richardscothern", |
|
| 18 |
+ "shykes", |
|
| 19 |
+ "stevvooe", |
|
| 20 |
+ ] |
|
| 7 | 21 |
|
| 22 |
+[people] |
|
| 8 | 23 |
|
| 24 |
+# A reference list of all people associated with the project. |
|
| 25 |
+# All other sections should refer to people by their canonical key |
|
| 26 |
+# in the people section. |
|
| 27 |
+ |
|
| 28 |
+ # ADD YOURSELF HERE IN ALPHABETICAL ORDER |
|
| 29 |
+ |
|
| 30 |
+ [people.aaronlehmann] |
|
| 31 |
+ Name = "Aaron Lehmann" |
|
| 32 |
+ Email = "aaron.lehmann@docker.com" |
|
| 33 |
+ GitHub = "aaronlehmann" |
|
| 34 |
+ |
|
| 35 |
+ [people.dmcgowan] |
|
| 36 |
+ Name = "Derek McGowan" |
|
| 37 |
+ Email = "derek@mcgstyle.net" |
|
| 38 |
+ GitHub = "dmcgowan" |
|
| 39 |
+ |
|
| 40 |
+ [people.dmp42] |
|
| 41 |
+ Name = "Olivier Gambier" |
|
| 42 |
+ Email = "olivier@docker.com" |
|
| 43 |
+ GitHub = "dmp42" |
|
| 44 |
+ |
|
| 45 |
+ [people.richardscothern] |
|
| 46 |
+ Name = "Richard Scothern" |
|
| 47 |
+ Email = "richard.scothern@gmail.com" |
|
| 48 |
+ GitHub = "richardscothern" |
|
| 49 |
+ |
|
| 50 |
+ [people.shykes] |
|
| 51 |
+ Name = "Solomon Hykes" |
|
| 52 |
+ Email = "solomon@docker.com" |
|
| 53 |
+ GitHub = "shykes" |
|
| 54 |
+ |
|
| 55 |
+ [people.stevvooe] |
|
| 56 |
+ Name = "Stephen Day" |
|
| 57 |
+ Email = "stephen.day@docker.com" |
|
| 58 |
+ GitHub = "stevvooe" |
| ... | ... |
@@ -17,9 +17,9 @@ This repository contains the following components: |
| 17 | 17 |
|**Component** |Description | |
| 18 | 18 |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| 19 | 19 |
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. | |
| 20 |
-| **libraries** | A rich set of libraries for interacting with,distribution components. Please see [godoc](http://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. | |
|
| 20 |
+| **libraries** | A rich set of libraries for interacting with,distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. | |
|
| 21 | 21 |
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) | |
| 22 |
-| **documentation** | Docker's full documentation set is available at [docs.docker.com](http://docs.docker.com). This repository [contains the subset](docs/index.md) related just to the registry. | |
|
| 22 |
+| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/index.md) related just to the registry. | |
|
| 23 | 23 |
|
| 24 | 24 |
### How does this integrate with Docker engine? |
| 25 | 25 |
|
| ... | ... |
@@ -58,7 +58,7 @@ For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md). |
| 58 | 58 |
### Who needs to deploy a registry? |
| 59 | 59 |
|
| 60 | 60 |
By default, Docker users pull images from Docker's public registry instance. |
| 61 |
-[Installing Docker](http://docs.docker.com/installation) gives users this |
|
| 61 |
+[Installing Docker](https://docs.docker.com/engine/installation/) gives users this |
|
| 62 | 62 |
ability. Users can also push images to a repository on Docker's public registry, |
| 63 | 63 |
if they have a [Docker Hub](https://hub.docker.com/) account. |
| 64 | 64 |
|
| ... | ... |
@@ -61,6 +61,15 @@ type Descriptor struct {
|
| 61 | 61 |
// depend on the simplicity of this type. |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 |
+// Descriptor returns the descriptor, to make it satisfy the Describable |
|
| 65 |
+// interface. Note that implementations of Describable are generally objects |
|
| 66 |
+// which can be described, not simply descriptors; this exception is in place |
|
| 67 |
+// to make it more convenient to pass actual descriptors to functions that |
|
| 68 |
+// expect Describable objects. |
|
| 69 |
+func (d Descriptor) Descriptor() Descriptor {
|
|
| 70 |
+ return d |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 64 | 73 |
// BlobStatter makes blob descriptors available by digest. The service may |
| 65 | 74 |
// provide a descriptor of a different digest if the provided digest is not |
| 66 | 75 |
// canonical. |
| ... | ... |
@@ -6,6 +6,8 @@ machine: |
| 6 | 6 |
# Install ceph to test rados driver & create pool |
| 7 | 7 |
- sudo -i ~/distribution/contrib/ceph/ci-setup.sh |
| 8 | 8 |
- ceph osd pool create docker-distribution 1 |
| 9 |
+ # Install codecov for coverage |
|
| 10 |
+ - pip install --user codecov |
|
| 9 | 11 |
|
| 10 | 12 |
post: |
| 11 | 13 |
# go |
| ... | ... |
@@ -45,9 +47,6 @@ dependencies: |
| 45 | 45 |
- > |
| 46 | 46 |
gvm use stable && |
| 47 | 47 |
go get github.com/axw/gocov/gocov github.com/golang/lint/golint |
| 48 |
- |
|
| 49 |
- # Disabling goveralls for now |
|
| 50 |
- # go get github.com/axw/gocov/gocov github.com/mattn/goveralls github.com/golang/lint/golint |
|
| 51 | 48 |
|
| 52 | 49 |
test: |
| 53 | 50 |
pre: |
| ... | ... |
@@ -73,25 +72,17 @@ test: |
| 73 | 73 |
pwd: $BASE_STABLE |
| 74 | 74 |
|
| 75 | 75 |
override: |
| 76 |
- |
|
| 77 | 76 |
# Test stable, and report |
| 78 |
- # Preset the goverall report file |
|
| 79 |
- # - echo "$CIRCLE_PAIN" > ~/goverage.report |
|
| 80 |
- |
|
| 81 |
- - gvm use stable; go list ./... | xargs -L 1 -I{} rm -f $GOPATH/src/{}/coverage.out:
|
|
| 82 |
- pwd: $BASE_STABLE |
|
| 83 |
- |
|
| 84 |
- - gvm use stable; go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/{}/coverage.out {}:
|
|
| 77 |
+ - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE':
|
|
| 85 | 78 |
timeout: 600 |
| 86 | 79 |
pwd: $BASE_STABLE |
| 87 | 80 |
|
| 88 | 81 |
post: |
| 89 |
- # Aggregate and report to coveralls |
|
| 90 |
- - gvm use stable; go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} cat "$GOPATH/src/{}/coverage.out" | grep -v "$CIRCLE_PAIN" >> ~/goverage.report:
|
|
| 82 |
+ # Report to codecov |
|
| 83 |
+ - bash <(curl -s https://codecov.io/bash): |
|
| 91 | 84 |
pwd: $BASE_STABLE |
| 92 | 85 |
|
| 93 | 86 |
## Notes |
| 94 |
- # Disabled coveralls reporting: build breaking sending coverage data to coveralls |
|
| 95 | 87 |
# Disabled the -race detector due to massive memory usage. |
| 96 | 88 |
# Do we want these as well? |
| 97 | 89 |
# - go get code.google.com/p/go.tools/cmd/goimports |
| 98 | 90 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,7 @@ |
| 0 |
+#!/usr/bin/env bash |
|
| 1 |
+# Given a subpackage and the containing package, figures out which packages |
|
| 2 |
+# need to be passed to `go test -coverpkg`: this includes all of the |
|
| 3 |
+# subpackage's dependencies within the containing package, as well as the |
|
| 4 |
+# subpackage itself. |
|
| 5 |
+DEPENDENCIES="$(go list -f $'{{range $f := .Deps}}{{$f}}\n{{end}}' ${1} | grep ${2})"
|
|
| 6 |
+echo "${1} ${DEPENDENCIES}" | xargs echo -n | tr ' ' ','
|
| ... | ... |
@@ -1,21 +1,14 @@ |
| 1 | 1 |
package digest |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"hash" |
| 7 | 6 |
"io" |
| 8 |
- "io/ioutil" |
|
| 9 | 7 |
"regexp" |
| 10 | 8 |
"strings" |
| 11 |
- |
|
| 12 |
- "github.com/docker/docker/pkg/tarsum" |
|
| 13 | 9 |
) |
| 14 | 10 |
|
| 15 | 11 |
const ( |
| 16 |
- // DigestTarSumV1EmptyTar is the digest for the empty tar file. |
|
| 17 |
- DigestTarSumV1EmptyTar = "tarsum.v1+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
|
| 18 |
- |
|
| 19 | 12 |
// DigestSha256EmptyTar is the canonical sha256 digest of empty data |
| 20 | 13 |
DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
| 21 | 14 |
) |
| ... | ... |
@@ -29,18 +22,21 @@ const ( |
| 29 | 29 |
// |
| 30 | 30 |
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc |
| 31 | 31 |
// |
| 32 |
-// More important for this code base, this type is compatible with tarsum |
|
| 33 |
-// digests. For example, the following would be a valid Digest: |
|
| 34 |
-// |
|
| 35 |
-// tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b |
|
| 36 |
-// |
|
| 37 | 32 |
// This allows to abstract the digest behind this type and work only in those |
| 38 | 33 |
// terms. |
| 39 | 34 |
type Digest string |
| 40 | 35 |
|
| 41 | 36 |
// NewDigest returns a Digest from alg and a hash.Hash object. |
| 42 | 37 |
func NewDigest(alg Algorithm, h hash.Hash) Digest {
|
| 43 |
- return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil)))
|
|
| 38 |
+ return NewDigestFromBytes(alg, h.Sum(nil)) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// NewDigestFromBytes returns a new digest from the byte contents of p. |
|
| 42 |
+// Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...) |
|
| 43 |
+// functions. This is also useful for rebuilding digests from binary |
|
| 44 |
+// serializations. |
|
| 45 |
+func NewDigestFromBytes(alg Algorithm, p []byte) Digest {
|
|
| 46 |
+ return Digest(fmt.Sprintf("%s:%x", alg, p))
|
|
| 44 | 47 |
} |
| 45 | 48 |
|
| 46 | 49 |
// NewDigestFromHex returns a Digest from alg and a the hex encoded digest. |
| ... | ... |
@@ -79,41 +75,15 @@ func FromReader(rd io.Reader) (Digest, error) {
|
| 79 | 79 |
return Canonical.FromReader(rd) |
| 80 | 80 |
} |
| 81 | 81 |
|
| 82 |
-// FromTarArchive produces a tarsum digest from reader rd. |
|
| 83 |
-func FromTarArchive(rd io.Reader) (Digest, error) {
|
|
| 84 |
- ts, err := tarsum.NewTarSum(rd, true, tarsum.Version1) |
|
| 85 |
- if err != nil {
|
|
| 86 |
- return "", err |
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- if _, err := io.Copy(ioutil.Discard, ts); err != nil {
|
|
| 90 |
- return "", err |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- d, err := ParseDigest(ts.Sum(nil)) |
|
| 94 |
- if err != nil {
|
|
| 95 |
- return "", err |
|
| 96 |
- } |
|
| 97 |
- |
|
| 98 |
- return d, nil |
|
| 99 |
-} |
|
| 100 |
- |
|
| 101 | 82 |
// FromBytes digests the input and returns a Digest. |
| 102 |
-func FromBytes(p []byte) (Digest, error) {
|
|
| 103 |
- return FromReader(bytes.NewReader(p)) |
|
| 83 |
+func FromBytes(p []byte) Digest {
|
|
| 84 |
+ return Canonical.FromBytes(p) |
|
| 104 | 85 |
} |
| 105 | 86 |
|
| 106 | 87 |
// Validate checks that the contents of d is a valid digest, returning an |
| 107 | 88 |
// error if not. |
| 108 | 89 |
func (d Digest) Validate() error {
|
| 109 | 90 |
s := string(d) |
| 110 |
- // Common case will be tarsum |
|
| 111 |
- _, err := ParseTarSum(s) |
|
| 112 |
- if err == nil {
|
|
| 113 |
- return nil |
|
| 114 |
- } |
|
| 115 |
- |
|
| 116 |
- // Continue on for general parser |
|
| 117 | 91 |
|
| 118 | 92 |
if !DigestRegexpAnchored.MatchString(s) {
|
| 119 | 93 |
return ErrDigestInvalidFormat |
| ... | ... |
@@ -2,6 +2,7 @@ package digest |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"crypto" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
"hash" |
| 6 | 7 |
"io" |
| 7 | 8 |
) |
| ... | ... |
@@ -13,10 +14,9 @@ type Algorithm string |
| 13 | 13 |
|
| 14 | 14 |
// supported digest types |
| 15 | 15 |
const ( |
| 16 |
- SHA256 Algorithm = "sha256" // sha256 with hex encoding |
|
| 17 |
- SHA384 Algorithm = "sha384" // sha384 with hex encoding |
|
| 18 |
- SHA512 Algorithm = "sha512" // sha512 with hex encoding |
|
| 19 |
- TarsumV1SHA256 Algorithm = "tarsum+v1+sha256" // supported tarsum version, verification only |
|
| 16 |
+ SHA256 Algorithm = "sha256" // sha256 with hex encoding |
|
| 17 |
+ SHA384 Algorithm = "sha384" // sha384 with hex encoding |
|
| 18 |
+ SHA512 Algorithm = "sha512" // sha512 with hex encoding |
|
| 20 | 19 |
|
| 21 | 20 |
// Canonical is the primary digest algorithm used with the distribution |
| 22 | 21 |
// project. Other digests may be used but this one is the primary storage |
| ... | ... |
@@ -85,11 +85,18 @@ func (a Algorithm) New() Digester {
|
| 85 | 85 |
} |
| 86 | 86 |
} |
| 87 | 87 |
|
| 88 |
-// Hash returns a new hash as used by the algorithm. If not available, nil is |
|
| 89 |
-// returned. Make sure to check Available before calling. |
|
| 88 |
+// Hash returns a new hash as used by the algorithm. If not available, the |
|
| 89 |
+// method will panic. Check Algorithm.Available() before calling. |
|
| 90 | 90 |
func (a Algorithm) Hash() hash.Hash {
|
| 91 | 91 |
if !a.Available() {
|
| 92 |
- return nil |
|
| 92 |
+ // NOTE(stevvooe): A missing hash is usually a programming error that |
|
| 93 |
+ // must be resolved at compile time. We don't import in the digest |
|
| 94 |
+ // package to allow users to choose their hash implementation (such as |
|
| 95 |
+ // when using stevvooe/resumable or a hardware accelerated package). |
|
| 96 |
+ // |
|
| 97 |
+ // Applications that may want to resolve the hash at runtime should |
|
| 98 |
+ // call Algorithm.Available before call Algorithm.Hash(). |
|
| 99 |
+ panic(fmt.Sprintf("%v not available (make sure it is imported)", a))
|
|
| 93 | 100 |
} |
| 94 | 101 |
|
| 95 | 102 |
return algorithms[a].New() |
| ... | ... |
@@ -106,6 +113,22 @@ func (a Algorithm) FromReader(rd io.Reader) (Digest, error) {
|
| 106 | 106 |
return digester.Digest(), nil |
| 107 | 107 |
} |
| 108 | 108 |
|
| 109 |
+// FromBytes digests the input and returns a Digest. |
|
| 110 |
+func (a Algorithm) FromBytes(p []byte) Digest {
|
|
| 111 |
+ digester := a.New() |
|
| 112 |
+ |
|
| 113 |
+ if _, err := digester.Hash().Write(p); err != nil {
|
|
| 114 |
+ // Writes to a Hash should never fail. None of the existing |
|
| 115 |
+ // hash implementations in the stdlib or hashes vendored |
|
| 116 |
+ // here can return errors from Write. Having a panic in this |
|
| 117 |
+ // condition instead of having FromBytes return an error value |
|
| 118 |
+ // avoids unnecessary error handling paths in all callers. |
|
| 119 |
+ panic("write to hash function returned error: " + err.Error())
|
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ return digester.Digest() |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 109 | 125 |
// TODO(stevvooe): Allow resolution of verifiers using the digest type and |
| 110 | 126 |
// this registration system. |
| 111 | 127 |
|
| ... | ... |
@@ -1,7 +1,7 @@ |
| 1 | 1 |
// Package digest provides a generalized type to opaquely represent message |
| 2 | 2 |
// digests and their operations within the registry. The Digest type is |
| 3 | 3 |
// designed to serve as a flexible identifier in a content-addressable system. |
| 4 |
-// More importantly, it provides tools and wrappers to work with tarsums and |
|
| 4 |
+// More importantly, it provides tools and wrappers to work with |
|
| 5 | 5 |
// hash.Hash-based digests with little effort. |
| 6 | 6 |
// |
| 7 | 7 |
// Basics |
| ... | ... |
@@ -16,17 +16,7 @@ |
| 16 | 16 |
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc |
| 17 | 17 |
// |
| 18 | 18 |
// In this case, the string "sha256" is the algorithm and the hex bytes are |
| 19 |
-// the "digest". A tarsum example will be more illustrative of the use case |
|
| 20 |
-// involved in the registry: |
|
| 21 |
-// |
|
| 22 |
-// tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b |
|
| 23 |
-// |
|
| 24 |
-// For this, we consider the algorithm to be "tarsum+sha256". Prudent |
|
| 25 |
-// applications will favor the ParseDigest function to verify the format over |
|
| 26 |
-// using simple type casts. However, a normal string can be cast as a digest |
|
| 27 |
-// with a simple type conversion: |
|
| 28 |
-// |
|
| 29 |
-// Digest("tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b")
|
|
| 19 |
+// the "digest". |
|
| 30 | 20 |
// |
| 31 | 21 |
// Because the Digest type is simply a string, once a valid Digest is |
| 32 | 22 |
// obtained, comparisons are cheap, quick and simple to express with the |
| 33 | 23 |
deleted file mode 100644 |
| ... | ... |
@@ -1,70 +0,0 @@ |
| 1 |
-package digest |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "fmt" |
|
| 5 |
- |
|
| 6 |
- "regexp" |
|
| 7 |
-) |
|
| 8 |
- |
|
| 9 |
-// TarsumRegexp defines a regular expression to match tarsum identifiers. |
|
| 10 |
-var TarsumRegexp = regexp.MustCompile("tarsum(?:.[a-z0-9]+)?\\+[a-zA-Z0-9]+:[A-Fa-f0-9]+")
|
|
| 11 |
- |
|
| 12 |
-// TarsumRegexpCapturing defines a regular expression to match tarsum identifiers with |
|
| 13 |
-// capture groups corresponding to each component. |
|
| 14 |
-var TarsumRegexpCapturing = regexp.MustCompile("(tarsum)(.([a-z0-9]+))?\\+([a-zA-Z0-9]+):([A-Fa-f0-9]+)")
|
|
| 15 |
- |
|
| 16 |
-// TarSumInfo contains information about a parsed tarsum. |
|
| 17 |
-type TarSumInfo struct {
|
|
| 18 |
- // Version contains the version of the tarsum. |
|
| 19 |
- Version string |
|
| 20 |
- |
|
| 21 |
- // Algorithm contains the algorithm for the final digest |
|
| 22 |
- Algorithm string |
|
| 23 |
- |
|
| 24 |
- // Digest contains the hex-encoded digest. |
|
| 25 |
- Digest string |
|
| 26 |
-} |
|
| 27 |
- |
|
| 28 |
-// InvalidTarSumError provides informations about a TarSum that cannot be parsed |
|
| 29 |
-// by ParseTarSum. |
|
| 30 |
-type InvalidTarSumError string |
|
| 31 |
- |
|
| 32 |
-func (e InvalidTarSumError) Error() string {
|
|
| 33 |
- return fmt.Sprintf("invalid tarsum: %q", string(e))
|
|
| 34 |
-} |
|
| 35 |
- |
|
| 36 |
-// ParseTarSum parses a tarsum string into its components of interest. For |
|
| 37 |
-// example, this method may receive the tarsum in the following format: |
|
| 38 |
-// |
|
| 39 |
-// tarsum.v1+sha256:220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e |
|
| 40 |
-// |
|
| 41 |
-// The function will return the following: |
|
| 42 |
-// |
|
| 43 |
-// TarSumInfo{
|
|
| 44 |
-// Version: "v1", |
|
| 45 |
-// Algorithm: "sha256", |
|
| 46 |
-// Digest: "220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e", |
|
| 47 |
-// } |
|
| 48 |
-// |
|
| 49 |
-func ParseTarSum(tarSum string) (tsi TarSumInfo, err error) {
|
|
| 50 |
- components := TarsumRegexpCapturing.FindStringSubmatch(tarSum) |
|
| 51 |
- |
|
| 52 |
- if len(components) != 1+TarsumRegexpCapturing.NumSubexp() {
|
|
| 53 |
- return TarSumInfo{}, InvalidTarSumError(tarSum)
|
|
| 54 |
- } |
|
| 55 |
- |
|
| 56 |
- return TarSumInfo{
|
|
| 57 |
- Version: components[3], |
|
| 58 |
- Algorithm: components[4], |
|
| 59 |
- Digest: components[5], |
|
| 60 |
- }, nil |
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-// String returns the valid, string representation of the tarsum info. |
|
| 64 |
-func (tsi TarSumInfo) String() string {
|
|
| 65 |
- if tsi.Version == "" {
|
|
| 66 |
- return fmt.Sprintf("tarsum+%s:%s", tsi.Algorithm, tsi.Digest)
|
|
| 67 |
- } |
|
| 68 |
- |
|
| 69 |
- return fmt.Sprintf("tarsum.%s+%s:%s", tsi.Version, tsi.Algorithm, tsi.Digest)
|
|
| 70 |
-} |
| ... | ... |
@@ -3,9 +3,6 @@ package digest |
| 3 | 3 |
import ( |
| 4 | 4 |
"hash" |
| 5 | 5 |
"io" |
| 6 |
- "io/ioutil" |
|
| 7 |
- |
|
| 8 |
- "github.com/docker/docker/pkg/tarsum" |
|
| 9 | 6 |
) |
| 10 | 7 |
|
| 11 | 8 |
// Verifier presents a general verification interface to be used with message |
| ... | ... |
@@ -27,70 +24,10 @@ func NewDigestVerifier(d Digest) (Verifier, error) {
|
| 27 | 27 |
return nil, err |
| 28 | 28 |
} |
| 29 | 29 |
|
| 30 |
- alg := d.Algorithm() |
|
| 31 |
- switch alg {
|
|
| 32 |
- case "sha256", "sha384", "sha512": |
|
| 33 |
- return hashVerifier{
|
|
| 34 |
- hash: alg.Hash(), |
|
| 35 |
- digest: d, |
|
| 36 |
- }, nil |
|
| 37 |
- default: |
|
| 38 |
- // Assume we have a tarsum. |
|
| 39 |
- version, err := tarsum.GetVersionFromTarsum(string(d)) |
|
| 40 |
- if err != nil {
|
|
| 41 |
- return nil, err |
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- pr, pw := io.Pipe() |
|
| 45 |
- |
|
| 46 |
- // TODO(stevvooe): We may actually want to ban the earlier versions of |
|
| 47 |
- // tarsum. That decision may not be the place of the verifier. |
|
| 48 |
- |
|
| 49 |
- ts, err := tarsum.NewTarSum(pr, true, version) |
|
| 50 |
- if err != nil {
|
|
| 51 |
- return nil, err |
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- // TODO(sday): Ick! A goroutine per digest verification? We'll have to |
|
| 55 |
- // get the tarsum library to export an io.Writer variant. |
|
| 56 |
- go func() {
|
|
| 57 |
- if _, err := io.Copy(ioutil.Discard, ts); err != nil {
|
|
| 58 |
- pr.CloseWithError(err) |
|
| 59 |
- } else {
|
|
| 60 |
- pr.Close() |
|
| 61 |
- } |
|
| 62 |
- }() |
|
| 63 |
- |
|
| 64 |
- return &tarsumVerifier{
|
|
| 65 |
- digest: d, |
|
| 66 |
- ts: ts, |
|
| 67 |
- pr: pr, |
|
| 68 |
- pw: pw, |
|
| 69 |
- }, nil |
|
| 70 |
- } |
|
| 71 |
-} |
|
| 72 |
- |
|
| 73 |
-// NewLengthVerifier returns a verifier that returns true when the number of |
|
| 74 |
-// read bytes equals the expected parameter. |
|
| 75 |
-func NewLengthVerifier(expected int64) Verifier {
|
|
| 76 |
- return &lengthVerifier{
|
|
| 77 |
- expected: expected, |
|
| 78 |
- } |
|
| 79 |
-} |
|
| 80 |
- |
|
| 81 |
-type lengthVerifier struct {
|
|
| 82 |
- expected int64 // expected bytes read |
|
| 83 |
- len int64 // bytes read |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func (lv *lengthVerifier) Write(p []byte) (n int, err error) {
|
|
| 87 |
- n = len(p) |
|
| 88 |
- lv.len += int64(n) |
|
| 89 |
- return n, err |
|
| 90 |
-} |
|
| 91 |
- |
|
| 92 |
-func (lv *lengthVerifier) Verified() bool {
|
|
| 93 |
- return lv.expected == lv.len |
|
| 30 |
+ return hashVerifier{
|
|
| 31 |
+ hash: d.Algorithm().Hash(), |
|
| 32 |
+ digest: d, |
|
| 33 |
+ }, nil |
|
| 94 | 34 |
} |
| 95 | 35 |
|
| 96 | 36 |
type hashVerifier struct {
|
| ... | ... |
@@ -105,18 +42,3 @@ func (hv hashVerifier) Write(p []byte) (n int, err error) {
|
| 105 | 105 |
func (hv hashVerifier) Verified() bool {
|
| 106 | 106 |
return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash) |
| 107 | 107 |
} |
| 108 |
- |
|
| 109 |
-type tarsumVerifier struct {
|
|
| 110 |
- digest Digest |
|
| 111 |
- ts tarsum.TarSum |
|
| 112 |
- pr *io.PipeReader |
|
| 113 |
- pw *io.PipeWriter |
|
| 114 |
-} |
|
| 115 |
- |
|
| 116 |
-func (tv *tarsumVerifier) Write(p []byte) (n int, err error) {
|
|
| 117 |
- return tv.pw.Write(p) |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-func (tv *tarsumVerifier) Verified() bool {
|
|
| 121 |
- return tv.digest == Digest(tv.ts.Sum(nil)) |
|
| 122 |
-} |
| ... | ... |
@@ -16,6 +16,15 @@ var ErrManifestNotModified = errors.New("manifest not modified")
|
| 16 | 16 |
// performed |
| 17 | 17 |
var ErrUnsupported = errors.New("operation unsupported")
|
| 18 | 18 |
|
| 19 |
+// ErrTagUnknown is returned if the given tag is not known by the tag service |
|
| 20 |
+type ErrTagUnknown struct {
|
|
| 21 |
+ Tag string |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (err ErrTagUnknown) Error() string {
|
|
| 25 |
+ return fmt.Sprintf("unknown tag=%s", err.Tag)
|
|
| 26 |
+} |
|
| 27 |
+ |
|
| 19 | 28 |
// ErrRepositoryUnknown is returned if the named repository is not known by |
| 20 | 29 |
// the registry. |
| 21 | 30 |
type ErrRepositoryUnknown struct {
|
| 22 | 31 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,147 @@ |
| 0 |
+package manifestlist |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// MediaTypeManifestList specifies the mediaType for manifest lists. |
|
| 13 |
+const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" |
|
| 14 |
+ |
|
| 15 |
+// SchemaVersion provides a pre-initialized version structure for this |
|
| 16 |
+// packages version of the manifest. |
|
| 17 |
+var SchemaVersion = manifest.Versioned{
|
|
| 18 |
+ SchemaVersion: 2, |
|
| 19 |
+ MediaType: MediaTypeManifestList, |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func init() {
|
|
| 23 |
+ manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 24 |
+ m := new(DeserializedManifestList) |
|
| 25 |
+ err := m.UnmarshalJSON(b) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, distribution.Descriptor{}, err
|
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ dgst := digest.FromBytes(b) |
|
| 31 |
+ return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err
|
|
| 32 |
+ } |
|
| 33 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) |
|
| 34 |
+ if err != nil {
|
|
| 35 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 36 |
+ } |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// PlatformSpec specifies a platform where a particular image manifest is |
|
| 40 |
+// applicable. |
|
| 41 |
+type PlatformSpec struct {
|
|
| 42 |
+ // Architecture field specifies the CPU architecture, for example |
|
| 43 |
+ // `amd64` or `ppc64`. |
|
| 44 |
+ Architecture string `json:"architecture"` |
|
| 45 |
+ |
|
| 46 |
+ // OS specifies the operating system, for example `linux` or `windows`. |
|
| 47 |
+ OS string `json:"os"` |
|
| 48 |
+ |
|
| 49 |
+ // Variant is an optional field specifying a variant of the CPU, for |
|
| 50 |
+ // example `ppc64le` to specify a little-endian version of a PowerPC CPU. |
|
| 51 |
+ Variant string `json:"variant,omitempty"` |
|
| 52 |
+ |
|
| 53 |
+ // Features is an optional field specifuing an array of strings, each |
|
| 54 |
+ // listing a required CPU feature (for example `sse4` or `aes`). |
|
| 55 |
+ Features []string `json:"features,omitempty"` |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+// A ManifestDescriptor references a platform-specific manifest. |
|
| 59 |
+type ManifestDescriptor struct {
|
|
| 60 |
+ distribution.Descriptor |
|
| 61 |
+ |
|
| 62 |
+ // Platform specifies which platform the manifest pointed to by the |
|
| 63 |
+ // descriptor runs on. |
|
| 64 |
+ Platform PlatformSpec `json:"platform"` |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+// ManifestList references manifests for various platforms. |
|
| 68 |
+type ManifestList struct {
|
|
| 69 |
+ manifest.Versioned |
|
| 70 |
+ |
|
| 71 |
+ // Config references the image configuration as a blob. |
|
| 72 |
+ Manifests []ManifestDescriptor `json:"manifests"` |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// References returnes the distribution descriptors for the referenced image |
|
| 76 |
+// manifests. |
|
| 77 |
+func (m ManifestList) References() []distribution.Descriptor {
|
|
| 78 |
+ dependencies := make([]distribution.Descriptor, len(m.Manifests)) |
|
| 79 |
+ for i := range m.Manifests {
|
|
| 80 |
+ dependencies[i] = m.Manifests[i].Descriptor |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ return dependencies |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+// DeserializedManifestList wraps ManifestList with a copy of the original |
|
| 87 |
+// JSON. |
|
| 88 |
+type DeserializedManifestList struct {
|
|
| 89 |
+ ManifestList |
|
| 90 |
+ |
|
| 91 |
+ // canonical is the canonical byte representation of the Manifest. |
|
| 92 |
+ canonical []byte |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// FromDescriptors takes a slice of descriptors, and returns a |
|
| 96 |
+// DeserializedManifestList which contains the resulting manifest list |
|
| 97 |
+// and its JSON representation. |
|
| 98 |
+func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
|
|
| 99 |
+ m := ManifestList{
|
|
| 100 |
+ Versioned: SchemaVersion, |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) |
|
| 104 |
+ copy(m.Manifests, descriptors) |
|
| 105 |
+ |
|
| 106 |
+ deserialized := DeserializedManifestList{
|
|
| 107 |
+ ManifestList: m, |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ var err error |
|
| 111 |
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ") |
|
| 112 |
+ return &deserialized, err |
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+// UnmarshalJSON populates a new ManifestList struct from JSON data. |
|
| 116 |
+func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error {
|
|
| 117 |
+ m.canonical = make([]byte, len(b), len(b)) |
|
| 118 |
+ // store manifest list in canonical |
|
| 119 |
+ copy(m.canonical, b) |
|
| 120 |
+ |
|
| 121 |
+ // Unmarshal canonical JSON into ManifestList object |
|
| 122 |
+ var manifestList ManifestList |
|
| 123 |
+ if err := json.Unmarshal(m.canonical, &manifestList); err != nil {
|
|
| 124 |
+ return err |
|
| 125 |
+ } |
|
| 126 |
+ |
|
| 127 |
+ m.ManifestList = manifestList |
|
| 128 |
+ |
|
| 129 |
+ return nil |
|
| 130 |
+} |
|
| 131 |
+ |
|
| 132 |
+// MarshalJSON returns the contents of canonical. If canonical is empty, |
|
| 133 |
+// marshals the inner contents. |
|
| 134 |
+func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
|
|
| 135 |
+ if len(m.canonical) > 0 {
|
|
| 136 |
+ return m.canonical, nil |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ return nil, errors.New("JSON representation not initialized in DeserializedManifestList")
|
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+// Payload returns the raw content of the manifest list. The contents can be |
|
| 143 |
+// used to calculate the content identifier. |
|
| 144 |
+func (m DeserializedManifestList) Payload() (string, []byte, error) {
|
|
| 145 |
+ return m.MediaType, m.canonical, nil |
|
| 146 |
+} |
| 0 | 147 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,278 @@ |
| 0 |
+package schema1 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "crypto/sha512" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/distribution" |
|
| 10 |
+ "github.com/docker/distribution/context" |
|
| 11 |
+ "github.com/docker/libtrust" |
|
| 12 |
+ |
|
| 13 |
+ "github.com/docker/distribution/digest" |
|
| 14 |
+ "github.com/docker/distribution/manifest" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+type diffID digest.Digest |
|
| 18 |
+ |
|
| 19 |
+// gzippedEmptyTar is a gzip-compressed version of an empty tar file |
|
| 20 |
+// (1024 NULL bytes) |
|
| 21 |
+var gzippedEmptyTar = []byte{
|
|
| 22 |
+ 31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88, |
|
| 23 |
+ 0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0, |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// digestSHA256GzippedEmptyTar is the canonical sha256 digest of |
|
| 27 |
+// gzippedEmptyTar |
|
| 28 |
+const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
|
|
| 29 |
+ |
|
| 30 |
+// configManifestBuilder is a type for constructing manifests from an image |
|
| 31 |
+// configuration and generic descriptors. |
|
| 32 |
+type configManifestBuilder struct {
|
|
| 33 |
+ // bs is a BlobService used to create empty layer tars in the |
|
| 34 |
+ // blob store if necessary. |
|
| 35 |
+ bs distribution.BlobService |
|
| 36 |
+ // pk is the libtrust private key used to sign the final manifest. |
|
| 37 |
+ pk libtrust.PrivateKey |
|
| 38 |
+ // configJSON is configuration supplied when the ManifestBuilder was |
|
| 39 |
+ // created. |
|
| 40 |
+ configJSON []byte |
|
| 41 |
+ // name is the name provided to NewConfigManifestBuilder |
|
| 42 |
+ name string |
|
| 43 |
+ // tag is the tag provided to NewConfigManifestBuilder |
|
| 44 |
+ tag string |
|
| 45 |
+ // descriptors is the set of descriptors referencing the layers. |
|
| 46 |
+ descriptors []distribution.Descriptor |
|
| 47 |
+ // emptyTarDigest is set to a valid digest if an empty tar has been |
|
| 48 |
+ // put in the blob store; otherwise it is empty. |
|
| 49 |
+ emptyTarDigest digest.Digest |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+// NewConfigManifestBuilder is used to build new manifests for the current |
|
| 53 |
+// schema version from an image configuration and a set of descriptors. |
|
| 54 |
+// It takes a BlobService so that it can add an empty tar to the blob store |
|
| 55 |
+// if the resulting manifest needs empty layers. |
|
| 56 |
+func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, name, tag string, configJSON []byte) distribution.ManifestBuilder {
|
|
| 57 |
+ return &configManifestBuilder{
|
|
| 58 |
+ bs: bs, |
|
| 59 |
+ pk: pk, |
|
| 60 |
+ configJSON: configJSON, |
|
| 61 |
+ name: name, |
|
| 62 |
+ tag: tag, |
|
| 63 |
+ } |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+// Build produces a final manifest from the given references |
|
| 67 |
+func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
|
|
| 68 |
+ type imageRootFS struct {
|
|
| 69 |
+ Type string `json:"type"` |
|
| 70 |
+ DiffIDs []diffID `json:"diff_ids,omitempty"` |
|
| 71 |
+ BaseLayer string `json:"base_layer,omitempty"` |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ type imageHistory struct {
|
|
| 75 |
+ Created time.Time `json:"created"` |
|
| 76 |
+ Author string `json:"author,omitempty"` |
|
| 77 |
+ CreatedBy string `json:"created_by,omitempty"` |
|
| 78 |
+ Comment string `json:"comment,omitempty"` |
|
| 79 |
+ EmptyLayer bool `json:"empty_layer,omitempty"` |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ type imageConfig struct {
|
|
| 83 |
+ RootFS *imageRootFS `json:"rootfs,omitempty"` |
|
| 84 |
+ History []imageHistory `json:"history,omitempty"` |
|
| 85 |
+ Architecture string `json:"architecture,omitempty"` |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ var img imageConfig |
|
| 89 |
+ |
|
| 90 |
+ if err := json.Unmarshal(mb.configJSON, &img); err != nil {
|
|
| 91 |
+ return nil, err |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ if len(img.History) == 0 {
|
|
| 95 |
+ return nil, errors.New("empty history when trying to create schema1 manifest")
|
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
|
|
| 99 |
+ return nil, errors.New("number of descriptors and number of layers in rootfs must match")
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ // Generate IDs for each layer |
|
| 103 |
+ // For non-top-level layers, create fake V1Compatibility strings that |
|
| 104 |
+ // fit the format and don't collide with anything else, but don't |
|
| 105 |
+ // result in runnable images on their own. |
|
| 106 |
+ type v1Compatibility struct {
|
|
| 107 |
+ ID string `json:"id"` |
|
| 108 |
+ Parent string `json:"parent,omitempty"` |
|
| 109 |
+ Comment string `json:"comment,omitempty"` |
|
| 110 |
+ Created time.Time `json:"created"` |
|
| 111 |
+ ContainerConfig struct {
|
|
| 112 |
+ Cmd []string |
|
| 113 |
+ } `json:"container_config,omitempty"` |
|
| 114 |
+ ThrowAway bool `json:"throwaway,omitempty"` |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ fsLayerList := make([]FSLayer, len(img.History)) |
|
| 118 |
+ history := make([]History, len(img.History)) |
|
| 119 |
+ |
|
| 120 |
+ parent := "" |
|
| 121 |
+ layerCounter := 0 |
|
| 122 |
+ for i, h := range img.History[:len(img.History)-1] {
|
|
| 123 |
+ var blobsum digest.Digest |
|
| 124 |
+ if h.EmptyLayer {
|
|
| 125 |
+ if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
| 126 |
+ return nil, err |
|
| 127 |
+ } |
|
| 128 |
+ } else {
|
|
| 129 |
+ if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 130 |
+ return nil, errors.New("too many non-empty layers in History section")
|
|
| 131 |
+ } |
|
| 132 |
+ blobsum = mb.descriptors[layerCounter].Digest |
|
| 133 |
+ layerCounter++ |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex() |
|
| 137 |
+ |
|
| 138 |
+ if i == 0 && img.RootFS.BaseLayer != "" {
|
|
| 139 |
+ // windows-only baselayer setup |
|
| 140 |
+ baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer)) |
|
| 141 |
+ parent = fmt.Sprintf("%x", baseID[:32])
|
|
| 142 |
+ } |
|
| 143 |
+ |
|
| 144 |
+ v1Compatibility := v1Compatibility{
|
|
| 145 |
+ ID: v1ID, |
|
| 146 |
+ Parent: parent, |
|
| 147 |
+ Comment: h.Comment, |
|
| 148 |
+ Created: h.Created, |
|
| 149 |
+ } |
|
| 150 |
+ v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
|
| 151 |
+ if h.EmptyLayer {
|
|
| 152 |
+ v1Compatibility.ThrowAway = true |
|
| 153 |
+ } |
|
| 154 |
+ jsonBytes, err := json.Marshal(&v1Compatibility) |
|
| 155 |
+ if err != nil {
|
|
| 156 |
+ return nil, err |
|
| 157 |
+ } |
|
| 158 |
+ |
|
| 159 |
+ reversedIndex := len(img.History) - i - 1 |
|
| 160 |
+ history[reversedIndex].V1Compatibility = string(jsonBytes) |
|
| 161 |
+ fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
|
|
| 162 |
+ |
|
| 163 |
+ parent = v1ID |
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ latestHistory := img.History[len(img.History)-1] |
|
| 167 |
+ |
|
| 168 |
+ var blobsum digest.Digest |
|
| 169 |
+ if latestHistory.EmptyLayer {
|
|
| 170 |
+ if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
| 171 |
+ return nil, err |
|
| 172 |
+ } |
|
| 173 |
+ } else {
|
|
| 174 |
+ if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 175 |
+ return nil, errors.New("too many non-empty layers in History section")
|
|
| 176 |
+ } |
|
| 177 |
+ blobsum = mb.descriptors[layerCounter].Digest |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ fsLayerList[0] = FSLayer{BlobSum: blobsum}
|
|
| 181 |
+ dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON))) |
|
| 182 |
+ |
|
| 183 |
+ // Top-level v1compatibility string should be a modified version of the |
|
| 184 |
+ // image config. |
|
| 185 |
+ transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer) |
|
| 186 |
+ if err != nil {
|
|
| 187 |
+ return nil, err |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ history[0].V1Compatibility = string(transformedConfig) |
|
| 191 |
+ |
|
| 192 |
+ mfst := Manifest{
|
|
| 193 |
+ Versioned: manifest.Versioned{
|
|
| 194 |
+ SchemaVersion: 1, |
|
| 195 |
+ }, |
|
| 196 |
+ Name: mb.name, |
|
| 197 |
+ Tag: mb.tag, |
|
| 198 |
+ Architecture: img.Architecture, |
|
| 199 |
+ FSLayers: fsLayerList, |
|
| 200 |
+ History: history, |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ return Sign(&mfst, mb.pk) |
|
| 204 |
+} |
|
| 205 |
+ |
|
| 206 |
+// emptyTar pushes a compressed empty tar to the blob store if one doesn't |
|
| 207 |
+// already exist, and returns its blobsum. |
|
| 208 |
+func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
|
|
| 209 |
+ if mb.emptyTarDigest != "" {
|
|
| 210 |
+ // Already put an empty tar |
|
| 211 |
+ return mb.emptyTarDigest, nil |
|
| 212 |
+ } |
|
| 213 |
+ |
|
| 214 |
+ descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar) |
|
| 215 |
+ switch err {
|
|
| 216 |
+ case nil: |
|
| 217 |
+ mb.emptyTarDigest = descriptor.Digest |
|
| 218 |
+ return descriptor.Digest, nil |
|
| 219 |
+ case distribution.ErrBlobUnknown: |
|
| 220 |
+ // nop |
|
| 221 |
+ default: |
|
| 222 |
+ return "", err |
|
| 223 |
+ } |
|
| 224 |
+ |
|
| 225 |
+ // Add gzipped empty tar to the blob store |
|
| 226 |
+ descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar) |
|
| 227 |
+ if err != nil {
|
|
| 228 |
+ return "", err |
|
| 229 |
+ } |
|
| 230 |
+ |
|
| 231 |
+ mb.emptyTarDigest = descriptor.Digest |
|
| 232 |
+ |
|
| 233 |
+ return descriptor.Digest, nil |
|
| 234 |
+} |
|
| 235 |
+ |
|
| 236 |
+// AppendReference adds a reference to the current ManifestBuilder |
|
| 237 |
+func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
| 238 |
+ // todo: verification here? |
|
| 239 |
+ mb.descriptors = append(mb.descriptors, d.Descriptor()) |
|
| 240 |
+ return nil |
|
| 241 |
+} |
|
| 242 |
+ |
|
| 243 |
+// References returns the current references added to this builder |
|
| 244 |
+func (mb *configManifestBuilder) References() []distribution.Descriptor {
|
|
| 245 |
+ return mb.descriptors |
|
| 246 |
+} |
|
| 247 |
+ |
|
| 248 |
+// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON |
|
| 249 |
+func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
|
|
| 250 |
+ // Top-level v1compatibility string should be a modified version of the |
|
| 251 |
+ // image config. |
|
| 252 |
+ var configAsMap map[string]*json.RawMessage |
|
| 253 |
+ if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
|
|
| 254 |
+ return nil, err |
|
| 255 |
+ } |
|
| 256 |
+ |
|
| 257 |
+ // Delete fields that didn't exist in old manifest |
|
| 258 |
+ delete(configAsMap, "rootfs") |
|
| 259 |
+ delete(configAsMap, "history") |
|
| 260 |
+ configAsMap["id"] = rawJSON(v1ID) |
|
| 261 |
+ if parentV1ID != "" {
|
|
| 262 |
+ configAsMap["parent"] = rawJSON(parentV1ID) |
|
| 263 |
+ } |
|
| 264 |
+ if throwaway {
|
|
| 265 |
+ configAsMap["throwaway"] = rawJSON(true) |
|
| 266 |
+ } |
|
| 267 |
+ |
|
| 268 |
+ return json.Marshal(configAsMap) |
|
| 269 |
+} |
|
| 270 |
+ |
|
| 271 |
+func rawJSON(value interface{}) *json.RawMessage {
|
|
| 272 |
+ jsonval, err := json.Marshal(value) |
|
| 273 |
+ if err != nil {
|
|
| 274 |
+ return nil |
|
| 275 |
+ } |
|
| 276 |
+ return (*json.RawMessage)(&jsonval) |
|
| 277 |
+} |
| ... | ... |
@@ -2,20 +2,22 @@ package schema1 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 6 | 8 |
"github.com/docker/distribution/digest" |
| 7 | 9 |
"github.com/docker/distribution/manifest" |
| 8 | 10 |
"github.com/docker/libtrust" |
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 |
-// TODO(stevvooe): When we rev the manifest format, the contents of this |
|
| 12 |
-// package should be moved to manifest/v1. |
|
| 13 |
- |
|
| 14 | 13 |
const ( |
| 15 |
- // ManifestMediaType specifies the mediaType for the current version. Note |
|
| 16 |
- // that for schema version 1, the the media is optionally |
|
| 17 |
- // "application/json". |
|
| 18 |
- ManifestMediaType = "application/vnd.docker.distribution.manifest.v1+json" |
|
| 14 |
+ // MediaTypeManifest specifies the mediaType for the current version. Note |
|
| 15 |
+ // that for schema version 1, the the media is optionally "application/json". |
|
| 16 |
+ MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json" |
|
| 17 |
+ // MediaTypeSignedManifest specifies the mediatype for current SignedManifest version |
|
| 18 |
+ MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" |
|
| 19 |
+ // MediaTypeManifestLayer specifies the media type for manifest layers |
|
| 20 |
+ MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar" |
|
| 19 | 21 |
) |
| 20 | 22 |
|
| 21 | 23 |
var ( |
| ... | ... |
@@ -26,6 +28,47 @@ var ( |
| 26 | 26 |
} |
| 27 | 27 |
) |
| 28 | 28 |
|
| 29 |
+func init() {
|
|
| 30 |
+ schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 31 |
+ sm := new(SignedManifest) |
|
| 32 |
+ err := sm.UnmarshalJSON(b) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return nil, distribution.Descriptor{}, err
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ desc := distribution.Descriptor{
|
|
| 38 |
+ Digest: digest.FromBytes(sm.Canonical), |
|
| 39 |
+ Size: int64(len(sm.Canonical)), |
|
| 40 |
+ MediaType: MediaTypeManifest, |
|
| 41 |
+ } |
|
| 42 |
+ return sm, desc, err |
|
| 43 |
+ } |
|
| 44 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifest, schema1Func) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 47 |
+ } |
|
| 48 |
+ err = distribution.RegisterManifestSchema("", schema1Func)
|
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 51 |
+ } |
|
| 52 |
+ err = distribution.RegisterManifestSchema("application/json; charset=utf-8", schema1Func)
|
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 55 |
+ } |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+// FSLayer is a container struct for BlobSums defined in an image manifest |
|
| 59 |
+type FSLayer struct {
|
|
| 60 |
+ // BlobSum is the tarsum of the referenced filesystem image layer |
|
| 61 |
+ BlobSum digest.Digest `json:"blobSum"` |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// History stores unstructured v1 compatibility information |
|
| 65 |
+type History struct {
|
|
| 66 |
+ // V1Compatibility is the raw v1 compatibility information |
|
| 67 |
+ V1Compatibility string `json:"v1Compatibility"` |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 29 | 70 |
// Manifest provides the base accessible fields for working with V2 image |
| 30 | 71 |
// format in the registry. |
| 31 | 72 |
type Manifest struct {
|
| ... | ... |
@@ -49,59 +92,64 @@ type Manifest struct {
|
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 | 51 |
// SignedManifest provides an envelope for a signed image manifest, including |
| 52 |
-// the format sensitive raw bytes. It contains fields to |
|
| 52 |
+// the format sensitive raw bytes. |
|
| 53 | 53 |
type SignedManifest struct {
|
| 54 | 54 |
Manifest |
| 55 | 55 |
|
| 56 |
- // Raw is the byte representation of the ImageManifest, used for signature |
|
| 57 |
- // verification. The value of Raw must be used directly during |
|
| 58 |
- // serialization, or the signature check will fail. The manifest byte |
|
| 56 |
+ // Canonical is the canonical byte representation of the ImageManifest, |
|
| 57 |
+ // without any attached signatures. The manifest byte |
|
| 59 | 58 |
// representation cannot change or it will have to be re-signed. |
| 60 |
- Raw []byte `json:"-"` |
|
| 59 |
+ Canonical []byte `json:"-"` |
|
| 60 |
+ |
|
| 61 |
+ // all contains the byte representation of the Manifest including signatures |
|
| 62 |
+ // and is retuend by Payload() |
|
| 63 |
+ all []byte |
|
| 61 | 64 |
} |
| 62 | 65 |
|
| 63 |
-// UnmarshalJSON populates a new ImageManifest struct from JSON data. |
|
| 66 |
+// UnmarshalJSON populates a new SignedManifest struct from JSON data. |
|
| 64 | 67 |
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
|
| 65 |
- sm.Raw = make([]byte, len(b), len(b)) |
|
| 66 |
- copy(sm.Raw, b) |
|
| 68 |
+ sm.all = make([]byte, len(b), len(b)) |
|
| 69 |
+ // store manifest and signatures in all |
|
| 70 |
+ copy(sm.all, b) |
|
| 67 | 71 |
|
| 68 |
- p, err := sm.Payload() |
|
| 72 |
+ jsig, err := libtrust.ParsePrettySignature(b, "signatures") |
|
| 69 | 73 |
if err != nil {
|
| 70 | 74 |
return err |
| 71 | 75 |
} |
| 72 | 76 |
|
| 77 |
+ // Resolve the payload in the manifest. |
|
| 78 |
+ bytes, err := jsig.Payload() |
|
| 79 |
+ if err != nil {
|
|
| 80 |
+ return err |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ // sm.Canonical stores the canonical manifest JSON |
|
| 84 |
+ sm.Canonical = make([]byte, len(bytes), len(bytes)) |
|
| 85 |
+ copy(sm.Canonical, bytes) |
|
| 86 |
+ |
|
| 87 |
+ // Unmarshal canonical JSON into Manifest object |
|
| 73 | 88 |
var manifest Manifest |
| 74 |
- if err := json.Unmarshal(p, &manifest); err != nil {
|
|
| 89 |
+ if err := json.Unmarshal(sm.Canonical, &manifest); err != nil {
|
|
| 75 | 90 |
return err |
| 76 | 91 |
} |
| 77 | 92 |
|
| 78 | 93 |
sm.Manifest = manifest |
| 94 |
+ |
|
| 79 | 95 |
return nil |
| 80 | 96 |
} |
| 81 | 97 |
|
| 82 |
-// Payload returns the raw, signed content of the signed manifest. The |
|
| 83 |
-// contents can be used to calculate the content identifier. |
|
| 84 |
-func (sm *SignedManifest) Payload() ([]byte, error) {
|
|
| 85 |
- jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 86 |
- if err != nil {
|
|
| 87 |
- return nil, err |
|
| 98 |
+// References returnes the descriptors of this manifests references |
|
| 99 |
+func (sm SignedManifest) References() []distribution.Descriptor {
|
|
| 100 |
+ dependencies := make([]distribution.Descriptor, len(sm.FSLayers)) |
|
| 101 |
+ for i, fsLayer := range sm.FSLayers {
|
|
| 102 |
+ dependencies[i] = distribution.Descriptor{
|
|
| 103 |
+ MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar", |
|
| 104 |
+ Digest: fsLayer.BlobSum, |
|
| 105 |
+ } |
|
| 88 | 106 |
} |
| 89 | 107 |
|
| 90 |
- // Resolve the payload in the manifest. |
|
| 91 |
- return jsig.Payload() |
|
| 92 |
-} |
|
| 93 |
- |
|
| 94 |
-// Signatures returns the signatures as provided by |
|
| 95 |
-// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws |
|
| 96 |
-// signatures. |
|
| 97 |
-func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
|
| 98 |
- jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 99 |
- if err != nil {
|
|
| 100 |
- return nil, err |
|
| 101 |
- } |
|
| 108 |
+ return dependencies |
|
| 102 | 109 |
|
| 103 |
- // Resolve the payload in the manifest. |
|
| 104 |
- return jsig.Signatures() |
|
| 105 | 110 |
} |
| 106 | 111 |
|
| 107 | 112 |
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner |
| ... | ... |
@@ -109,22 +157,28 @@ func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
| 109 | 109 |
// use Raw directly, since the the content produced by json.Marshal will be |
| 110 | 110 |
// compacted and will fail signature checks. |
| 111 | 111 |
func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
|
| 112 |
- if len(sm.Raw) > 0 {
|
|
| 113 |
- return sm.Raw, nil |
|
| 112 |
+ if len(sm.all) > 0 {
|
|
| 113 |
+ return sm.all, nil |
|
| 114 | 114 |
} |
| 115 | 115 |
|
| 116 | 116 |
// If the raw data is not available, just dump the inner content. |
| 117 | 117 |
return json.Marshal(&sm.Manifest) |
| 118 | 118 |
} |
| 119 | 119 |
|
| 120 |
-// FSLayer is a container struct for BlobSums defined in an image manifest |
|
| 121 |
-type FSLayer struct {
|
|
| 122 |
- // BlobSum is the tarsum of the referenced filesystem image layer |
|
| 123 |
- BlobSum digest.Digest `json:"blobSum"` |
|
| 120 |
+// Payload returns the signed content of the signed manifest. |
|
| 121 |
+func (sm SignedManifest) Payload() (string, []byte, error) {
|
|
| 122 |
+ return MediaTypeManifest, sm.all, nil |
|
| 124 | 123 |
} |
| 125 | 124 |
|
| 126 |
-// History stores unstructured v1 compatibility information |
|
| 127 |
-type History struct {
|
|
| 128 |
- // V1Compatibility is the raw v1 compatibility information |
|
| 129 |
- V1Compatibility string `json:"v1Compatibility"` |
|
| 125 |
+// Signatures returns the signatures as provided by |
|
| 126 |
+// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws |
|
| 127 |
+// signatures. |
|
| 128 |
+func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
|
| 129 |
+ jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 130 |
+ if err != nil {
|
|
| 131 |
+ return nil, err |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ // Resolve the payload in the manifest. |
|
| 135 |
+ return jsig.Signatures() |
|
| 130 | 136 |
} |
| 131 | 137 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package schema1 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "errors" |
|
| 6 |
+ "github.com/docker/distribution" |
|
| 7 |
+ "github.com/docker/distribution/context" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+ "github.com/docker/libtrust" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// referenceManifestBuilder is a type for constructing manifests from schema1 |
|
| 14 |
+// dependencies. |
|
| 15 |
+type referenceManifestBuilder struct {
|
|
| 16 |
+ Manifest |
|
| 17 |
+ pk libtrust.PrivateKey |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// NewReferenceManifestBuilder is used to build new manifests for the current |
|
| 21 |
+// schema version using schema1 dependencies. |
|
| 22 |
+func NewReferenceManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) distribution.ManifestBuilder {
|
|
| 23 |
+ return &referenceManifestBuilder{
|
|
| 24 |
+ Manifest: Manifest{
|
|
| 25 |
+ Versioned: manifest.Versioned{
|
|
| 26 |
+ SchemaVersion: 1, |
|
| 27 |
+ }, |
|
| 28 |
+ Name: name, |
|
| 29 |
+ Tag: tag, |
|
| 30 |
+ Architecture: architecture, |
|
| 31 |
+ }, |
|
| 32 |
+ pk: pk, |
|
| 33 |
+ } |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) {
|
|
| 37 |
+ m := mb.Manifest |
|
| 38 |
+ if len(m.FSLayers) == 0 {
|
|
| 39 |
+ return nil, errors.New("cannot build manifest with zero layers or history")
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers)) |
|
| 43 |
+ m.History = make([]History, len(mb.Manifest.History)) |
|
| 44 |
+ copy(m.FSLayers, mb.Manifest.FSLayers) |
|
| 45 |
+ copy(m.History, mb.Manifest.History) |
|
| 46 |
+ |
|
| 47 |
+ return Sign(&m, mb.pk) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// AppendReference adds a reference to the current ManifestBuilder |
|
| 51 |
+func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
| 52 |
+ r, ok := d.(Reference) |
|
| 53 |
+ if !ok {
|
|
| 54 |
+ return fmt.Errorf("Unable to add non-reference type to v1 builder")
|
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ // Entries need to be prepended |
|
| 58 |
+ mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...)
|
|
| 59 |
+ mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...)
|
|
| 60 |
+ return nil |
|
| 61 |
+ |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// References returns the current references added to this builder |
|
| 65 |
+func (mb *referenceManifestBuilder) References() []distribution.Descriptor {
|
|
| 66 |
+ refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) |
|
| 67 |
+ for i := range mb.Manifest.FSLayers {
|
|
| 68 |
+ layerDigest := mb.Manifest.FSLayers[i].BlobSum |
|
| 69 |
+ history := mb.Manifest.History[i] |
|
| 70 |
+ ref := Reference{layerDigest, 0, history}
|
|
| 71 |
+ refs[i] = ref.Descriptor() |
|
| 72 |
+ } |
|
| 73 |
+ return refs |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+// Reference describes a manifest v2, schema version 1 dependency. |
|
| 77 |
+// An FSLayer associated with a history entry. |
|
| 78 |
+type Reference struct {
|
|
| 79 |
+ Digest digest.Digest |
|
| 80 |
+ Size int64 // if we know it, set it for the descriptor. |
|
| 81 |
+ History History |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+// Descriptor describes a reference |
|
| 85 |
+func (r Reference) Descriptor() distribution.Descriptor {
|
|
| 86 |
+ return distribution.Descriptor{
|
|
| 87 |
+ MediaType: MediaTypeManifestLayer, |
|
| 88 |
+ Digest: r.Digest, |
|
| 89 |
+ Size: r.Size, |
|
| 90 |
+ } |
|
| 91 |
+} |
| ... | ... |
@@ -31,8 +31,9 @@ func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) {
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
return &SignedManifest{
|
| 34 |
- Manifest: *m, |
|
| 35 |
- Raw: pretty, |
|
| 34 |
+ Manifest: *m, |
|
| 35 |
+ all: pretty, |
|
| 36 |
+ Canonical: p, |
|
| 36 | 37 |
}, nil |
| 37 | 38 |
} |
| 38 | 39 |
|
| ... | ... |
@@ -60,7 +61,8 @@ func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certifica |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 | 62 |
return &SignedManifest{
|
| 63 |
- Manifest: *m, |
|
| 64 |
- Raw: pretty, |
|
| 63 |
+ Manifest: *m, |
|
| 64 |
+ all: pretty, |
|
| 65 |
+ Canonical: p, |
|
| 65 | 66 |
}, nil |
| 66 | 67 |
} |
| ... | ... |
@@ -10,7 +10,7 @@ import ( |
| 10 | 10 |
// Verify verifies the signature of the signed manifest returning the public |
| 11 | 11 |
// keys used during signing. |
| 12 | 12 |
func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
| 13 |
- js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 13 |
+ js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 14 | 14 |
if err != nil {
|
| 15 | 15 |
logrus.WithField("err", err).Debugf("(*SignedManifest).Verify")
|
| 16 | 16 |
return nil, err |
| ... | ... |
@@ -23,7 +23,7 @@ func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
| 23 | 23 |
// certificate pool returning the list of verified chains. Signatures without |
| 24 | 24 |
// an x509 chain are not checked. |
| 25 | 25 |
func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) {
|
| 26 |
- js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 26 |
+ js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 27 | 27 |
if err != nil {
|
| 28 | 28 |
return nil, err |
| 29 | 29 |
} |
| 30 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,74 @@ |
| 0 |
+package schema2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/docker/distribution" |
|
| 4 |
+ "github.com/docker/distribution/context" |
|
| 5 |
+ "github.com/docker/distribution/digest" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// builder is a type for constructing manifests. |
|
| 9 |
+type builder struct {
|
|
| 10 |
+ // bs is a BlobService used to publish the configuration blob. |
|
| 11 |
+ bs distribution.BlobService |
|
| 12 |
+ |
|
| 13 |
+ // configJSON references |
|
| 14 |
+ configJSON []byte |
|
| 15 |
+ |
|
| 16 |
+ // layers is a list of layer descriptors that gets built by successive |
|
| 17 |
+ // calls to AppendReference. |
|
| 18 |
+ layers []distribution.Descriptor |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// NewManifestBuilder is used to build new manifests for the current schema |
|
| 22 |
+// version. It takes a BlobService so it can publish the configuration blob |
|
| 23 |
+// as part of the Build process. |
|
| 24 |
+func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder {
|
|
| 25 |
+ mb := &builder{
|
|
| 26 |
+ bs: bs, |
|
| 27 |
+ configJSON: make([]byte, len(configJSON)), |
|
| 28 |
+ } |
|
| 29 |
+ copy(mb.configJSON, configJSON) |
|
| 30 |
+ |
|
| 31 |
+ return mb |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// Build produces a final manifest from the given references. |
|
| 35 |
+func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
|
|
| 36 |
+ m := Manifest{
|
|
| 37 |
+ Versioned: SchemaVersion, |
|
| 38 |
+ Layers: make([]distribution.Descriptor, len(mb.layers)), |
|
| 39 |
+ } |
|
| 40 |
+ copy(m.Layers, mb.layers) |
|
| 41 |
+ |
|
| 42 |
+ configDigest := digest.FromBytes(mb.configJSON) |
|
| 43 |
+ |
|
| 44 |
+ var err error |
|
| 45 |
+ m.Config, err = mb.bs.Stat(ctx, configDigest) |
|
| 46 |
+ switch err {
|
|
| 47 |
+ case nil: |
|
| 48 |
+ return FromStruct(m) |
|
| 49 |
+ case distribution.ErrBlobUnknown: |
|
| 50 |
+ // nop |
|
| 51 |
+ default: |
|
| 52 |
+ return nil, err |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ // Add config to the blob store |
|
| 56 |
+ m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return nil, err |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ return FromStruct(m) |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// AppendReference adds a reference to the current ManifestBuilder. |
|
| 65 |
+func (mb *builder) AppendReference(d distribution.Describable) error {
|
|
| 66 |
+ mb.layers = append(mb.layers, d.Descriptor()) |
|
| 67 |
+ return nil |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+// References returns the current references added to this builder. |
|
| 71 |
+func (mb *builder) References() []distribution.Descriptor {
|
|
| 72 |
+ return mb.layers |
|
| 73 |
+} |
| 0 | 74 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,125 @@ |
| 0 |
+package schema2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+const ( |
|
| 13 |
+ // MediaTypeManifest specifies the mediaType for the current version. |
|
| 14 |
+ MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" |
|
| 15 |
+ |
|
| 16 |
+ // MediaTypeConfig specifies the mediaType for the image configuration. |
|
| 17 |
+ MediaTypeConfig = "application/vnd.docker.container.image.v1+json" |
|
| 18 |
+ |
|
| 19 |
+ // MediaTypeLayer is the mediaType used for layers referenced by the |
|
| 20 |
+ // manifest. |
|
| 21 |
+ MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+var ( |
|
| 25 |
+ // SchemaVersion provides a pre-initialized version structure for this |
|
| 26 |
+ // packages version of the manifest. |
|
| 27 |
+ SchemaVersion = manifest.Versioned{
|
|
| 28 |
+ SchemaVersion: 2, |
|
| 29 |
+ MediaType: MediaTypeManifest, |
|
| 30 |
+ } |
|
| 31 |
+) |
|
| 32 |
+ |
|
| 33 |
+func init() {
|
|
| 34 |
+ schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 35 |
+ m := new(DeserializedManifest) |
|
| 36 |
+ err := m.UnmarshalJSON(b) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return nil, distribution.Descriptor{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ dgst := digest.FromBytes(b) |
|
| 42 |
+ return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
|
|
| 43 |
+ } |
|
| 44 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// Manifest defines a schema2 manifest. |
|
| 51 |
+type Manifest struct {
|
|
| 52 |
+ manifest.Versioned |
|
| 53 |
+ |
|
| 54 |
+ // Config references the image configuration as a blob. |
|
| 55 |
+ Config distribution.Descriptor `json:"config"` |
|
| 56 |
+ |
|
| 57 |
+ // Layers lists descriptors for the layers referenced by the |
|
| 58 |
+ // configuration. |
|
| 59 |
+ Layers []distribution.Descriptor `json:"layers"` |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// References returnes the descriptors of this manifests references. |
|
| 63 |
+func (m Manifest) References() []distribution.Descriptor {
|
|
| 64 |
+ return m.Layers |
|
| 65 |
+ |
|
| 66 |
+} |
|
| 67 |
+ |
|
| 68 |
+// Target returns the target of this signed manifest. |
|
| 69 |
+func (m Manifest) Target() distribution.Descriptor {
|
|
| 70 |
+ return m.Config |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// DeserializedManifest wraps Manifest with a copy of the original JSON. |
|
| 74 |
+// It satisfies the distribution.Manifest interface. |
|
| 75 |
+type DeserializedManifest struct {
|
|
| 76 |
+ Manifest |
|
| 77 |
+ |
|
| 78 |
+ // canonical is the canonical byte representation of the Manifest. |
|
| 79 |
+ canonical []byte |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+// FromStruct takes a Manifest structure, marshals it to JSON, and returns a |
|
| 83 |
+// DeserializedManifest which contains the manifest and its JSON representation. |
|
| 84 |
+func FromStruct(m Manifest) (*DeserializedManifest, error) {
|
|
| 85 |
+ var deserialized DeserializedManifest |
|
| 86 |
+ deserialized.Manifest = m |
|
| 87 |
+ |
|
| 88 |
+ var err error |
|
| 89 |
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ") |
|
| 90 |
+ return &deserialized, err |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// UnmarshalJSON populates a new Manifest struct from JSON data. |
|
| 94 |
+func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
|
|
| 95 |
+ m.canonical = make([]byte, len(b), len(b)) |
|
| 96 |
+ // store manifest in canonical |
|
| 97 |
+ copy(m.canonical, b) |
|
| 98 |
+ |
|
| 99 |
+ // Unmarshal canonical JSON into Manifest object |
|
| 100 |
+ var manifest Manifest |
|
| 101 |
+ if err := json.Unmarshal(m.canonical, &manifest); err != nil {
|
|
| 102 |
+ return err |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ m.Manifest = manifest |
|
| 106 |
+ |
|
| 107 |
+ return nil |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+// MarshalJSON returns the contents of canonical. If canonical is empty, |
|
| 111 |
+// marshals the inner contents. |
|
| 112 |
+func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
|
|
| 113 |
+ if len(m.canonical) > 0 {
|
|
| 114 |
+ return m.canonical, nil |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ return nil, errors.New("JSON representation not initialized in DeserializedManifest")
|
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+// Payload returns the raw content of the manifest. The contents can be used to |
|
| 121 |
+// calculate the content identifier. |
|
| 122 |
+func (m DeserializedManifest) Payload() (string, []byte, error) {
|
|
| 123 |
+ return m.MediaType, m.canonical, nil |
|
| 124 |
+} |
| ... | ... |
@@ -1,9 +1,12 @@ |
| 1 | 1 |
package manifest |
| 2 | 2 |
|
| 3 |
-// Versioned provides a struct with just the manifest schemaVersion. Incoming |
|
| 3 |
+// Versioned provides a struct with the manifest schemaVersion and . Incoming |
|
| 4 | 4 |
// content with unknown schema version can be decoded against this struct to |
| 5 | 5 |
// check the version. |
| 6 | 6 |
type Versioned struct {
|
| 7 | 7 |
// SchemaVersion is the image manifest schema that this image follows |
| 8 | 8 |
SchemaVersion int `json:"schemaVersion"` |
| 9 |
+ |
|
| 10 |
+ // MediaType is the media type of this schema. |
|
| 11 |
+ MediaType string `json:"mediaType,omitempty"` |
|
| 9 | 12 |
} |
| 10 | 13 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/distribution/context" |
|
| 6 |
+ "github.com/docker/distribution/digest" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// Manifest represents a registry object specifying a set of |
|
| 10 |
+// references and an optional target |
|
| 11 |
+type Manifest interface {
|
|
| 12 |
+ // References returns a list of objects which make up this manifest. |
|
| 13 |
+ // The references are strictly ordered from base to head. A reference |
|
| 14 |
+ // is anything which can be represented by a distribution.Descriptor |
|
| 15 |
+ References() []Descriptor |
|
| 16 |
+ |
|
| 17 |
+ // Payload provides the serialized format of the manifest, in addition to |
|
| 18 |
+ // the mediatype. |
|
| 19 |
+ Payload() (mediatype string, payload []byte, err error) |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// ManifestBuilder creates a manifest allowing one to include dependencies. |
|
| 23 |
+// Instances can be obtained from a version-specific manifest package. Manifest |
|
| 24 |
+// specific data is passed into the function which creates the builder. |
|
| 25 |
+type ManifestBuilder interface {
|
|
| 26 |
+ // Build creates the manifest from his builder. |
|
| 27 |
+ Build(ctx context.Context) (Manifest, error) |
|
| 28 |
+ |
|
| 29 |
+ // References returns a list of objects which have been added to this |
|
| 30 |
+ // builder. The dependencies are returned in the order they were added, |
|
| 31 |
+ // which should be from base to head. |
|
| 32 |
+ References() []Descriptor |
|
| 33 |
+ |
|
| 34 |
+ // AppendReference includes the given object in the manifest after any |
|
| 35 |
+ // existing dependencies. If the add fails, such as when adding an |
|
| 36 |
+ // unsupported dependency, an error may be returned. |
|
| 37 |
+ AppendReference(dependency Describable) error |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// ManifestService describes operations on image manifests. |
|
| 41 |
+type ManifestService interface {
|
|
| 42 |
+ // Exists returns true if the manifest exists. |
|
| 43 |
+ Exists(ctx context.Context, dgst digest.Digest) (bool, error) |
|
| 44 |
+ |
|
| 45 |
+ // Get retrieves the manifest specified by the given digest |
|
| 46 |
+ Get(ctx context.Context, dgst digest.Digest, options ...ManifestServiceOption) (Manifest, error) |
|
| 47 |
+ |
|
| 48 |
+ // Put creates or updates the given manifest returning the manifest digest |
|
| 49 |
+ Put(ctx context.Context, manifest Manifest, options ...ManifestServiceOption) (digest.Digest, error) |
|
| 50 |
+ |
|
| 51 |
+ // Delete removes the manifest specified by the given digest. Deleting |
|
| 52 |
+ // a manifest that doesn't exist will return ErrManifestNotFound |
|
| 53 |
+ Delete(ctx context.Context, dgst digest.Digest) error |
|
| 54 |
+ |
|
| 55 |
+ // Enumerate fills 'manifests' with the manifests in this service up |
|
| 56 |
+ // to the size of 'manifests' and returns 'n' for the number of entries |
|
| 57 |
+ // which were filled. 'last' contains an offset in the manifest set |
|
| 58 |
+ // and can be used to resume iteration. |
|
| 59 |
+ //Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error) |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// Describable is an interface for descriptors |
|
| 63 |
+type Describable interface {
|
|
| 64 |
+ Descriptor() Descriptor |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+// ManifestMediaTypes returns the supported media types for manifests. |
|
| 68 |
+func ManifestMediaTypes() (mediaTypes []string) {
|
|
| 69 |
+ for t := range mappings {
|
|
| 70 |
+ mediaTypes = append(mediaTypes, t) |
|
| 71 |
+ } |
|
| 72 |
+ return |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// UnmarshalFunc implements manifest unmarshalling a given MediaType |
|
| 76 |
+type UnmarshalFunc func([]byte) (Manifest, Descriptor, error) |
|
| 77 |
+ |
|
| 78 |
+var mappings = make(map[string]UnmarshalFunc, 0) |
|
| 79 |
+ |
|
| 80 |
+// UnmarshalManifest looks up manifest unmarshall functions based on |
|
| 81 |
+// MediaType |
|
| 82 |
+func UnmarshalManifest(mediatype string, p []byte) (Manifest, Descriptor, error) {
|
|
| 83 |
+ unmarshalFunc, ok := mappings[mediatype] |
|
| 84 |
+ if !ok {
|
|
| 85 |
+ return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype: %s", mediatype)
|
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ return unmarshalFunc(p) |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+// RegisterManifestSchema registers an UnmarshalFunc for a given schema type. This |
|
| 92 |
+// should be called from specific |
|
| 93 |
+func RegisterManifestSchema(mediatype string, u UnmarshalFunc) error {
|
|
| 94 |
+ if _, ok := mappings[mediatype]; ok {
|
|
| 95 |
+ return fmt.Errorf("manifest mediatype registration would overwrite existing: %s", mediatype)
|
|
| 96 |
+ } |
|
| 97 |
+ mappings[mediatype] = u |
|
| 98 |
+ return nil |
|
| 99 |
+} |
| ... | ... |
@@ -4,22 +4,16 @@ |
| 4 | 4 |
// Grammar |
| 5 | 5 |
// |
| 6 | 6 |
// reference := repository [ ":" tag ] [ "@" digest ] |
| 7 |
+// name := [hostname '/'] component ['/' component]* |
|
| 8 |
+// hostname := hostcomponent ['.' hostcomponent]* [':' port-number] |
|
| 9 |
+// hostcomponent := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/ |
|
| 10 |
+// port-number := /[0-9]+/ |
|
| 11 |
+// component := alpha-numeric [separator alpha-numeric]* |
|
| 12 |
+// alpha-numeric := /[a-z0-9]+/ |
|
| 13 |
+// separator := /[_.]|__|[-]*/ |
|
| 7 | 14 |
// |
| 8 |
-// // repository.go |
|
| 9 |
-// repository := hostname ['/' component]+ |
|
| 10 |
-// hostname := hostcomponent [':' port-number] |
|
| 11 |
-// component := subcomponent [separator subcomponent]* |
|
| 12 |
-// subcomponent := alpha-numeric ['-'* alpha-numeric]* |
|
| 13 |
-// hostcomponent := [hostpart '.']* hostpart |
|
| 14 |
-// alpha-numeric := /[a-z0-9]+/ |
|
| 15 |
-// separator := /([_.]|__)/ |
|
| 16 |
-// port-number := /[0-9]+/ |
|
| 17 |
-// hostpart := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/ |
|
| 18 |
-// |
|
| 19 |
-// // tag.go |
|
| 20 | 15 |
// tag := /[\w][\w.-]{0,127}/
|
| 21 | 16 |
// |
| 22 |
-// // from the digest package |
|
| 23 | 17 |
// digest := digest-algorithm ":" digest-hex |
| 24 | 18 |
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] |
| 25 | 19 |
// digest-algorithm-separator := /[+.-_]/ |
| ... | ... |
@@ -52,8 +46,7 @@ var ( |
| 52 | 52 |
// ErrNameEmpty is returned for empty, invalid repository names. |
| 53 | 53 |
ErrNameEmpty = errors.New("repository name must have at least one component")
|
| 54 | 54 |
|
| 55 |
- // ErrNameTooLong is returned when a repository name is longer than |
|
| 56 |
- // RepositoryNameTotalLengthMax |
|
| 55 |
+ // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax. |
|
| 57 | 56 |
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
|
| 58 | 57 |
) |
| 59 | 58 |
|
| ... | ... |
@@ -3,47 +3,122 @@ package reference |
| 3 | 3 |
import "regexp" |
| 4 | 4 |
|
| 5 | 5 |
var ( |
| 6 |
- // nameSubComponentRegexp defines the part of the name which must be |
|
| 7 |
- // begin and end with an alphanumeric character. These characters can |
|
| 8 |
- // be separated by any number of dashes. |
|
| 9 |
- nameSubComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[-]+[a-z0-9]+)*`) |
|
| 6 |
+ // alphaNumericRegexp defines the alpha numeric atom, typically a |
|
| 7 |
+ // component of names. This only allows lower case characters and digits. |
|
| 8 |
+ alphaNumericRegexp = match(`[a-z0-9]+`) |
|
| 10 | 9 |
|
| 11 |
- // nameComponentRegexp restricts registry path component names to |
|
| 12 |
- // start with at least one letter or number, with following parts able to |
|
| 13 |
- // be separated by one period, underscore or double underscore. |
|
| 14 |
- nameComponentRegexp = regexp.MustCompile(nameSubComponentRegexp.String() + `(?:(?:[._]|__)` + nameSubComponentRegexp.String() + `)*`) |
|
| 10 |
+ // separatorRegexp defines the separators allowed to be embedded in name |
|
| 11 |
+ // components. This allow one period, one or two underscore and multiple |
|
| 12 |
+ // dashes. |
|
| 13 |
+ separatorRegexp = match(`(?:[._]|__|[-]*)`) |
|
| 15 | 14 |
|
| 16 |
- nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String()) |
|
| 15 |
+ // nameComponentRegexp restricts registry path component names to start |
|
| 16 |
+ // with at least one letter or number, with following parts able to be |
|
| 17 |
+ // separated by one period, one or two underscore and multiple dashes. |
|
| 18 |
+ nameComponentRegexp = expression( |
|
| 19 |
+ alphaNumericRegexp, |
|
| 20 |
+ optional(repeated(separatorRegexp, alphaNumericRegexp))) |
|
| 17 | 21 |
|
| 18 |
- hostnameComponentRegexp = regexp.MustCompile(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`) |
|
| 22 |
+ // hostnameComponentRegexp restricts the registry hostname component of a |
|
| 23 |
+ // repository name to start with a component as defined by hostnameRegexp |
|
| 24 |
+ // and followed by an optional port. |
|
| 25 |
+ hostnameComponentRegexp = match(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`) |
|
| 19 | 26 |
|
| 20 |
- // hostnameComponentRegexp restricts the registry hostname component of a repository name to |
|
| 21 |
- // start with a component as defined by hostnameRegexp and followed by an optional port. |
|
| 22 |
- hostnameRegexp = regexp.MustCompile(`(?:` + hostnameComponentRegexp.String() + `\.)*` + hostnameComponentRegexp.String() + `(?::[0-9]+)?`) |
|
| 27 |
+ // hostnameRegexp defines the structure of potential hostname components |
|
| 28 |
+ // that may be part of image names. This is purposely a subset of what is |
|
| 29 |
+ // allowed by DNS to ensure backwards compatibility with Docker image |
|
| 30 |
+ // names. |
|
| 31 |
+ hostnameRegexp = expression( |
|
| 32 |
+ hostnameComponentRegexp, |
|
| 33 |
+ optional(repeated(literal(`.`), hostnameComponentRegexp)), |
|
| 34 |
+ optional(literal(`:`), match(`[0-9]+`))) |
|
| 23 | 35 |
|
| 24 | 36 |
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go. |
| 25 |
- TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
|
| 37 |
+ TagRegexp = match(`[\w][\w.-]{0,127}`)
|
|
| 26 | 38 |
|
| 27 | 39 |
// anchoredTagRegexp matches valid tag names, anchored at the start and |
| 28 | 40 |
// end of the matched string. |
| 29 |
- anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) |
|
| 41 |
+ anchoredTagRegexp = anchored(TagRegexp) |
|
| 30 | 42 |
|
| 31 | 43 |
// DigestRegexp matches valid digests. |
| 32 |
- DigestRegexp = regexp.MustCompile(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
|
|
| 44 |
+ DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
|
|
| 33 | 45 |
|
| 34 | 46 |
// anchoredDigestRegexp matches valid digests, anchored at the start and |
| 35 | 47 |
// end of the matched string. |
| 36 |
- anchoredDigestRegexp = regexp.MustCompile(`^` + DigestRegexp.String() + `$`) |
|
| 48 |
+ anchoredDigestRegexp = anchored(DigestRegexp) |
|
| 37 | 49 |
|
| 38 | 50 |
// NameRegexp is the format for the name component of references. The |
| 39 | 51 |
// regexp has capturing groups for the hostname and name part omitting |
| 40 | 52 |
// the seperating forward slash from either. |
| 41 |
- NameRegexp = regexp.MustCompile(`(?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String()) |
|
| 53 |
+ NameRegexp = expression( |
|
| 54 |
+ optional(hostnameRegexp, literal(`/`)), |
|
| 55 |
+ nameComponentRegexp, |
|
| 56 |
+ optional(repeated(literal(`/`), nameComponentRegexp))) |
|
| 42 | 57 |
|
| 43 |
- // ReferenceRegexp is the full supported format of a reference. The |
|
| 44 |
- // regexp has capturing groups for name, tag, and digest components. |
|
| 45 |
- ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@](` + DigestRegexp.String() + `))?$`) |
|
| 58 |
+ // anchoredNameRegexp is used to parse a name value, capturing the |
|
| 59 |
+ // hostname and trailing components. |
|
| 60 |
+ anchoredNameRegexp = anchored( |
|
| 61 |
+ optional(capture(hostnameRegexp), literal(`/`)), |
|
| 62 |
+ capture(nameComponentRegexp, |
|
| 63 |
+ optional(repeated(literal(`/`), nameComponentRegexp)))) |
|
| 46 | 64 |
|
| 47 |
- // anchoredNameRegexp is used to parse a name value, capturing hostname |
|
| 48 |
- anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`) |
|
| 65 |
+ // ReferenceRegexp is the full supported format of a reference. The regexp |
|
| 66 |
+ // is anchored and has capturing groups for name, tag, and digest |
|
| 67 |
+ // components. |
|
| 68 |
+ ReferenceRegexp = anchored(capture(NameRegexp), |
|
| 69 |
+ optional(literal(":"), capture(TagRegexp)),
|
|
| 70 |
+ optional(literal("@"), capture(DigestRegexp)))
|
|
| 49 | 71 |
) |
| 72 |
+ |
|
| 73 |
+// match compiles the string to a regular expression. |
|
| 74 |
+var match = regexp.MustCompile |
|
| 75 |
+ |
|
| 76 |
+// literal compiles s into a literal regular expression, escaping any regexp |
|
| 77 |
+// reserved characters. |
|
| 78 |
+func literal(s string) *regexp.Regexp {
|
|
| 79 |
+ re := match(regexp.QuoteMeta(s)) |
|
| 80 |
+ |
|
| 81 |
+ if _, complete := re.LiteralPrefix(); !complete {
|
|
| 82 |
+ panic("must be a literal")
|
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ return re |
|
| 86 |
+} |
|
| 87 |
+ |
|
| 88 |
+// expression defines a full expression, where each regular expression must |
|
| 89 |
+// follow the previous. |
|
| 90 |
+func expression(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 91 |
+ var s string |
|
| 92 |
+ for _, re := range res {
|
|
| 93 |
+ s += re.String() |
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ return match(s) |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+// optional wraps the expression in a non-capturing group and makes the |
|
| 100 |
+// production optional. |
|
| 101 |
+func optional(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 102 |
+ return match(group(expression(res...)).String() + `?`) |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// repeated wraps the regexp in a non-capturing group to get one or more |
|
| 106 |
+// matches. |
|
| 107 |
+func repeated(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 108 |
+ return match(group(expression(res...)).String() + `+`) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+// group wraps the regexp in a non-capturing group. |
|
| 112 |
+func group(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 113 |
+ return match(`(?:` + expression(res...).String() + `)`) |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+// capture wraps the expression in a capturing group. |
|
| 117 |
+func capture(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 118 |
+ return match(`(` + expression(res...).String() + `)`) |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+// anchored anchors the regular expression by adding start and end delimiters. |
|
| 122 |
+func anchored(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 123 |
+ return match(`^` + expression(res...).String() + `$`) |
|
| 124 |
+} |
| ... | ... |
@@ -2,8 +2,6 @@ package distribution |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"github.com/docker/distribution/context" |
| 5 |
- "github.com/docker/distribution/digest" |
|
| 6 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 7 | 5 |
) |
| 8 | 6 |
|
| 9 | 7 |
// Scope defines the set of items that match a namespace. |
| ... | ... |
@@ -44,7 +42,9 @@ type Namespace interface {
|
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
// ManifestServiceOption is a function argument for Manifest Service methods |
| 47 |
-type ManifestServiceOption func(ManifestService) error |
|
| 47 |
+type ManifestServiceOption interface {
|
|
| 48 |
+ Apply(ManifestService) error |
|
| 49 |
+} |
|
| 48 | 50 |
|
| 49 | 51 |
// Repository is a named collection of manifests and layers. |
| 50 | 52 |
type Repository interface {
|
| ... | ... |
@@ -62,59 +62,10 @@ type Repository interface {
|
| 62 | 62 |
// be a BlobService for use with clients. This will allow such |
| 63 | 63 |
// implementations to avoid implementing ServeBlob. |
| 64 | 64 |
|
| 65 |
- // Signatures returns a reference to this repository's signatures service. |
|
| 66 |
- Signatures() SignatureService |
|
| 65 |
+ // Tags returns a reference to this repositories tag service |
|
| 66 |
+ Tags(ctx context.Context) TagService |
|
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
// TODO(stevvooe): Must add close methods to all these. May want to change the |
| 70 | 70 |
// way instances are created to better reflect internal dependency |
| 71 | 71 |
// relationships. |
| 72 |
- |
|
| 73 |
-// ManifestService provides operations on image manifests. |
|
| 74 |
-type ManifestService interface {
|
|
| 75 |
- // Exists returns true if the manifest exists. |
|
| 76 |
- Exists(dgst digest.Digest) (bool, error) |
|
| 77 |
- |
|
| 78 |
- // Get retrieves the identified by the digest, if it exists. |
|
| 79 |
- Get(dgst digest.Digest) (*schema1.SignedManifest, error) |
|
| 80 |
- |
|
| 81 |
- // Delete removes the manifest, if it exists. |
|
| 82 |
- Delete(dgst digest.Digest) error |
|
| 83 |
- |
|
| 84 |
- // Put creates or updates the manifest. |
|
| 85 |
- Put(manifest *schema1.SignedManifest) error |
|
| 86 |
- |
|
| 87 |
- // TODO(stevvooe): The methods after this message should be moved to a |
|
| 88 |
- // discrete TagService, per active proposals. |
|
| 89 |
- |
|
| 90 |
- // Tags lists the tags under the named repository. |
|
| 91 |
- Tags() ([]string, error) |
|
| 92 |
- |
|
| 93 |
- // ExistsByTag returns true if the manifest exists. |
|
| 94 |
- ExistsByTag(tag string) (bool, error) |
|
| 95 |
- |
|
| 96 |
- // GetByTag retrieves the named manifest, if it exists. |
|
| 97 |
- GetByTag(tag string, options ...ManifestServiceOption) (*schema1.SignedManifest, error) |
|
| 98 |
- |
|
| 99 |
- // TODO(stevvooe): There are several changes that need to be done to this |
|
| 100 |
- // interface: |
|
| 101 |
- // |
|
| 102 |
- // 1. Allow explicit tagging with Tag(digest digest.Digest, tag string) |
|
| 103 |
- // 2. Support reading tags with a re-entrant reader to avoid large |
|
| 104 |
- // allocations in the registry. |
|
| 105 |
- // 3. Long-term: Provide All() method that lets one scroll through all of |
|
| 106 |
- // the manifest entries. |
|
| 107 |
- // 4. Long-term: break out concept of signing from manifests. This is |
|
| 108 |
- // really a part of the distribution sprint. |
|
| 109 |
- // 5. Long-term: Manifest should be an interface. This code shouldn't |
|
| 110 |
- // really be concerned with the storage format. |
|
| 111 |
-} |
|
| 112 |
- |
|
| 113 |
-// SignatureService provides operations on signatures. |
|
| 114 |
-type SignatureService interface {
|
|
| 115 |
- // Get retrieves all of the signature blobs for the specified digest. |
|
| 116 |
- Get(dgst digest.Digest) ([][]byte, error) |
|
| 117 |
- |
|
| 118 |
- // Put stores the signature for the provided digest. |
|
| 119 |
- Put(dgst digest.Digest, signatures ...[]byte) error |
|
| 120 |
-} |
| ... | ... |
@@ -25,7 +25,8 @@ func (ec ErrorCode) ErrorCode() ErrorCode {
|
| 25 | 25 |
|
| 26 | 26 |
// Error returns the ID/Value |
| 27 | 27 |
func (ec ErrorCode) Error() string {
|
| 28 |
- return ec.Descriptor().Value |
|
| 28 |
+ // NOTE(stevvooe): Cannot use message here since it may have unpopulated args. |
|
| 29 |
+ return strings.ToLower(strings.Replace(ec.String(), "_", " ", -1)) |
|
| 29 | 30 |
} |
| 30 | 31 |
|
| 31 | 32 |
// Descriptor returns the descriptor for the error code. |
| ... | ... |
@@ -104,9 +105,7 @@ func (e Error) ErrorCode() ErrorCode {
|
| 104 | 104 |
|
| 105 | 105 |
// Error returns a human readable representation of the error. |
| 106 | 106 |
func (e Error) Error() string {
|
| 107 |
- return fmt.Sprintf("%s: %s",
|
|
| 108 |
- strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)), |
|
| 109 |
- e.Message) |
|
| 107 |
+ return fmt.Sprintf("%s: %s", e.Code.Error(), e.Message)
|
|
| 110 | 108 |
} |
| 111 | 109 |
|
| 112 | 110 |
// WithDetail will return a new Error, based on the current one, but with |
| ... | ... |
@@ -495,7 +495,7 @@ var routeDescriptors = []RouteDescriptor{
|
| 495 | 495 |
Methods: []MethodDescriptor{
|
| 496 | 496 |
{
|
| 497 | 497 |
Method: "GET", |
| 498 |
- Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest.", |
|
| 498 |
+ Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", |
|
| 499 | 499 |
Requests: []RequestDescriptor{
|
| 500 | 500 |
{
|
| 501 | 501 |
Headers: []ParameterDescriptor{
|
| ... | ... |
@@ -204,7 +204,9 @@ func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
|
| 204 | 204 |
routeURL.Path = routeURL.Path[1:] |
| 205 | 205 |
} |
| 206 | 206 |
|
| 207 |
- return cr.root.ResolveReference(routeURL), nil |
|
| 207 |
+ url := cr.root.ResolveReference(routeURL) |
|
| 208 |
+ url.Scheme = cr.root.Scheme |
|
| 209 |
+ return url, nil |
|
| 208 | 210 |
} |
| 209 | 211 |
|
| 210 | 212 |
// appendValuesURL appends the parameters to the url. |
| ... | ... |
@@ -240,7 +240,8 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon |
| 240 | 240 |
defer resp.Body.Close() |
| 241 | 241 |
|
| 242 | 242 |
if !client.SuccessStatus(resp.StatusCode) {
|
| 243 |
- return nil, fmt.Errorf("token auth attempt for registry: %s request failed with status: %d %s", req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
| 243 |
+ err := client.HandleErrorResponse(resp) |
|
| 244 |
+ return nil, err |
|
| 244 | 245 |
} |
| 245 | 246 |
|
| 246 | 247 |
decoder := json.NewDecoder(resp.Body) |
| ... | ... |
@@ -33,7 +33,7 @@ func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error {
|
| 33 | 33 |
if resp.StatusCode == http.StatusNotFound {
|
| 34 | 34 |
return distribution.ErrBlobUploadUnknown |
| 35 | 35 |
} |
| 36 |
- return handleErrorResponse(resp) |
|
| 36 |
+ return HandleErrorResponse(resp) |
|
| 37 | 37 |
} |
| 38 | 38 |
|
| 39 | 39 |
func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) {
|
| ... | ... |
@@ -47,7 +47,11 @@ func parseHTTPErrorResponse(r io.Reader) error {
|
| 47 | 47 |
return errors |
| 48 | 48 |
} |
| 49 | 49 |
|
| 50 |
-func handleErrorResponse(resp *http.Response) error {
|
|
| 50 |
+// HandleErrorResponse returns error parsed from HTTP response for an |
|
| 51 |
+// unsuccessful HTTP response code (in the range 400 - 499 inclusive). An |
|
| 52 |
+// UnexpectedHTTPStatusError returned for response code outside of expected |
|
| 53 |
+// range. |
|
| 54 |
+func HandleErrorResponse(resp *http.Response) error {
|
|
| 51 | 55 |
if resp.StatusCode == 401 {
|
| 52 | 56 |
err := parseHTTPErrorResponse(resp.Body) |
| 53 | 57 |
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok {
|
| ... | ... |
@@ -3,6 +3,7 @@ package client |
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
"encoding/json" |
| 6 |
+ "errors" |
|
| 6 | 7 |
"fmt" |
| 7 | 8 |
"io" |
| 8 | 9 |
"io/ioutil" |
| ... | ... |
@@ -14,7 +15,6 @@ import ( |
| 14 | 14 |
"github.com/docker/distribution" |
| 15 | 15 |
"github.com/docker/distribution/context" |
| 16 | 16 |
"github.com/docker/distribution/digest" |
| 17 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 18 | 17 |
"github.com/docker/distribution/reference" |
| 19 | 18 |
"github.com/docker/distribution/registry/api/v2" |
| 20 | 19 |
"github.com/docker/distribution/registry/client/transport" |
| ... | ... |
@@ -91,7 +91,7 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri |
| 91 | 91 |
returnErr = io.EOF |
| 92 | 92 |
} |
| 93 | 93 |
} else {
|
| 94 |
- return 0, handleErrorResponse(resp) |
|
| 94 |
+ return 0, HandleErrorResponse(resp) |
|
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 | 97 |
return numFilled, returnErr |
| ... | ... |
@@ -156,74 +156,151 @@ func (r *repository) Manifests(ctx context.Context, options ...distribution.Mani |
| 156 | 156 |
}, nil |
| 157 | 157 |
} |
| 158 | 158 |
|
| 159 |
-func (r *repository) Signatures() distribution.SignatureService {
|
|
| 160 |
- ms, _ := r.Manifests(r.context) |
|
| 161 |
- return &signatures{
|
|
| 162 |
- manifests: ms, |
|
| 159 |
+func (r *repository) Tags(ctx context.Context) distribution.TagService {
|
|
| 160 |
+ return &tags{
|
|
| 161 |
+ client: r.client, |
|
| 162 |
+ ub: r.ub, |
|
| 163 |
+ context: r.context, |
|
| 164 |
+ name: r.Name(), |
|
| 163 | 165 |
} |
| 164 | 166 |
} |
| 165 | 167 |
|
| 166 |
-type signatures struct {
|
|
| 167 |
- manifests distribution.ManifestService |
|
| 168 |
-} |
|
| 169 |
- |
|
| 170 |
-func (s *signatures) Get(dgst digest.Digest) ([][]byte, error) {
|
|
| 171 |
- m, err := s.manifests.Get(dgst) |
|
| 172 |
- if err != nil {
|
|
| 173 |
- return nil, err |
|
| 174 |
- } |
|
| 175 |
- return m.Signatures() |
|
| 168 |
+// tags implements remote tagging operations. |
|
| 169 |
+type tags struct {
|
|
| 170 |
+ client *http.Client |
|
| 171 |
+ ub *v2.URLBuilder |
|
| 172 |
+ context context.Context |
|
| 173 |
+ name string |
|
| 176 | 174 |
} |
| 177 | 175 |
|
| 178 |
-func (s *signatures) Put(dgst digest.Digest, signatures ...[]byte) error {
|
|
| 179 |
- panic("not implemented")
|
|
| 180 |
-} |
|
| 176 |
+// All returns all tags |
|
| 177 |
+func (t *tags) All(ctx context.Context) ([]string, error) {
|
|
| 178 |
+ var tags []string |
|
| 181 | 179 |
|
| 182 |
-type manifests struct {
|
|
| 183 |
- name string |
|
| 184 |
- ub *v2.URLBuilder |
|
| 185 |
- client *http.Client |
|
| 186 |
- etags map[string]string |
|
| 187 |
-} |
|
| 188 |
- |
|
| 189 |
-func (ms *manifests) Tags() ([]string, error) {
|
|
| 190 |
- u, err := ms.ub.BuildTagsURL(ms.name) |
|
| 180 |
+ u, err := t.ub.BuildTagsURL(t.name) |
|
| 191 | 181 |
if err != nil {
|
| 192 |
- return nil, err |
|
| 182 |
+ return tags, err |
|
| 193 | 183 |
} |
| 194 | 184 |
|
| 195 |
- resp, err := ms.client.Get(u) |
|
| 185 |
+ resp, err := t.client.Get(u) |
|
| 196 | 186 |
if err != nil {
|
| 197 |
- return nil, err |
|
| 187 |
+ return tags, err |
|
| 198 | 188 |
} |
| 199 | 189 |
defer resp.Body.Close() |
| 200 | 190 |
|
| 201 | 191 |
if SuccessStatus(resp.StatusCode) {
|
| 202 | 192 |
b, err := ioutil.ReadAll(resp.Body) |
| 203 | 193 |
if err != nil {
|
| 204 |
- return nil, err |
|
| 194 |
+ return tags, err |
|
| 205 | 195 |
} |
| 206 | 196 |
|
| 207 | 197 |
tagsResponse := struct {
|
| 208 | 198 |
Tags []string `json:"tags"` |
| 209 | 199 |
}{}
|
| 210 | 200 |
if err := json.Unmarshal(b, &tagsResponse); err != nil {
|
| 211 |
- return nil, err |
|
| 201 |
+ return tags, err |
|
| 212 | 202 |
} |
| 203 |
+ tags = tagsResponse.Tags |
|
| 204 |
+ return tags, nil |
|
| 205 |
+ } |
|
| 206 |
+ return tags, HandleErrorResponse(resp) |
|
| 207 |
+} |
|
| 213 | 208 |
|
| 214 |
- return tagsResponse.Tags, nil |
|
| 209 |
+func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) {
|
|
| 210 |
+ desc := distribution.Descriptor{}
|
|
| 211 |
+ headers := response.Header |
|
| 212 |
+ |
|
| 213 |
+ ctHeader := headers.Get("Content-Type")
|
|
| 214 |
+ if ctHeader == "" {
|
|
| 215 |
+ return distribution.Descriptor{}, errors.New("missing or empty Content-Type header")
|
|
| 216 |
+ } |
|
| 217 |
+ desc.MediaType = ctHeader |
|
| 218 |
+ |
|
| 219 |
+ digestHeader := headers.Get("Docker-Content-Digest")
|
|
| 220 |
+ if digestHeader == "" {
|
|
| 221 |
+ bytes, err := ioutil.ReadAll(response.Body) |
|
| 222 |
+ if err != nil {
|
|
| 223 |
+ return distribution.Descriptor{}, err
|
|
| 224 |
+ } |
|
| 225 |
+ _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ return distribution.Descriptor{}, err
|
|
| 228 |
+ } |
|
| 229 |
+ return desc, nil |
|
| 230 |
+ } |
|
| 231 |
+ |
|
| 232 |
+ dgst, err := digest.ParseDigest(digestHeader) |
|
| 233 |
+ if err != nil {
|
|
| 234 |
+ return distribution.Descriptor{}, err
|
|
| 235 |
+ } |
|
| 236 |
+ desc.Digest = dgst |
|
| 237 |
+ |
|
| 238 |
+ lengthHeader := headers.Get("Content-Length")
|
|
| 239 |
+ if lengthHeader == "" {
|
|
| 240 |
+ return distribution.Descriptor{}, errors.New("missing or empty Content-Length header")
|
|
| 241 |
+ } |
|
| 242 |
+ length, err := strconv.ParseInt(lengthHeader, 10, 64) |
|
| 243 |
+ if err != nil {
|
|
| 244 |
+ return distribution.Descriptor{}, err
|
|
| 245 |
+ } |
|
| 246 |
+ desc.Size = length |
|
| 247 |
+ |
|
| 248 |
+ return desc, nil |
|
| 249 |
+ |
|
| 250 |
+} |
|
| 251 |
+ |
|
| 252 |
+// Get issues a HEAD request for a Manifest against its named endpoint in order |
|
| 253 |
+// to construct a descriptor for the tag. If the registry doesn't support HEADing |
|
| 254 |
+// a manifest, fallback to GET. |
|
| 255 |
+func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
|
|
| 256 |
+ u, err := t.ub.BuildManifestURL(t.name, tag) |
|
| 257 |
+ if err != nil {
|
|
| 258 |
+ return distribution.Descriptor{}, err
|
|
| 259 |
+ } |
|
| 260 |
+ var attempts int |
|
| 261 |
+ resp, err := t.client.Head(u) |
|
| 262 |
+ |
|
| 263 |
+check: |
|
| 264 |
+ if err != nil {
|
|
| 265 |
+ return distribution.Descriptor{}, err
|
|
| 266 |
+ } |
|
| 267 |
+ |
|
| 268 |
+ switch {
|
|
| 269 |
+ case resp.StatusCode >= 200 && resp.StatusCode < 400: |
|
| 270 |
+ return descriptorFromResponse(resp) |
|
| 271 |
+ case resp.StatusCode == http.StatusMethodNotAllowed: |
|
| 272 |
+ resp, err = t.client.Get(u) |
|
| 273 |
+ attempts++ |
|
| 274 |
+ if attempts > 1 {
|
|
| 275 |
+ return distribution.Descriptor{}, err
|
|
| 276 |
+ } |
|
| 277 |
+ goto check |
|
| 278 |
+ default: |
|
| 279 |
+ return distribution.Descriptor{}, HandleErrorResponse(resp)
|
|
| 215 | 280 |
} |
| 216 |
- return nil, handleErrorResponse(resp) |
|
| 217 | 281 |
} |
| 218 | 282 |
|
| 219 |
-func (ms *manifests) Exists(dgst digest.Digest) (bool, error) {
|
|
| 220 |
- // Call by Tag endpoint since the API uses the same |
|
| 221 |
- // URL endpoint for tags and digests. |
|
| 222 |
- return ms.ExistsByTag(dgst.String()) |
|
| 283 |
+func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
|
|
| 284 |
+ panic("not implemented")
|
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
|
|
| 288 |
+ panic("not implemented")
|
|
| 223 | 289 |
} |
| 224 | 290 |
|
| 225 |
-func (ms *manifests) ExistsByTag(tag string) (bool, error) {
|
|
| 226 |
- u, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 291 |
+func (t *tags) Untag(ctx context.Context, tag string) error {
|
|
| 292 |
+ panic("not implemented")
|
|
| 293 |
+} |
|
| 294 |
+ |
|
| 295 |
+type manifests struct {
|
|
| 296 |
+ name string |
|
| 297 |
+ ub *v2.URLBuilder |
|
| 298 |
+ client *http.Client |
|
| 299 |
+ etags map[string]string |
|
| 300 |
+} |
|
| 301 |
+ |
|
| 302 |
+func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
|
|
| 303 |
+ u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) |
|
| 227 | 304 |
if err != nil {
|
| 228 | 305 |
return false, err |
| 229 | 306 |
} |
| ... | ... |
@@ -238,49 +315,66 @@ func (ms *manifests) ExistsByTag(tag string) (bool, error) {
|
| 238 | 238 |
} else if resp.StatusCode == http.StatusNotFound {
|
| 239 | 239 |
return false, nil |
| 240 | 240 |
} |
| 241 |
- return false, handleErrorResponse(resp) |
|
| 242 |
-} |
|
| 243 |
- |
|
| 244 |
-func (ms *manifests) Get(dgst digest.Digest) (*schema1.SignedManifest, error) {
|
|
| 245 |
- // Call by Tag endpoint since the API uses the same |
|
| 246 |
- // URL endpoint for tags and digests. |
|
| 247 |
- return ms.GetByTag(dgst.String()) |
|
| 241 |
+ return false, HandleErrorResponse(resp) |
|
| 248 | 242 |
} |
| 249 | 243 |
|
| 250 |
-// AddEtagToTag allows a client to supply an eTag to GetByTag which will be |
|
| 244 |
+// AddEtagToTag allows a client to supply an eTag to Get which will be |
|
| 251 | 245 |
// used for a conditional HTTP request. If the eTag matches, a nil manifest |
| 252 |
-// and nil error will be returned. etag is automatically quoted when added to |
|
| 253 |
-// this map. |
|
| 246 |
+// and ErrManifestNotModified error will be returned. etag is automatically |
|
| 247 |
+// quoted when added to this map. |
|
| 254 | 248 |
func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
|
| 255 |
- return func(ms distribution.ManifestService) error {
|
|
| 256 |
- if ms, ok := ms.(*manifests); ok {
|
|
| 257 |
- ms.etags[tag] = fmt.Sprintf(`"%s"`, etag) |
|
| 258 |
- return nil |
|
| 259 |
- } |
|
| 260 |
- return fmt.Errorf("etag options is a client-only option")
|
|
| 249 |
+ return etagOption{tag, etag}
|
|
| 250 |
+} |
|
| 251 |
+ |
|
| 252 |
+type etagOption struct{ tag, etag string }
|
|
| 253 |
+ |
|
| 254 |
+func (o etagOption) Apply(ms distribution.ManifestService) error {
|
|
| 255 |
+ if ms, ok := ms.(*manifests); ok {
|
|
| 256 |
+ ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) |
|
| 257 |
+ return nil |
|
| 261 | 258 |
} |
| 259 |
+ return fmt.Errorf("etag options is a client-only option")
|
|
| 262 | 260 |
} |
| 263 | 261 |
|
| 264 |
-func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) {
|
|
| 262 |
+func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
|
|
| 263 |
+ |
|
| 264 |
+ var tag string |
|
| 265 | 265 |
for _, option := range options {
|
| 266 |
- err := option(ms) |
|
| 267 |
- if err != nil {
|
|
| 268 |
- return nil, err |
|
| 266 |
+ if opt, ok := option.(withTagOption); ok {
|
|
| 267 |
+ tag = opt.tag |
|
| 268 |
+ } else {
|
|
| 269 |
+ err := option.Apply(ms) |
|
| 270 |
+ if err != nil {
|
|
| 271 |
+ return nil, err |
|
| 272 |
+ } |
|
| 269 | 273 |
} |
| 270 | 274 |
} |
| 271 | 275 |
|
| 272 |
- u, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 276 |
+ var ref string |
|
| 277 |
+ if tag != "" {
|
|
| 278 |
+ ref = tag |
|
| 279 |
+ } else {
|
|
| 280 |
+ ref = dgst.String() |
|
| 281 |
+ } |
|
| 282 |
+ |
|
| 283 |
+ u, err := ms.ub.BuildManifestURL(ms.name, ref) |
|
| 273 | 284 |
if err != nil {
|
| 274 | 285 |
return nil, err |
| 275 | 286 |
} |
| 287 |
+ |
|
| 276 | 288 |
req, err := http.NewRequest("GET", u, nil)
|
| 277 | 289 |
if err != nil {
|
| 278 | 290 |
return nil, err |
| 279 | 291 |
} |
| 280 | 292 |
|
| 281 |
- if _, ok := ms.etags[tag]; ok {
|
|
| 282 |
- req.Header.Set("If-None-Match", ms.etags[tag])
|
|
| 293 |
+ for _, t := range distribution.ManifestMediaTypes() {
|
|
| 294 |
+ req.Header.Add("Accept", t)
|
|
| 295 |
+ } |
|
| 296 |
+ |
|
| 297 |
+ if _, ok := ms.etags[ref]; ok {
|
|
| 298 |
+ req.Header.Set("If-None-Match", ms.etags[ref])
|
|
| 283 | 299 |
} |
| 300 |
+ |
|
| 284 | 301 |
resp, err := ms.client.Do(req) |
| 285 | 302 |
if err != nil {
|
| 286 | 303 |
return nil, err |
| ... | ... |
@@ -289,44 +383,89 @@ func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServic |
| 289 | 289 |
if resp.StatusCode == http.StatusNotModified {
|
| 290 | 290 |
return nil, distribution.ErrManifestNotModified |
| 291 | 291 |
} else if SuccessStatus(resp.StatusCode) {
|
| 292 |
- var sm schema1.SignedManifest |
|
| 293 |
- decoder := json.NewDecoder(resp.Body) |
|
| 292 |
+ mt := resp.Header.Get("Content-Type")
|
|
| 293 |
+ body, err := ioutil.ReadAll(resp.Body) |
|
| 294 | 294 |
|
| 295 |
- if err := decoder.Decode(&sm); err != nil {
|
|
| 295 |
+ if err != nil {
|
|
| 296 |
+ return nil, err |
|
| 297 |
+ } |
|
| 298 |
+ m, _, err := distribution.UnmarshalManifest(mt, body) |
|
| 299 |
+ if err != nil {
|
|
| 296 | 300 |
return nil, err |
| 297 | 301 |
} |
| 298 |
- return &sm, nil |
|
| 302 |
+ return m, nil |
|
| 303 |
+ } |
|
| 304 |
+ return nil, HandleErrorResponse(resp) |
|
| 305 |
+} |
|
| 306 |
+ |
|
| 307 |
+// WithTag allows a tag to be passed into Put which enables the client |
|
| 308 |
+// to build a correct URL. |
|
| 309 |
+func WithTag(tag string) distribution.ManifestServiceOption {
|
|
| 310 |
+ return withTagOption{tag}
|
|
| 311 |
+} |
|
| 312 |
+ |
|
| 313 |
+type withTagOption struct{ tag string }
|
|
| 314 |
+ |
|
| 315 |
+func (o withTagOption) Apply(m distribution.ManifestService) error {
|
|
| 316 |
+ if _, ok := m.(*manifests); ok {
|
|
| 317 |
+ return nil |
|
| 299 | 318 |
} |
| 300 |
- return nil, handleErrorResponse(resp) |
|
| 319 |
+ return fmt.Errorf("withTagOption is a client-only option")
|
|
| 301 | 320 |
} |
| 302 | 321 |
|
| 303 |
-func (ms *manifests) Put(m *schema1.SignedManifest) error {
|
|
| 304 |
- manifestURL, err := ms.ub.BuildManifestURL(ms.name, m.Tag) |
|
| 322 |
+// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the |
|
| 323 |
+// tag name in order to build the correct upload URL. This state is written and read under a lock. |
|
| 324 |
+func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
|
|
| 325 |
+ var tag string |
|
| 326 |
+ |
|
| 327 |
+ for _, option := range options {
|
|
| 328 |
+ if opt, ok := option.(withTagOption); ok {
|
|
| 329 |
+ tag = opt.tag |
|
| 330 |
+ } else {
|
|
| 331 |
+ err := option.Apply(ms) |
|
| 332 |
+ if err != nil {
|
|
| 333 |
+ return "", err |
|
| 334 |
+ } |
|
| 335 |
+ } |
|
| 336 |
+ } |
|
| 337 |
+ |
|
| 338 |
+ manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 305 | 339 |
if err != nil {
|
| 306 |
- return err |
|
| 340 |
+ return "", err |
|
| 307 | 341 |
} |
| 308 | 342 |
|
| 309 |
- // todo(richardscothern): do something with options here when they become applicable |
|
| 343 |
+ mediaType, p, err := m.Payload() |
|
| 344 |
+ if err != nil {
|
|
| 345 |
+ return "", err |
|
| 346 |
+ } |
|
| 310 | 347 |
|
| 311 |
- putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(m.Raw))
|
|
| 348 |
+ putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p))
|
|
| 312 | 349 |
if err != nil {
|
| 313 |
- return err |
|
| 350 |
+ return "", err |
|
| 314 | 351 |
} |
| 315 | 352 |
|
| 353 |
+ putRequest.Header.Set("Content-Type", mediaType)
|
|
| 354 |
+ |
|
| 316 | 355 |
resp, err := ms.client.Do(putRequest) |
| 317 | 356 |
if err != nil {
|
| 318 |
- return err |
|
| 357 |
+ return "", err |
|
| 319 | 358 |
} |
| 320 | 359 |
defer resp.Body.Close() |
| 321 | 360 |
|
| 322 | 361 |
if SuccessStatus(resp.StatusCode) {
|
| 323 |
- // TODO(dmcgowan): make use of digest header |
|
| 324 |
- return nil |
|
| 362 |
+ dgstHeader := resp.Header.Get("Docker-Content-Digest")
|
|
| 363 |
+ dgst, err := digest.ParseDigest(dgstHeader) |
|
| 364 |
+ if err != nil {
|
|
| 365 |
+ return "", err |
|
| 366 |
+ } |
|
| 367 |
+ |
|
| 368 |
+ return dgst, nil |
|
| 325 | 369 |
} |
| 326 |
- return handleErrorResponse(resp) |
|
| 370 |
+ |
|
| 371 |
+ return "", HandleErrorResponse(resp) |
|
| 327 | 372 |
} |
| 328 | 373 |
|
| 329 |
-func (ms *manifests) Delete(dgst digest.Digest) error {
|
|
| 374 |
+func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
|
|
| 330 | 375 |
u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) |
| 331 | 376 |
if err != nil {
|
| 332 | 377 |
return err |
| ... | ... |
@@ -345,9 +484,14 @@ func (ms *manifests) Delete(dgst digest.Digest) error {
|
| 345 | 345 |
if SuccessStatus(resp.StatusCode) {
|
| 346 | 346 |
return nil |
| 347 | 347 |
} |
| 348 |
- return handleErrorResponse(resp) |
|
| 348 |
+ return HandleErrorResponse(resp) |
|
| 349 | 349 |
} |
| 350 | 350 |
|
| 351 |
+// todo(richardscothern): Restore interface and implementation with merge of #1050 |
|
| 352 |
+/*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
|
|
| 353 |
+ panic("not supported")
|
|
| 354 |
+}*/ |
|
| 355 |
+ |
|
| 351 | 356 |
type blobs struct {
|
| 352 | 357 |
name string |
| 353 | 358 |
ub *v2.URLBuilder |
| ... | ... |
@@ -377,11 +521,7 @@ func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Des |
| 377 | 377 |
} |
| 378 | 378 |
|
| 379 | 379 |
func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
|
| 380 |
- desc, err := bs.Stat(ctx, dgst) |
|
| 381 |
- if err != nil {
|
|
| 382 |
- return nil, err |
|
| 383 |
- } |
|
| 384 |
- reader, err := bs.Open(ctx, desc.Digest) |
|
| 380 |
+ reader, err := bs.Open(ctx, dgst) |
|
| 385 | 381 |
if err != nil {
|
| 386 | 382 |
return nil, err |
| 387 | 383 |
} |
| ... | ... |
@@ -401,7 +541,7 @@ func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.Rea |
| 401 | 401 |
if resp.StatusCode == http.StatusNotFound {
|
| 402 | 402 |
return distribution.ErrBlobUnknown |
| 403 | 403 |
} |
| 404 |
- return handleErrorResponse(resp) |
|
| 404 |
+ return HandleErrorResponse(resp) |
|
| 405 | 405 |
}), nil |
| 406 | 406 |
} |
| 407 | 407 |
|
| ... | ... |
@@ -457,7 +597,7 @@ func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
| 457 | 457 |
location: location, |
| 458 | 458 |
}, nil |
| 459 | 459 |
} |
| 460 |
- return nil, handleErrorResponse(resp) |
|
| 460 |
+ return nil, HandleErrorResponse(resp) |
|
| 461 | 461 |
} |
| 462 | 462 |
|
| 463 | 463 |
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
|
| ... | ... |
@@ -488,6 +628,10 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi |
| 488 | 488 |
|
| 489 | 489 |
if SuccessStatus(resp.StatusCode) {
|
| 490 | 490 |
lengthHeader := resp.Header.Get("Content-Length")
|
| 491 |
+ if lengthHeader == "" {
|
|
| 492 |
+ return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u)
|
|
| 493 |
+ } |
|
| 494 |
+ |
|
| 491 | 495 |
length, err := strconv.ParseInt(lengthHeader, 10, 64) |
| 492 | 496 |
if err != nil {
|
| 493 | 497 |
return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err)
|
| ... | ... |
@@ -501,7 +645,7 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi |
| 501 | 501 |
} else if resp.StatusCode == http.StatusNotFound {
|
| 502 | 502 |
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
| 503 | 503 |
} |
| 504 |
- return distribution.Descriptor{}, handleErrorResponse(resp)
|
|
| 504 |
+ return distribution.Descriptor{}, HandleErrorResponse(resp)
|
|
| 505 | 505 |
} |
| 506 | 506 |
|
| 507 | 507 |
func buildCatalogValues(maxEntries int, last string) url.Values {
|
| ... | ... |
@@ -538,7 +682,7 @@ func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
|
| 538 | 538 |
if SuccessStatus(resp.StatusCode) {
|
| 539 | 539 |
return nil |
| 540 | 540 |
} |
| 541 |
- return handleErrorResponse(resp) |
|
| 541 |
+ return HandleErrorResponse(resp) |
|
| 542 | 542 |
} |
| 543 | 543 |
|
| 544 | 544 |
func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
|
| 545 | 545 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/docker/distribution/context" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// TagService provides access to information about tagged objects. |
|
| 7 |
+type TagService interface {
|
|
| 8 |
+ // Get retrieves the descriptor identified by the tag. Some |
|
| 9 |
+ // implementations may differentiate between "trusted" tags and |
|
| 10 |
+ // "untrusted" tags. If a tag is "untrusted", the mapping will be returned |
|
| 11 |
+ // as an ErrTagUntrusted error, with the target descriptor. |
|
| 12 |
+ Get(ctx context.Context, tag string) (Descriptor, error) |
|
| 13 |
+ |
|
| 14 |
+ // Tag associates the tag with the provided descriptor, updating the |
|
| 15 |
+ // current association, if needed. |
|
| 16 |
+ Tag(ctx context.Context, tag string, desc Descriptor) error |
|
| 17 |
+ |
|
| 18 |
+ // Untag removes the given tag association |
|
| 19 |
+ Untag(ctx context.Context, tag string) error |
|
| 20 |
+ |
|
| 21 |
+ // All returns the set of tags managed by this tag service |
|
| 22 |
+ All(ctx context.Context) ([]string, error) |
|
| 23 |
+ |
|
| 24 |
+ // Lookup returns the set of tags referencing the given digest. |
|
| 25 |
+ Lookup(ctx context.Context, digest Descriptor) ([]string, error) |
|
| 26 |
+} |