Merge pull request #17557 from aaronlehmann/update-distribution

Vendor updated version of docker/distribution
This commit is contained in:
Derek McGowan 2015-11-02 14:37:04 -08:00
commit 6964f42350
48 changed files with 1026 additions and 616 deletions

View file

@ -11,7 +11,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/docker/image"
"github.com/docker/docker/pkg/broadcaster"
"github.com/docker/docker/pkg/progressreader"
@ -244,7 +244,7 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (tagUpdated
if unverifiedManifest == nil {
return false, fmt.Errorf("image manifest does not exist for tag %q", tag)
}
var verifiedManifest *manifest.Manifest
var verifiedManifest *schema1.Manifest
verifiedManifest, err = verifyManifest(unverifiedManifest, tag)
if err != nil {
return false, err
@ -440,7 +440,7 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (tagUpdated
return tagUpdated, nil
}
func verifyManifest(signedManifest *manifest.SignedManifest, tag string) (m *manifest.Manifest, err error) {
func verifyManifest(signedManifest *schema1.SignedManifest, tag string) (m *schema1.Manifest, err error) {
// If pull by digest, then verify the manifest digest. NOTE: It is
// important to do this first, before any other content validation. If the
// digest cannot be verified, don't even bother with those other things.
@ -464,7 +464,7 @@ func verifyManifest(signedManifest *manifest.SignedManifest, tag string) (m *man
return nil, err
}
var verifiedManifest manifest.Manifest
var verifiedManifest schema1.Manifest
if err = json.Unmarshal(payload, &verifiedManifest); err != nil {
return nil, err
}
@ -487,7 +487,7 @@ func verifyManifest(signedManifest *manifest.SignedManifest, tag string) (m *man
// fixManifestLayers removes repeated layers from the manifest and checks the
// correctness of the parent chain.
func fixManifestLayers(m *manifest.Manifest) error {
func fixManifestLayers(m *schema1.Manifest) error {
images := make([]*image.Image, len(m.FSLayers))
for i := range m.FSLayers {
img, err := image.NewImgJSON([]byte(m.History[i].V1Compatibility))
@ -533,7 +533,7 @@ func fixManifestLayers(m *manifest.Manifest) error {
// getImageInfos returns an imageinfo struct for every image in the manifest.
// These objects contain both calculated strongIDs and compatibilityIDs found
// in v1Compatibility object.
func (p *v2Puller) getImageInfos(m *manifest.Manifest) ([]contentAddressableDescriptor, error) {
func (p *v2Puller) getImageInfos(m *schema1.Manifest) ([]contentAddressableDescriptor, error) {
imgs := make([]contentAddressableDescriptor, len(m.FSLayers))
var parent digest.Digest

View file

@ -8,32 +8,32 @@ import (
"testing"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
)
// TestFixManifestLayers checks that fixManifestLayers removes a duplicate
// layer, and that it makes no changes to the manifest when called a second
// time, after the duplicate is removed.
func TestFixManifestLayers(t *testing.T) {
duplicateLayerManifest := manifest.Manifest{
FSLayers: []manifest.FSLayer{
duplicateLayerManifest := schema1.Manifest{
FSLayers: []schema1.FSLayer{
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
},
History: []manifest.History{
History: []schema1.History{
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
},
}
duplicateLayerManifestExpectedOutput := manifest.Manifest{
FSLayers: []manifest.FSLayer{
duplicateLayerManifestExpectedOutput := schema1.Manifest{
FSLayers: []schema1.FSLayer{
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
},
History: []manifest.History{
History: []schema1.History{
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
},
@ -61,13 +61,13 @@ func TestFixManifestLayers(t *testing.T) {
// TestFixManifestLayersBaseLayerParent makes sure that fixManifestLayers fails
// if the base layer configuration specifies a parent.
func TestFixManifestLayersBaseLayerParent(t *testing.T) {
duplicateLayerManifest := manifest.Manifest{
FSLayers: []manifest.FSLayer{
duplicateLayerManifest := schema1.Manifest{
FSLayers: []schema1.FSLayer{
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
},
History: []manifest.History{
History: []schema1.History{
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"parent\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
@ -83,13 +83,13 @@ func TestFixManifestLayersBaseLayerParent(t *testing.T) {
// if an image configuration specifies a parent that doesn't directly follow
// that (deduplicated) image in the image history.
func TestFixManifestLayersBadParent(t *testing.T) {
duplicateLayerManifest := manifest.Manifest{
FSLayers: []manifest.FSLayer{
duplicateLayerManifest := schema1.Manifest{
FSLayers: []schema1.FSLayer{
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
},
History: []manifest.History{
History: []schema1.History{
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9\",\"parent\":\"ac3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:11.368300679Z\",\"container\":\"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT [\\\"/go/bin/dnsdock\\\"]\"],\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":null,\"Image\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":[\"/go/bin/dnsdock\"],\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"},
{V1Compatibility: "{\"id\":\"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02\",\"created\":\"2015-08-19T16:49:07.568027497Z\",\"container\":\"fe9e5a5264a843c9292d17b736c92dd19bdb49986a8782d7389964ddaff887cc\",\"container_config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"cd /go/src/github.com/tonistiigi/dnsdock \\u0026\\u0026 go get -v github.com/tools/godep \\u0026\\u0026 godep restore \\u0026\\u0026 go install -ldflags \\\"-X main.version `git describe --tags HEAD``if [[ -n $(command git status --porcelain --untracked-files=no 2\\u003e/dev/null) ]]; then echo \\\"-dirty\\\"; fi`\\\" ./...\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"docker_version\":\"1.6.2\",\"config\":{\"Hostname\":\"03797203757d\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"GOLANG_VERSION=1.4.1\",\"GOPATH=/go\"],\"Cmd\":[\"/bin/bash\"],\"Image\":\"e3b0ff09e647595dafee15c54cd632c900df9e82b1d4d313b1e20639a1461779\",\"Volumes\":null,\"WorkingDir\":\"/go\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":118430532}\n"},
@ -113,7 +113,7 @@ func TestValidateManifest(t *testing.T) {
t.Fatal("error reading fixture:", err)
}
var goodSignedManifest manifest.SignedManifest
var goodSignedManifest schema1.SignedManifest
err = json.Unmarshal(goodManifestBytes, &goodSignedManifest)
if err != nil {
t.Fatal("error unmarshaling manifest:", err)
@ -135,7 +135,7 @@ func TestValidateManifest(t *testing.T) {
t.Fatal("error reading fixture:", err)
}
var extraDataSignedManifest manifest.SignedManifest
var extraDataSignedManifest schema1.SignedManifest
err = json.Unmarshal(extraDataManifestBytes, &extraDataSignedManifest)
if err != nil {
t.Fatal("error unmarshaling manifest:", err)
@ -157,7 +157,7 @@ func TestValidateManifest(t *testing.T) {
t.Fatal("error reading fixture:", err)
}
var badSignedManifest manifest.SignedManifest
var badSignedManifest schema1.SignedManifest
err = json.Unmarshal(badManifestBytes, &badSignedManifest)
if err != nil {
t.Fatal("error unmarshaling manifest:", err)
@ -177,8 +177,9 @@ func TestValidateManifest(t *testing.T) {
t.Fatal("error reading fixture:", err)
}
var noSignatureSignedManifest manifest.SignedManifest
err = json.Unmarshal(noSignatureManifestBytes, &noSignatureSignedManifest)
var noSignatureSignedManifest schema1.SignedManifest
noSignatureSignedManifest.Raw = noSignatureManifestBytes
err = json.Unmarshal(noSignatureManifestBytes, &noSignatureSignedManifest.Manifest)
if err != nil {
t.Fatal("error unmarshaling manifest:", err)
}

View file

@ -11,6 +11,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/docker/image"
"github.com/docker/docker/pkg/progressreader"
"github.com/docker/docker/pkg/streamformatter"
@ -103,15 +104,15 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
return err
}
m := &manifest.Manifest{
m := &schema1.Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 1,
},
Name: p.repo.Name(),
Tag: tag,
Architecture: layer.Architecture,
FSLayers: []manifest.FSLayer{},
History: []manifest.History{},
FSLayers: []schema1.FSLayer{},
History: []schema1.History{},
}
var metadata runconfig.Config
@ -192,15 +193,15 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
return err
}
m.FSLayers = append(m.FSLayers, manifest.FSLayer{BlobSum: dgst})
m.History = append(m.History, manifest.History{V1Compatibility: string(jsonData)})
m.FSLayers = append(m.FSLayers, schema1.FSLayer{BlobSum: dgst})
m.History = append(m.History, schema1.History{V1Compatibility: string(jsonData)})
layersSeen[layer.ID] = true
p.layersPushed[dgst] = true
}
logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
signed, err := manifest.Sign(m, p.trustKey)
signed, err := schema1.Sign(m, p.trustKey)
if err != nil {
return err
}

View file

@ -10,7 +10,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
@ -100,7 +100,7 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd
return client.NewRepository(ctx, repoName, endpoint.URL, tr)
}
func digestFromManifest(m *manifest.SignedManifest, localName string) (digest.Digest, int, error) {
func digestFromManifest(m *schema1.SignedManifest, localName string) (digest.Digest, int, error) {
payload, err := m.Payload()
if err != nil {
// If this failed, the signatures section was corrupted

View file

@ -2,13 +2,16 @@ package tags
import (
"fmt"
"regexp"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/reference"
)
// DefaultTag defines the default tag used when performing images related actions and no tag string is specified
const DefaultTag = "latest"
var anchoredTagRegexp = regexp.MustCompile(`^` + reference.TagRegexp.String() + `$`)
// ErrTagInvalidFormat is returned if tag is invalid.
type ErrTagInvalidFormat struct {
name string
@ -20,13 +23,13 @@ func (e ErrTagInvalidFormat) Error() string {
// ValidateTagName validates the name of a tag.
// It returns an error if the given name is an emtpy string.
// If name does not match v2.TagNameAnchoredRegexp regexp, it returns ErrTagInvalidFormat
// If name is not valid, it returns ErrTagInvalidFormat
func ValidateTagName(name string) error {
if name == "" {
return fmt.Errorf("tag name can't be empty")
}
if !v2.TagNameAnchoredRegexp.MatchString(name) {
if !anchoredTagRegexp.MatchString(name) {
return ErrTagInvalidFormat{name}
}
return nil

View file

@ -39,7 +39,7 @@ clone git github.com/hashicorp/consul v0.5.2
clone git github.com/boltdb/bolt v1.1.0
# get graph and distribution packages
clone git github.com/docker/distribution 20c4b7a1805a52753dfd593ee1cc35558722a0ce # docker/1.9 branch
clone git github.com/docker/distribution c6c9194e9c6097f84b0ff468a741086ff7704aa3
clone git github.com/vbatts/tar-split v0.9.10
clone git github.com/docker/notary 089d8450d8928aa1c58fd03f09cabbde9bcb4590

View file

@ -7,7 +7,7 @@ import (
"strings"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/utils"
@ -481,22 +481,22 @@ func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
// Load the target manifest blob.
manifestBlob := s.reg.readBlobContents(c, manifestDigest)
var imgManifest manifest.Manifest
var imgManifest schema1.Manifest
if err := json.Unmarshal(manifestBlob, &imgManifest); err != nil {
c.Fatalf("unable to decode image manifest from blob: %s", err)
}
// Add a malicious layer digest to the list of layers in the manifest.
imgManifest.FSLayers = append(imgManifest.FSLayers, manifest.FSLayer{
// Change a layer in the manifest.
imgManifest.FSLayers[0] = schema1.FSLayer{
BlobSum: digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
})
}
// Move the existing data file aside, so that we can replace it with a
// malicious blob of data. NOTE: we defer the returned undo func.
undo := s.reg.tempMoveBlobData(c, manifestDigest)
defer undo()
alteredManifestBlob, err := json.Marshal(imgManifest)
alteredManifestBlob, err := json.MarshalIndent(imgManifest, "", " ")
if err != nil {
c.Fatalf("unable to encode altered image manifest to JSON: %s", err)
}
@ -531,7 +531,7 @@ func (s *DockerRegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) {
// Load the target manifest blob.
manifestBlob := s.reg.readBlobContents(c, manifestDigest)
var imgManifest manifest.Manifest
var imgManifest schema1.Manifest
if err := json.Unmarshal(manifestBlob, &imgManifest); err != nil {
c.Fatalf("unable to decode image manifest from blob: %s", err)
}

View file

@ -101,12 +101,12 @@ func (s *DockerSuite) TestTagWithPrefixHyphen(c *check.C) {
// test repository name begin with '-'
out, _, err := dockerCmdWithError("tag", "busybox:latest", "-busybox:test")
c.Assert(err, checker.NotNil, check.Commentf(out))
c.Assert(out, checker.Contains, "repository name component must match", check.Commentf("tag a name begin with '-' should failed"))
c.Assert(out, checker.Contains, "invalid reference format", check.Commentf("tag a name begin with '-' should failed"))
// test namespace name begin with '-'
out, _, err = dockerCmdWithError("tag", "busybox:latest", "-test/busybox:test")
c.Assert(err, checker.NotNil, check.Commentf(out))
c.Assert(out, checker.Contains, "repository name component must match", check.Commentf("tag a name begin with '-' should failed"))
c.Assert(out, checker.Contains, "invalid reference format", check.Commentf("tag a name begin with '-' should failed"))
// test index name begin with '-'
out, _, err = dockerCmdWithError("tag", "busybox:latest", "-index:5000/busybox:test")

View file

@ -8,7 +8,7 @@ import (
"net/url"
"strings"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/reference"
"github.com/docker/docker/image"
"github.com/docker/docker/opts"
flag "github.com/docker/docker/pkg/mflag"
@ -226,7 +226,8 @@ func validateRemoteName(remoteName string) error {
}
}
return v2.ValidateRepositoryName(remoteName)
_, err := reference.WithName(remoteName)
return err
}
func validateNoSchema(reposName string) error {

View file

@ -190,7 +190,7 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
func shouldV2Fallback(err errcode.Error) bool {
logrus.Debugf("v2 error: %T %v", err, err)
switch err.Code {
case v2.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
return true
}
return false

View file

@ -776,6 +776,9 @@ func TestValidRemoteName(t *testing.T) {
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
if err := validateRemoteName(repositoryName); err != nil {
@ -803,8 +806,7 @@ func TestValidRemoteName(t *testing.T) {
"_docker/_docker",
// Disallow consecutive underscores and periods.
"dock__er/docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",

View file

@ -3,4 +3,5 @@ Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com>
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com>
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu>
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com>

View file

@ -1,6 +1,8 @@
Aaron Lehmann <aaron.lehmann@docker.com>
Adam Enger <adamenger@gmail.com>
Adrian Mouat <adrian.mouat@gmail.com>
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
Alex Chan <alex.chan@metaswitch.com>
Alex Elman <aelman@indeed.com>
Amy Lindburg <amy.lindburg@docker.com>
Andrey Kostov <kostov.andrey@gmail.com>
@ -8,47 +10,63 @@ Andy Goldstein <agoldste@redhat.com>
Anton Tiurin <noxiouz@yandex.ru>
Antonio Mercado <amercado@thinknode.com>
Arnaud Porterie <arnaud.porterie@docker.com>
Ayose Cazorla <ayosec@gmail.com>
BadZen <dave.trombley@gmail.com>
Ben Firshman <ben@firshman.co.uk>
bin liu <liubin0329@gmail.com>
Brian Bland <brian.bland@docker.com>
burnettk <burnettk@gmail.com>
Chris Dillon <squarism@gmail.com>
Daisuke Fujita <dtanshi45@gmail.com>
Darren Shepherd <darren@rancher.com>
Dave Trombley <dave.trombley@gmail.com>
Dave Tucker <dt@docker.com>
David Lawrence <david.lawrence@docker.com>
David Verhasselt <david@crowdway.com>
David Xia <dxia@spotify.com>
davidli <wenquan.li@hp.com>
Derek McGowan <derek@mcgstyle.net>
Diogo Mónica <diogo.monica@gmail.com>
Donald Huang <don.hcd@gmail.com>
Doug Davis <dug@us.ibm.com>
Florentin Raud <florentin.raud@gmail.com>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
Henri Gomez <henri.gomez@gmail.com>
Hu Keping <hukeping@huawei.com>
Ian Babrou <ibobrik@gmail.com>
Jeff Nickoloff <jeff@allingeek.com>
Jessie Frazelle <jfrazelle@users.noreply.github.com>
Jianqing Wang <tsing@jianqing.org>
Jon Poler <jonathan.poler@apcera.com>
Jordan Liggitt <jliggitt@redhat.com>
Josh Hawn <josh.hawn@docker.com>
Julien Fernandez <julien.fernandez@gmail.com>
Kelsey Hightower <kelsey.hightower@gmail.com>
Kenneth Lim <kennethlimcp@gmail.com>
Li Yi <denverdino@gmail.com>
Luke Carpenter <x@rubynerd.net>
Mary Anthony <mary@docker.com>
Matt Bentley <mbentley@mbentley.net>
Matt Robenolt <matt@ydekproductions.com>
Michael Prokop <mika@grml.org>
moxiegirl <mary@docker.com>
Nathan Sullivan <nathan@nightsys.net>
nevermosby <robolwq@qq.com>
Nghia Tran <tcnghia@gmail.com>
Oilbeater <liumengxinfly@gmail.com>
Olivier Gambier <olivier@docker.com>
Olivier Jacques <olivier.jacques@hp.com>
Patrick Devine <patrick.devine@docker.com>
Philip Misiowiec <philip@atlashealth.com>
Richard Scothern <richard.scothern@docker.com>
Richard Scothern <richard.scothern@gmail.com>
Sebastiaan van Stijn <github@gone.nl>
Shawn Falkner-Horine <dreadpirateshawn@gmail.com>
Shreyas Karnik <karnik.shreyas@gmail.com>
Simon Thulbourn <simon+github@thulbourn.com>
Spencer Rinehart <anubis@overthemonkey.com>
Stephen J Day <stephen.day@docker.com>
Sylvain Baubeau <sbaubeau@redhat.com>
tgic <farmer1992@gmail.com>
Thomas Sjögren <konstruktoid@users.noreply.github.com>
Tianon Gravi <admwiggin@gmail.com>
Tibor Vass <teabee89@gmail.com>

View file

@ -33,6 +33,7 @@ By following these simple rules you will get better and faster feedback on your
- please refrain from adding "same thing here" or "+1" comments
- you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button
- comment if you have some new, technical and relevant information to add to the case
- __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue.
### If you have not found an existing issue that describes your problem:

View file

@ -6,14 +6,14 @@ RUN apt-get update && \
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV GOPATH $DISTRIBUTION_DIR/Godeps/_workspace:$GOPATH
ENV DOCKER_BUILDTAGS include_rados
ENV DOCKER_BUILDTAGS include_rados include_oss include_gcs
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR
COPY cmd/registry/config-dev.yml $DISTRIBUTION_DIR/cmd/registry/config.yml
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml
RUN make PREFIX=/go clean binaries
VOLUME ["/var/lib/registry"]
EXPOSE 5000
ENTRYPOINT ["registry"]
CMD ["cmd/registry/config.yml"]
CMD ["/etc/docker/registry/config.yml"]

View file

@ -28,18 +28,20 @@ ${PREFIX}/bin/registry: version/version.go $(shell find . -type f -name '*.go')
@echo "+ $@"
@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry
${PREFIX}/bin/digest: version/version.go $(shell find . -type f -name '*.go')
@echo "+ $@"
@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/digest
${PREFIX}/bin/registry-api-descriptor-template: version/version.go $(shell find . -type f -name '*.go')
@echo "+ $@"
@go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry-api-descriptor-template
${PREFIX}/bin/dist: version/version.go $(shell find . -type f -name '*.go')
@echo "+ $@"
@go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/dist
docs/spec/api.md: docs/spec/api.md.tmpl ${PREFIX}/bin/registry-api-descriptor-template
./bin/registry-api-descriptor-template $< > $@
vet:
# Depends on binaries because vet will silently fail if it can't load compiled
# imports
vet: binaries
@echo "+ $@"
@go vet ./...
@ -64,11 +66,9 @@ test-full:
@echo "+ $@"
@go test ./...
binaries: ${PREFIX}/bin/registry ${PREFIX}/bin/registry-api-descriptor-template ${PREFIX}/bin/dist
binaries: ${PREFIX}/bin/registry ${PREFIX}/bin/digest ${PREFIX}/bin/registry-api-descriptor-template
@echo "+ $@"
clean:
@echo "+ $@"
@rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/registry-api-descriptor-template"

View file

@ -15,7 +15,6 @@ This repository contains the following components:
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. |
| **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**. |
| **dist** | An _experimental_ tool to provide distribution, oriented functionality without the `docker` daemon. |
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) |
| **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. |
@ -69,6 +68,14 @@ image repository for images used to test or in continuous integration. For these
use cases and others, [deploying your own registry instance](docs/deploying.md)
may be the better choice.
### Migration to Registry 2.0
For those who have previously deployed their own registry based on the Registry
1.0 implementation and wish to deploy a Registry 2.0 while retaining images,
data migration is required. A tool to assist with migration efforts has been
created. For more information see [docker/migrator]
(https://github.com/docker/migrator).
## Contribute
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute

View file

@ -103,20 +103,20 @@ via IRC or the mailing list and we can talk about adding it. The goal here is
to make sure that new features go through a rigid design process before
landing in the registry.
##### Mirroring and Pull-through Caching
##### Proxying to other Registries
Mirroring and pull-through caching are related but slight different. We've
adopted the term _mirroring_ to be a proper mirror of a registry, meaning it
has all the content the upstream would have. Providing such mirrors in the
Docker ecosystem is dependent on a solid trust system, which is still in the
works.
A _pull-through caching_ mode exists for the registry, but is restricted from
within the docker client to only mirror the official Docker Hub. This functionality
can be expanded when image provenance has been specified and implemented in the
distribution project.
The more commonly helpful feature is _pull-through caching_, where data is
fetched from an upstream when not available in a local registry instance.
##### Metadata storage
Please see the following issues:
- https://github.com/docker/distribution/issues/459
Metadata for the registry is currently stored with the manifest and layer data on
the storage backend. While this is a big win for simplicity and reliably maintaining
state, it comes with the cost of consistency and high latency. The mutable registry
metadata operations should be abstracted behind an API which will allow ACID compliant
storage systems to handle metadata.
##### Peer to Peer transfer
@ -263,11 +263,5 @@ just the registry.
### Project Planning
Distribution Components map to Docker Platform Releases via the use of labels. Project Pages are used to define the set of features that are included in each Docker Platform Release.
| Platform Version | Label | Planning |
|-----------|------|-----|
| Docker 1.6 | [Docker/1.6](https://github.com/docker/distribution/labels/docker%2F1.6) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.6-Project-Page) |
| Docker 1.7| [Docker/1.7](https://github.com/docker/distribution/labels/docker%2F1.7) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.7-Project-Page) |
| Docker 1.8| [Docker/1.8](https://github.com/docker/distribution/labels/docker%2F1.8) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.8-Project-Page) |
An [Open-Source Planning Process](https://github.com/docker/distribution/wiki/Open-Source-Planning-Process) is used to define the Roadmap. [Project Pages](https://github.com/docker/distribution/wiki) define the goals for each Milestone and identify current progress.

View file

@ -27,9 +27,6 @@ var (
// ErrBlobInvalidLength returned when the blob has an expected length on
// commit, meaning mismatched with the descriptor or an invalid value.
ErrBlobInvalidLength = errors.New("blob invalid length")
// ErrUnsupported returned when an unsupported operation is attempted
ErrUnsupported = errors.New("unsupported operation")
)
// ErrBlobInvalidDigest returned when digest check fails.
@ -183,6 +180,9 @@ type BlobWriter interface {
// result in a no-op. This allows use of Cancel in a defer statement,
// increasing the assurance that it is correctly called.
Cancel(ctx context.Context) error
// Get a reader to the blob being written by this BlobWriter
Reader() (io.ReadCloser, error)
}
// BlobService combines the operations to access, read and write blobs. This

View file

@ -21,7 +21,7 @@ machine:
BASE_OLD: ../../../$HOME/.gvm/pkgsets/old/global/$BASE_DIR
BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR
# BASE_BLEED: ../../../$HOME/.gvm/pkgsets/bleed/global/$BASE_DIR
DOCKER_BUILDTAGS: "include_rados"
DOCKER_BUILDTAGS: "include_rados include_oss include_gcs"
# Workaround Circle parsing dumb bugs and/or YAML wonkyness
CIRCLE_PAIN: "mode: set"
# Ceph config
@ -75,6 +75,11 @@ test:
- gvm use stable && go version
# - gvm use bleed && go version
# First thing: build everything. This will catch compile errors, and it's
# also necessary for go vet to work properly (see #807).
- gvm use stable && godep go install ./...:
pwd: $BASE_STABLE
# FMT
- gvm use stable && test -z "$(gofmt -s -l . | grep -v Godeps/_workspace/src/ | tee /dev/stderr)":
pwd: $BASE_STABLE

View file

@ -3,6 +3,19 @@
// logging relevent request information but this package is not limited to
// that purpose.
//
// The easiest way to get started is to get the background context:
//
// ctx := context.Background()
//
// The returned context should be passed around your application and be the
// root of all other context instances. If the application has a version, this
// line should be called before anything else:
//
// ctx := context.WithVersion(context.Background(), version)
//
// The above will store the version in the context and will be available to
// the logger.
//
// Logging
//
// The most useful aspect of this package is GetLogger. This function takes

View file

@ -103,12 +103,21 @@ func GetRequestID(ctx Context) string {
// WithResponseWriter returns a new context and response writer that makes
// interesting response statistics available within the context.
func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) {
irw := &instrumentedResponseWriter{
irw := instrumentedResponseWriter{
ResponseWriter: w,
Context: ctx,
}
return irw, irw
if closeNotifier, ok := w.(http.CloseNotifier); ok {
irwCN := &instrumentedResponseWriterCN{
instrumentedResponseWriter: irw,
CloseNotifier: closeNotifier,
}
return irwCN, irwCN
}
return &irw, &irw
}
// GetResponseWriter returns the http.ResponseWriter from the provided
@ -258,8 +267,17 @@ func (ctx *muxVarsContext) Value(key interface{}) interface{} {
return ctx.Context.Value(key)
}
// instrumentedResponseWriterCN provides response writer information in a
// context. It implements http.CloseNotifier so that users can detect
// early disconnects.
type instrumentedResponseWriterCN struct {
instrumentedResponseWriter
http.CloseNotifier
}
// instrumentedResponseWriter provides response writer information in a
// context.
// context. This variant is only used in the case where CloseNotifier is not
// implemented by the parent ResponseWriter.
type instrumentedResponseWriter struct {
http.ResponseWriter
Context
@ -334,3 +352,13 @@ func (irw *instrumentedResponseWriter) Value(key interface{}) interface{} {
fallback:
return irw.Context.Value(key)
}
func (irw *instrumentedResponseWriterCN) Value(key interface{}) interface{} {
if keyStr, ok := key.(string); ok {
if keyStr == "http.response" {
return irw
}
}
return irw.instrumentedResponseWriter.Value(key)
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/Sirupsen/logrus"
"runtime"
)
// Logger provides a leveled-logging interface.
@ -54,8 +55,14 @@ func GetLoggerWithField(ctx Context, key, value interface{}, keys ...interface{}
// GetLoggerWithFields returns a logger instance with the specified fields
// without affecting the context. Extra specified keys will be resolved from
// the context.
func GetLoggerWithFields(ctx Context, fields map[string]interface{}, keys ...interface{}) Logger {
return getLogrusLogger(ctx, keys...).WithFields(logrus.Fields(fields))
func GetLoggerWithFields(ctx Context, fields map[interface{}]interface{}, keys ...interface{}) Logger {
// must convert from interface{} -> interface{} to string -> interface{} for logrus.
lfields := make(logrus.Fields, len(fields))
for key, value := range fields {
lfields[fmt.Sprint(key)] = value
}
return getLogrusLogger(ctx, keys...).WithFields(lfields)
}
// GetLogger returns the logger from the current context, if present. If one
@ -84,12 +91,20 @@ func getLogrusLogger(ctx Context, keys ...interface{}) *logrus.Entry {
}
if logger == nil {
fields := logrus.Fields{}
// Fill in the instance id, if we have it.
instanceID := ctx.Value("instance.id")
if instanceID != nil {
fields["instance.id"] = instanceID
}
fields["go.version"] = runtime.Version()
// If no logger is found, just return the standard logger.
logger = logrus.NewEntry(logrus.StandardLogger())
logger = logrus.StandardLogger().WithFields(fields)
}
fields := logrus.Fields{}
for _, key := range keys {
v := ctx.Value(key)
if v != nil {

View file

@ -20,7 +20,7 @@ func Since(ctx Context, key interface{}) time.Duration {
// GetStringValue returns a string value from the context. The empty string
// will be returned if not found.
func GetStringValue(ctx Context, key string) (value string) {
func GetStringValue(ctx Context, key interface{}) (value string) {
stringi := ctx.Value(key)
if stringi != nil {
if valuev, ok := stringi.(string); ok {

View file

@ -0,0 +1,16 @@
package context
// WithVersion stores the application version in the context. The new context
// gets a logger to ensure log messages are marked with the application
// version.
func WithVersion(ctx Context, version string) Context {
ctx = WithValue(ctx, "version", version)
// push a new logger onto the stack
return WithLogger(ctx, GetLogger(ctx, "version"))
}
// GetVersion returns the application version from the context. An empty
// string may returned if the version was not set on the context.
func GetVersion(ctx Context) string {
return GetStringValue(ctx, "version")
}

View file

@ -70,15 +70,10 @@ func ParseDigest(s string) (Digest, error) {
return d, d.Validate()
}
// FromReader returns the most valid digest for the underlying content.
// FromReader returns the most valid digest for the underlying content using
// the canonical digest algorithm.
func FromReader(rd io.Reader) (Digest, error) {
digester := Canonical.New()
if _, err := io.Copy(digester.Hash(), rd); err != nil {
return "", err
}
return digester.Digest(), nil
return Canonical.FromReader(rd)
}
// FromTarArchive produces a tarsum digest from reader rd.

View file

@ -3,6 +3,7 @@ package digest
import (
"crypto"
"hash"
"io"
)
// Algorithm identifies and implementation of a digester by an identifier.
@ -49,6 +50,22 @@ func (a Algorithm) Available() bool {
return h.Available()
}
func (a Algorithm) String() string {
return string(a)
}
// Set implemented to allow use of Algorithm as a command line flag.
func (a *Algorithm) Set(value string) error {
if value == "" {
*a = Canonical
} else {
// just do a type conversion, support is queried with Available.
*a = Algorithm(value)
}
return nil
}
// New returns a new digester for the specified algorithm. If the algorithm
// does not have a digester implementation, nil will be returned. This can be
// checked by calling Available before calling New.
@ -69,6 +86,17 @@ func (a Algorithm) Hash() hash.Hash {
return algorithms[a].New()
}
// FromReader returns the digest of the reader using the algorithm.
func (a Algorithm) FromReader(rd io.Reader) (Digest, error) {
digester := a.New()
if _, err := io.Copy(digester.Hash(), rd); err != nil {
return "", err
}
return digester.Digest(), nil
}
// TODO(stevvooe): Allow resolution of verifiers using the digest type and
// this registration system.

View file

@ -4,6 +4,7 @@ import (
"errors"
"sort"
"strings"
"sync"
)
var (
@ -27,6 +28,7 @@ var (
// the complete set of digests. To mitigate collisions, an
// appropriately long short code should be used.
type Set struct {
mutex sync.RWMutex
entries digestEntries
}
@ -63,6 +65,8 @@ func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool {
// with an empty digest value. If multiple matches are found
// ErrDigestAmbiguous will be returned with an empty digest value.
func (dst *Set) Lookup(d string) (Digest, error) {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
if len(dst.entries) == 0 {
return "", ErrDigestNotFound
}
@ -101,13 +105,15 @@ func (dst *Set) Lookup(d string) (Digest, error) {
return dst.entries[idx].digest, nil
}
// Add adds the given digests to the set. An error will be returned
// Add adds the given digest to the set. An error will be returned
// if the given digest is invalid. If the digest already exists in the
// table, this operation will be a no-op.
// set, this operation will be a no-op.
func (dst *Set) Add(d Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
@ -130,12 +136,56 @@ func (dst *Set) Add(d Digest) error {
return nil
}
// Remove removes the given digest from the set. An err will be
// returned if the given digest is invalid. If the digest does
// not exist in the set, this operation will be a no-op.
func (dst *Set) Remove(d Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
return dst.entries[i].alg >= entry.alg
}
return dst.entries[i].val >= entry.val
}
idx := sort.Search(len(dst.entries), searchFunc)
// Not found if idx is after or value at idx is not digest
if idx == len(dst.entries) || dst.entries[idx].digest != d {
return nil
}
entries := dst.entries
copy(entries[idx:], entries[idx+1:])
entries = entries[:len(entries)-1]
dst.entries = entries
return nil
}
// All returns all the digests in the set
func (dst *Set) All() []Digest {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
retValues := make([]Digest, len(dst.entries))
for i := range dst.entries {
retValues[i] = dst.entries[i].digest
}
return retValues
}
// ShortCodeTable returns a map of Digest to unique short codes. The
// length represents the minimum value, the maximum length may be the
// entire value of digest if uniqueness cannot be achieved without the
// full value. This function will attempt to make short codes as short
// as possible to be unique.
func ShortCodeTable(dst *Set, length int) map[Digest]string {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
m := make(map[Digest]string, len(dst.entries))
l := length
resetIdx := 0

View file

@ -6,7 +6,7 @@ import (
"regexp"
)
// TarSumRegexp defines a regular expression to match tarsum identifiers.
// TarsumRegexp defines a regular expression to match tarsum identifiers.
var TarsumRegexp = regexp.MustCompile("tarsum(?:.[a-z0-9]+)?\\+[a-zA-Z0-9]+:[A-Fa-f0-9]+")
// TarsumRegexpCapturing defines a regular expression to match tarsum identifiers with

View file

@ -1,12 +1,21 @@
package distribution
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/digest"
)
// ErrManifestNotModified is returned when a conditional manifest GetByTag
// returns nil due to the client indicating it has the latest version
var ErrManifestNotModified = errors.New("manifest not modified")
// ErrUnsupported is returned when an unimplemented or unsupported action is
// performed
var ErrUnsupported = errors.New("operation unsupported")
// ErrRepositoryUnknown is returned if the named repository is not known by
// the registry.
type ErrRepositoryUnknown struct {

View file

@ -0,0 +1 @@
package manifest

View file

@ -1,9 +1,10 @@
package manifest
package schema1
import (
"encoding/json"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/libtrust"
)
@ -17,18 +18,18 @@ const (
ManifestMediaType = "application/vnd.docker.distribution.manifest.v1+json"
)
// Versioned provides a struct with just the manifest schemaVersion. Incoming
// content with unknown schema version can be decoded against this struct to
// check the version.
type Versioned struct {
// SchemaVersion is the image manifest schema that this image follows
SchemaVersion int `json:"schemaVersion"`
}
var (
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
SchemaVersion = manifest.Versioned{
SchemaVersion: 1,
}
)
// Manifest provides the base accessible fields for working with V2 image
// format in the registry.
type Manifest struct {
Versioned
manifest.Versioned
// Name is the name of the image's repository
Name string `json:"name"`
@ -61,15 +62,20 @@ type SignedManifest struct {
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
sm.Raw = make([]byte, len(b), len(b))
copy(sm.Raw, b)
p, err := sm.Payload()
if err != nil {
return err
}
var manifest Manifest
if err := json.Unmarshal(b, &manifest); err != nil {
if err := json.Unmarshal(p, &manifest); err != nil {
return err
}
sm.Manifest = manifest
sm.Raw = make([]byte, len(b), len(b))
copy(sm.Raw, b)
return nil
}

View file

@ -1,4 +1,4 @@
package manifest
package schema1
import (
"crypto/x509"

View file

@ -1,4 +1,4 @@
package manifest
package schema1
import (
"crypto/x509"

View file

@ -0,0 +1,9 @@
package manifest
// Versioned provides a struct with just the manifest schemaVersion. Incoming
// content with unknown schema version can be decoded against this struct to
// check the version.
type Versioned struct {
// SchemaVersion is the image manifest schema that this image follows
SchemaVersion int `json:"schemaVersion"`
}

View file

@ -0,0 +1,341 @@
// Package reference provides a general type to represent any way of referencing images within the registry.
// Its main purpose is to abstract tags and digests (content-addressable hash).
//
// Grammar
//
// reference := repository [ ":" tag ] [ "@" digest ]
//
// // repository.go
// repository := hostname ['/' component]+
// hostname := hostcomponent [':' port-number]
// component := subcomponent [separator subcomponent]*
// subcomponent := alpha-numeric ['-'* alpha-numeric]*
// hostcomponent := [hostpart '.']* hostpart
// alpha-numeric := /[a-z0-9]+/
// separator := /([_.]|__)/
// port-number := /[0-9]+/
// hostpart := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/
//
// // tag.go
// tag := /[\w][\w.-]{0,127}/
//
// // from the digest package
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]
// digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; Atleast 128 bit digest value
package reference
import (
"errors"
"fmt"
"github.com/docker/distribution/digest"
)
const (
// NameTotalLengthMax is the maximum total number of characters in a repository name.
NameTotalLengthMax = 255
)
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
ErrReferenceInvalidFormat = errors.New("invalid reference format")
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
ErrTagInvalidFormat = errors.New("invalid tag format")
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format")
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than
// RepositoryNameTotalLengthMax
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
type Reference interface {
// String returns the full reference
String() string
}
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
type Field struct {
reference Reference
}
// AsField wraps a reference in a Field for encoding.
func AsField(reference Reference) Field {
return Field{reference}
}
// Reference unwraps the reference type from the field to
// return the Reference object. This object should be
// of the appropriate type to further check for different
// reference types.
func (f Field) Reference() Reference {
return f.reference
}
// MarshalText serializes the field to byte text which
// is the string of the reference.
func (f Field) MarshalText() (p []byte, err error) {
return []byte(f.reference.String()), nil
}
// UnmarshalText parses text bytes by invoking the
// reference parser to ensure the appropriately
// typed reference object is wrapped by field.
func (f *Field) UnmarshalText(p []byte) error {
r, err := Parse(string(p))
if err != nil {
return err
}
f.reference = r
return nil
}
// Named is an object with a full name
type Named interface {
Reference
Name() string
}
// Tagged is an object which has a tag
type Tagged interface {
Reference
Tag() string
}
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
Named
Tag() string
}
// Digested is an object which has a digest
// in which it can be referenced by
type Digested interface {
Reference
Digest() digest.Digest
}
// Canonical reference is an object with a fully unique
// name including a name with hostname and digest
type Canonical interface {
Named
Digest() digest.Digest
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
func SplitHostname(named Named) (string, string) {
name := named.Name()
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
if s == "" {
return nil, ErrNameEmpty
}
// TODO(dmcgowan): Provide more specific and helpful error
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
ref := reference{
name: matches[1],
tag: matches[2],
}
if matches[3] != "" {
var err error
ref.digest, err = digest.ParseDigest(matches[3])
if err != nil {
return nil, err
}
}
r := getBestReferenceType(ref)
if r == nil {
return nil, ErrNameEmpty
}
return r, nil
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name, otherwise an error is
// returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
ref, err := Parse(s)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
if !anchoredNameRegexp.MatchString(name) {
return nil, ErrReferenceInvalidFormat
}
return repository(name), nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
if !anchoredTagRegexp.MatchString(tag) {
return nil, ErrTagInvalidFormat
}
return taggedReference{
name: name.Name(),
tag: tag,
}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat
}
return canonicalReference{
name: name.Name(),
digest: digest,
}, nil
}
func getBestReferenceType(ref reference) Reference {
if ref.name == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
}
return nil
}
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
name: ref.name,
digest: ref.digest,
}
}
return repository(ref.name)
}
if ref.digest == "" {
return taggedReference{
name: ref.name,
tag: ref.tag,
}
}
return ref
}
type reference struct {
name string
tag string
digest digest.Digest
}
func (r reference) String() string {
return r.name + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Name() string {
return r.name
}
func (r reference) Tag() string {
return r.tag
}
func (r reference) Digest() digest.Digest {
return r.digest
}
type repository string
func (r repository) String() string {
return string(r)
}
func (r repository) Name() string {
return string(r)
}
type digestReference digest.Digest
func (d digestReference) String() string {
return d.String()
}
func (d digestReference) Digest() digest.Digest {
return digest.Digest(d)
}
type taggedReference struct {
name string
tag string
}
func (t taggedReference) String() string {
return t.name + ":" + t.tag
}
func (t taggedReference) Name() string {
return t.name
}
func (t taggedReference) Tag() string {
return t.tag
}
type canonicalReference struct {
name string
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.name + "@" + c.digest.String()
}
func (c canonicalReference) Name() string {
return c.name
}
func (c canonicalReference) Digest() digest.Digest {
return c.digest
}

View file

@ -0,0 +1,49 @@
package reference
import "regexp"
var (
// nameSubComponentRegexp defines the part of the name which must be
// begin and end with an alphanumeric character. These characters can
// be separated by any number of dashes.
nameSubComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[-]+[a-z0-9]+)*`)
// nameComponentRegexp restricts registry path component names to
// start with at least one letter or number, with following parts able to
// be separated by one period, underscore or double underscore.
nameComponentRegexp = regexp.MustCompile(nameSubComponentRegexp.String() + `(?:(?:[._]|__)` + nameSubComponentRegexp.String() + `)*`)
nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String())
hostnameComponentRegexp = regexp.MustCompile(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`)
// hostnameComponentRegexp restricts the registry hostname component of a repository name to
// start with a component as defined by hostnameRegexp and followed by an optional port.
hostnameRegexp = regexp.MustCompile(`(?:` + hostnameComponentRegexp.String() + `\.)*` + hostnameComponentRegexp.String() + `(?::[0-9]+)?`)
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`)
// DigestRegexp matches valid digests.
DigestRegexp = regexp.MustCompile(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = regexp.MustCompile(`^` + DigestRegexp.String() + `$`)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the hostname and name part omitting
// the seperating forward slash from either.
NameRegexp = regexp.MustCompile(`(?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String())
// ReferenceRegexp is the full supported format of a reference. The
// regexp has capturing groups for name, tag, and digest components.
ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@](` + DigestRegexp.String() + `))?$`)
// anchoredNameRegexp is used to parse a name value, capturing hostname
anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`)
)

View file

@ -3,7 +3,7 @@ package distribution
import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
)
// Scope defines the set of items that match a namespace.
@ -76,13 +76,13 @@ type ManifestService interface {
Exists(dgst digest.Digest) (bool, error)
// Get retrieves the identified by the digest, if it exists.
Get(dgst digest.Digest) (*manifest.SignedManifest, error)
Get(dgst digest.Digest) (*schema1.SignedManifest, error)
// Delete removes the manifest, if it exists.
Delete(dgst digest.Digest) error
// Put creates or updates the manifest.
Put(manifest *manifest.SignedManifest) error
Put(manifest *schema1.SignedManifest) error
// TODO(stevvooe): The methods after this message should be moved to a
// discrete TagService, per active proposals.
@ -94,7 +94,7 @@ type ManifestService interface {
ExistsByTag(tag string) (bool, error)
// GetByTag retrieves the named manifest, if it exists.
GetByTag(tag string, options ...ManifestServiceOption) (*manifest.SignedManifest, error)
GetByTag(tag string, options ...ManifestServiceOption) (*schema1.SignedManifest, error)
// TODO(stevvooe): There are several changes that need to be done to this
// interface:

View file

@ -13,15 +13,57 @@ var (
groupToDescriptors = map[string][]ErrorDescriptor{}
)
// ErrorCodeUnknown is a generic error that can be used as a last
// resort if there is no situation-specific error message that can be used
var ErrorCodeUnknown = Register("errcode", ErrorDescriptor{
Value: "UNKNOWN",
Message: "unknown error",
Description: `Generic error returned when the error does not have an
var (
// ErrorCodeUnknown is a generic error that can be used as a last
// resort if there is no situation-specific error message that can be used
ErrorCodeUnknown = Register("errcode", ErrorDescriptor{
Value: "UNKNOWN",
Message: "unknown error",
Description: `Generic error returned when the error does not have an
API classification.`,
HTTPStatusCode: http.StatusInternalServerError,
})
HTTPStatusCode: http.StatusInternalServerError,
})
// ErrorCodeUnsupported is returned when an operation is not supported.
ErrorCodeUnsupported = Register("errcode", ErrorDescriptor{
Value: "UNSUPPORTED",
Message: "The operation is unsupported.",
Description: `The operation was unsupported due to a missing
implementation or invalid set of parameters.`,
HTTPStatusCode: http.StatusMethodNotAllowed,
})
// ErrorCodeUnauthorized is returned if a request requires
// authentication.
ErrorCodeUnauthorized = Register("errcode", ErrorDescriptor{
Value: "UNAUTHORIZED",
Message: "authentication required",
Description: `The access controller was unable to authenticate
the client. Often this will be accompanied by a
Www-Authenticate HTTP response header indicating how to
authenticate.`,
HTTPStatusCode: http.StatusUnauthorized,
})
// ErrorCodeDenied is returned if a client does not have sufficient
// permission to perform an action.
ErrorCodeDenied = Register("errcode", ErrorDescriptor{
Value: "DENIED",
Message: "requested access to the resource is denied",
Description: `The access controller denied access for the
operation on a resource.`,
HTTPStatusCode: http.StatusForbidden,
})
// ErrorCodeUnavailable provides a common error to report unavialability
// of a service or endpoint.
ErrorCodeUnavailable = Register("errcode", ErrorDescriptor{
Value: "UNAVAILABLE",
Message: "service unavailable",
Description: "Returned when a service is not available",
HTTPStatusCode: http.StatusServiceUnavailable,
})
)
var nextCode = 1000
var registerLock sync.Mutex

View file

@ -5,6 +5,7 @@ import (
"regexp"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
)
@ -12,7 +13,7 @@ var (
nameParameterDescriptor = ParameterDescriptor{
Name: "name",
Type: "string",
Format: RepositoryNameRegexp.String(),
Format: reference.NameRegexp.String(),
Required: true,
Description: `Name of the target repository.`,
}
@ -20,7 +21,7 @@ var (
referenceParameterDescriptor = ParameterDescriptor{
Name: "reference",
Type: "string",
Format: TagNameRegexp.String(),
Format: reference.TagRegexp.String(),
Required: true,
Description: `Tag or digest of the target manifest.`,
}
@ -111,45 +112,67 @@ var (
},
}
unauthorizedResponse = ResponseDescriptor{
Description: "The client does not have access to the repository.",
unauthorizedResponseDescriptor = ResponseDescriptor{
Name: "Authentication Required",
StatusCode: http.StatusUnauthorized,
Description: "The client is not authenticated.",
Headers: []ParameterDescriptor{
authChallengeHeader,
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON error response body.",
Description: "Length of the JSON response body.",
Format: "<length>",
},
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: unauthorizedErrorsBody,
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
errcode.ErrorCodeUnauthorized,
},
}
unauthorizedResponsePush = ResponseDescriptor{
Description: "The client does not have access to push to the repository.",
StatusCode: http.StatusUnauthorized,
repositoryNotFoundResponseDescriptor = ResponseDescriptor{
Name: "No Such Repository Error",
StatusCode: http.StatusNotFound,
Description: "The repository is not known to the registry.",
Headers: []ParameterDescriptor{
authChallengeHeader,
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON error response body.",
Description: "Length of the JSON response body.",
Format: "<length>",
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
ErrorCodeNameUnknown,
},
}
deniedResponseDescriptor = ResponseDescriptor{
Name: "Access Denied",
StatusCode: http.StatusForbidden,
Description: "The client does not have required access to the repository.",
Headers: []ParameterDescriptor{
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON response body.",
Format: "<length>",
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: unauthorizedErrorsBody,
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
errcode.ErrorCodeDenied,
},
}
)
@ -345,7 +368,7 @@ var routeDescriptors = []RouteDescriptor{
Name: RouteNameBase,
Path: "/v2/",
Entity: "Base",
Description: `Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authorization.`,
Description: `Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authentication.`,
Methods: []MethodDescriptor{
{
Method: "GET",
@ -363,24 +386,11 @@ var routeDescriptors = []RouteDescriptor{
},
},
Failures: []ResponseDescriptor{
{
Description: "The client is not authorized to access the registry.",
StatusCode: http.StatusUnauthorized,
Headers: []ParameterDescriptor{
authChallengeHeader,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
},
{
Description: "The registry does not implement the V2 API.",
StatusCode: http.StatusNotFound,
},
unauthorizedResponseDescriptor,
},
},
},
@ -389,7 +399,7 @@ var routeDescriptors = []RouteDescriptor{
},
{
Name: RouteNameTags,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list",
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list",
Entity: "Tags",
Description: "Retrieve information about tags.",
Methods: []MethodDescriptor{
@ -432,28 +442,9 @@ var routeDescriptors = []RouteDescriptor{
},
},
Failures: []ResponseDescriptor{
{
StatusCode: http.StatusNotFound,
Description: "The repository is not known to the registry.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeNameUnknown,
},
},
{
StatusCode: http.StatusUnauthorized,
Description: "The client does not have access to the repository.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
{
@ -487,28 +478,9 @@ var routeDescriptors = []RouteDescriptor{
},
},
Failures: []ResponseDescriptor{
{
StatusCode: http.StatusNotFound,
Description: "The repository is not known to the registry.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeNameUnknown,
},
},
{
StatusCode: http.StatusUnauthorized,
Description: "The client does not have access to the repository.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -517,9 +489,9 @@ var routeDescriptors = []RouteDescriptor{
},
{
Name: RouteNameManifest,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
Entity: "Manifest",
Description: "Create, update and retrieve manifests.",
Description: "Create, update, delete and retrieve manifests.",
Methods: []MethodDescriptor{
{
Method: "GET",
@ -536,7 +508,7 @@ var routeDescriptors = []RouteDescriptor{
},
Successes: []ResponseDescriptor{
{
Description: "The manifest idenfied by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.",
Description: "The manifest identified by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.",
StatusCode: http.StatusOK,
Headers: []ParameterDescriptor{
digestHeader,
@ -560,29 +532,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
{
StatusCode: http.StatusUnauthorized,
Description: "The client does not have access to the repository.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
},
{
Description: "The named manifest is not known to the registry.",
StatusCode: http.StatusNotFound,
ErrorCodes: []errcode.ErrorCode{
ErrorCodeNameUnknown,
ErrorCodeManifestUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -637,17 +589,9 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUnknown,
},
},
{
StatusCode: http.StatusUnauthorized,
Description: "The client does not have permission to push to the repository.",
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
{
Name: "Missing Layer(s)",
Description: "One or more layers may be missing during a manifest upload. If so, the missing layers will be enumerated in the error response.",
@ -671,22 +615,11 @@ var routeDescriptors = []RouteDescriptor{
},
},
{
StatusCode: http.StatusUnauthorized,
Headers: []ParameterDescriptor{
authChallengeHeader,
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON error response body.",
Format: "<length>",
},
},
Name: "Not allowed",
Description: "Manifest put is not allowed because the registry is configured as a pull-through cache or for some other reason",
StatusCode: http.StatusMethodNotAllowed,
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
errcode.ErrorCodeUnsupported,
},
},
},
@ -725,25 +658,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
{
StatusCode: http.StatusUnauthorized,
Headers: []ParameterDescriptor{
authChallengeHeader,
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON error response body.",
Format: "<length>",
},
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeUnauthorized,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
{
Name: "Unknown Manifest",
Description: "The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.",
@ -757,6 +674,14 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
{
Name: "Not allowed",
Description: "Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.",
StatusCode: http.StatusMethodNotAllowed,
ErrorCodes: []errcode.ErrorCode{
errcode.ErrorCodeUnsupported,
},
},
},
},
},
@ -766,11 +691,10 @@ var routeDescriptors = []RouteDescriptor{
{
Name: RouteNameBlob,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
Entity: "Blob",
Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by digest.",
Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.",
Methods: []MethodDescriptor{
{
Method: "GET",
Description: "Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.",
@ -830,7 +754,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponse,
{
Description: "The blob, identified by `name` and `digest`, is unknown to the registry.",
StatusCode: http.StatusNotFound,
@ -843,6 +766,9 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUnknown,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
{
@ -899,7 +825,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponse,
{
StatusCode: http.StatusNotFound,
ErrorCodes: []errcode.ErrorCode{
@ -915,10 +840,80 @@ var routeDescriptors = []RouteDescriptor{
Description: "The range specification cannot be satisfied for the requested content. This can happen when the range is not formatted correctly or if the range is outside of the valid size of the content.",
StatusCode: http.StatusRequestedRangeNotSatisfiable,
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
},
{
Method: "DELETE",
Description: "Delete the blob identified by `name` and `digest`",
Requests: []RequestDescriptor{
{
Headers: []ParameterDescriptor{
hostHeader,
authHeader,
},
PathParameters: []ParameterDescriptor{
nameParameterDescriptor,
digestPathParameter,
},
Successes: []ResponseDescriptor{
{
StatusCode: http.StatusAccepted,
Headers: []ParameterDescriptor{
{
Name: "Content-Length",
Type: "integer",
Description: "0",
Format: "0",
},
digestHeader,
},
},
},
Failures: []ResponseDescriptor{
{
Name: "Invalid Name or Digest",
StatusCode: http.StatusBadRequest,
ErrorCodes: []errcode.ErrorCode{
ErrorCodeDigestInvalid,
ErrorCodeNameInvalid,
},
},
{
Description: "The blob, identified by `name` and `digest`, is unknown to the registry.",
StatusCode: http.StatusNotFound,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
ErrorCodeNameUnknown,
ErrorCodeBlobUnknown,
},
},
{
Description: "Blob delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled",
StatusCode: http.StatusMethodNotAllowed,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
errcode.ErrorCodeUnsupported,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
},
// TODO(stevvooe): We may want to add a PUT request here to
// kickoff an upload of a blob, integrated with the blob upload
// API.
@ -927,8 +922,8 @@ var routeDescriptors = []RouteDescriptor{
{
Name: RouteNameBlobUpload,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/",
Entity: "Intiate Blob Upload",
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/",
Entity: "Initiate Blob Upload",
Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
Methods: []MethodDescriptor{
{
@ -987,7 +982,17 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeNameInvalid,
},
},
unauthorizedResponsePush,
{
Name: "Not allowed",
Description: "Blob upload is not allowed because the registry is configured as a pull-through cache or for some other reason",
StatusCode: http.StatusMethodNotAllowed,
ErrorCodes: []errcode.ErrorCode{
errcode.ErrorCodeUnsupported,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
{
@ -1031,7 +1036,9 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeNameInvalid,
},
},
unauthorizedResponsePush,
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -1041,7 +1048,7 @@ var routeDescriptors = []RouteDescriptor{
{
Name: RouteNameBlobUploadChunk,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
Entity: "Blob Upload",
Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
Methods: []MethodDescriptor{
@ -1090,7 +1097,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponse,
{
Description: "The upload is unknown to the registry. The upload must be restarted.",
StatusCode: http.StatusNotFound,
@ -1102,6 +1108,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -1162,7 +1171,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponsePush,
{
Description: "The upload is unknown to the registry. The upload must be restarted.",
StatusCode: http.StatusNotFound,
@ -1174,6 +1182,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
{
@ -1241,7 +1252,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponsePush,
{
Description: "The upload is unknown to the registry. The upload must be restarted.",
StatusCode: http.StatusNotFound,
@ -1257,6 +1267,9 @@ var routeDescriptors = []RouteDescriptor{
Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid.",
StatusCode: http.StatusRequestedRangeNotSatisfiable,
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -1326,13 +1339,13 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeDigestInvalid,
ErrorCodeNameInvalid,
ErrorCodeBlobUploadInvalid,
errcode.ErrorCodeUnsupported,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: errorsBody,
},
},
unauthorizedResponsePush,
{
Description: "The upload is unknown to the registry. The upload must be restarted.",
StatusCode: http.StatusNotFound,
@ -1344,6 +1357,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},
@ -1386,7 +1402,6 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponse,
{
Description: "The upload is unknown to the registry. The client may ignore this error and assume the upload has been deleted.",
StatusCode: http.StatusNotFound,
@ -1398,6 +1413,9 @@ var routeDescriptors = []RouteDescriptor{
Format: errorsBody,
},
},
unauthorizedResponseDescriptor,
repositoryNotFoundResponseDescriptor,
deniedResponseDescriptor,
},
},
},

View file

@ -9,24 +9,6 @@ import (
const errGroup = "registry.api.v2"
var (
// ErrorCodeUnsupported is returned when an operation is not supported.
ErrorCodeUnsupported = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "UNSUPPORTED",
Message: "The operation is unsupported.",
Description: `The operation was unsupported due to a missing
implementation or invalid set of parameters.`,
})
// ErrorCodeUnauthorized is returned if a request is not authorized.
ErrorCodeUnauthorized = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "UNAUTHORIZED",
Message: "access to the requested resource is not authorized",
Description: `The access controller denied access for the operation on
a resource. Often this will be accompanied by a 401 Unauthorized
response status.`,
HTTPStatusCode: http.StatusUnauthorized,
})
// ErrorCodeDigestInvalid is returned when uploading a blob if the
// provided digest does not match the blob contents.
ErrorCodeDigestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{

View file

@ -1,96 +0,0 @@
package v2
import (
"fmt"
"regexp"
"strings"
)
// TODO(stevvooe): Move these definitions to the future "reference" package.
// While they are used with v2 definitions, their relevance expands beyond.
const (
// RepositoryNameTotalLengthMax is the maximum total number of characters in
// a repository name
RepositoryNameTotalLengthMax = 255
)
// domainLabelRegexp represents the following RFC-2396 BNF construct:
// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
var domainLabelRegexp = regexp.MustCompile(`[a-z0-9](?:-*[a-z0-9])*`)
// RepositoryNameComponentRegexp restricts registry path component names to
// the allow valid hostnames according to: https://www.ietf.org/rfc/rfc2396.txt
// with the following differences:
// 1) It DOES NOT allow for fully-qualified domain names, which include a
// trailing '.', e.g. "google.com."
// 2) It DOES NOT restrict 'top-level' domain labels to start with just alpha
// characters.
// 3) It DOES allow for underscores to appear in the same situations as dots.
//
// RFC-2396 uses the BNF construct:
// hostname = *( domainlabel "." ) toplabel [ "." ]
var RepositoryNameComponentRegexp = regexp.MustCompile(
domainLabelRegexp.String() + `(?:[._]` + domainLabelRegexp.String() + `)*`)
// RepositoryNameComponentAnchoredRegexp is the version of
// RepositoryNameComponentRegexp which must completely match the content
var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
// multiple path components, separated by a forward slash.
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
// TagNameAnchoredRegexp matches valid tag names, anchored at the start and
// end of the matched string.
var TagNameAnchoredRegexp = regexp.MustCompile("^" + TagNameRegexp.String() + "$")
var (
// ErrRepositoryNameEmpty is returned for empty, invalid repository names.
ErrRepositoryNameEmpty = fmt.Errorf("repository name must have at least one component")
// ErrRepositoryNameLong is returned when a repository name is longer than
// RepositoryNameTotalLengthMax
ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
// ErrRepositoryNameComponentInvalid is returned when a repository name does
// not match RepositoryNameComponentRegexp
ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
)
// ValidateRepositoryName ensures the repository name is valid for use in the
// registry. This function accepts a superset of what might be accepted by
// docker core or docker hub. If the name does not pass validation, an error,
// describing the conditions, is returned.
//
// Effectively, the name should comply with the following grammar:
//
// alpha-numeric := /[a-z0-9]+/
// separator := /[._-]/
// component := alpha-numeric [separator alpha-numeric]*
// namespace := component ['/' component]*
//
// The result of the production, known as the "namespace", should be limited
// to 255 characters.
func ValidateRepositoryName(name string) error {
if name == "" {
return ErrRepositoryNameEmpty
}
if len(name) > RepositoryNameTotalLengthMax {
return ErrRepositoryNameLong
}
components := strings.Split(name, "/")
for _, component := range components {
if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
return ErrRepositoryNameComponentInvalid
}
}
return nil
}

View file

@ -10,6 +10,7 @@ import (
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/transport"
)
@ -85,11 +86,24 @@ func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
return nil
}
// This is the minimum duration a token can last (in seconds).
// A token must not live less than 60 seconds because older versions
// of the Docker client didn't read their expiration from the token
// response and assumed 60 seconds. So to remain compatible with
// those implementations, a token must live at least this long.
const minimumTokenLifetimeSeconds = 60
// Private interface for time used by this package to enable tests to provide their own implementation.
type clock interface {
Now() time.Time
}
type tokenHandler struct {
header http.Header
creds CredentialStore
scope tokenScope
transport http.RoundTripper
clock clock
tokenLock sync.Mutex
tokenCache string
@ -108,12 +122,24 @@ func (ts tokenScope) String() string {
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
}
// An implementation of clock for providing real time data.
type realClock struct{}
// Now implements clock
func (realClock) Now() time.Time { return time.Now() }
// NewTokenHandler creates a new AuthenicationHandler which supports
// fetching tokens from a remote token server.
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
return newTokenHandler(transport, creds, realClock{}, scope, actions...)
}
// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
return &tokenHandler{
transport: transport,
creds: creds,
clock: c,
scope: tokenScope{
Resource: "repository",
Scope: scope,
@ -146,40 +172,43 @@ func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]st
func (th *tokenHandler) refreshToken(params map[string]string) error {
th.tokenLock.Lock()
defer th.tokenLock.Unlock()
now := time.Now()
now := th.clock.Now()
if now.After(th.tokenExpiration) {
token, err := th.fetchToken(params)
tr, err := th.fetchToken(params)
if err != nil {
return err
}
th.tokenCache = token
th.tokenExpiration = now.Add(time.Minute)
th.tokenCache = tr.Token
th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
}
return nil
}
type tokenResponse struct {
Token string `json:"token"`
Token string `json:"token"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
}
func (th *tokenHandler) fetchToken(params map[string]string) (token string, err error) {
func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
realm, ok := params["realm"]
if !ok {
return "", errors.New("no realm specified for token auth challenge")
return nil, errors.New("no realm specified for token auth challenge")
}
// TODO(dmcgowan): Handle empty scheme
realmURL, err := url.Parse(realm)
if err != nil {
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
}
req, err := http.NewRequest("GET", realmURL.String(), nil)
if err != nil {
return "", err
return nil, err
}
reqParams := req.URL.Query()
@ -206,26 +235,44 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token string, err
resp, err := th.client().Do(req)
if err != nil {
return "", err
return nil, err
}
defer resp.Body.Close()
if !client.SuccessStatus(resp.StatusCode) {
return "", fmt.Errorf("token auth attempt for registry: %s request failed with status: %d %s", req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
return nil, fmt.Errorf("token auth attempt for registry: %s request failed with status: %d %s", req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
}
decoder := json.NewDecoder(resp.Body)
tr := new(tokenResponse)
if err = decoder.Decode(tr); err != nil {
return "", fmt.Errorf("unable to decode token response: %s", err)
return nil, fmt.Errorf("unable to decode token response: %s", err)
}
// `access_token` is equivalent to `token` and if both are specified
// the choice is undefined. Canonicalize `access_token` by sticking
// things in `token`.
if tr.AccessToken != "" {
tr.Token = tr.AccessToken
}
if tr.Token == "" {
return "", errors.New("authorization server did not include a token in the response")
return nil, errors.New("authorization server did not include a token in the response")
}
return tr.Token, nil
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
// The default/minimum lifetime.
tr.ExpiresIn = minimumTokenLifetimeSeconds
}
if tr.IssuedAt.IsZero() {
// issued_at is optional in the token response.
tr.IssuedAt = th.clock.Now()
}
return tr, nil
}
type basicHandler struct {

View file

@ -25,6 +25,10 @@ type httpBlobUpload struct {
closed bool
}
func (hbu *httpBlobUpload) Reader() (io.ReadCloser, error) {
panic("Not implemented")
}
func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error {
if resp.StatusCode == http.StatusNotFound {
return distribution.ErrBlobUploadUnknown

View file

@ -8,7 +8,6 @@ import (
"net/http"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
)
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
@ -52,7 +51,7 @@ func handleErrorResponse(resp *http.Response) error {
if resp.StatusCode == 401 {
err := parseHTTPErrorResponse(resp.Body)
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok {
return v2.ErrorCodeUnauthorized.WithDetail(uErr.Response)
return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
}
return err
}

View file

@ -14,7 +14,8 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/distribution/registry/storage/cache"
@ -96,9 +97,9 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri
return numFilled, returnErr
}
// NewRepository creates a new Repository for the given repository name and base URL
// NewRepository creates a new Repository for the given repository name and base URL.
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
if err := v2.ValidateRepositoryName(name); err != nil {
if _, err := reference.ParseNamed(name); err != nil {
return nil, err
}
@ -211,8 +212,6 @@ func (ms *manifests) Tags() ([]string, error) {
}
return tagsResponse.Tags, nil
} else if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
return nil, handleErrorResponse(resp)
}
@ -242,7 +241,7 @@ func (ms *manifests) ExistsByTag(tag string) (bool, error) {
return false, handleErrorResponse(resp)
}
func (ms *manifests) Get(dgst digest.Digest) (*manifest.SignedManifest, error) {
func (ms *manifests) Get(dgst digest.Digest) (*schema1.SignedManifest, error) {
// Call by Tag endpoint since the API uses the same
// URL endpoint for tags and digests.
return ms.GetByTag(dgst.String())
@ -262,7 +261,7 @@ func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
}
}
func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*manifest.SignedManifest, error) {
func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) {
for _, option := range options {
err := option(ms)
if err != nil {
@ -280,18 +279,17 @@ func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServic
}
if _, ok := ms.etags[tag]; ok {
req.Header.Set("eTag", ms.etags[tag])
req.Header.Set("If-None-Match", ms.etags[tag])
}
resp, err := ms.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
return nil, nil
return nil, distribution.ErrManifestNotModified
} else if SuccessStatus(resp.StatusCode) {
var sm manifest.SignedManifest
var sm schema1.SignedManifest
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&sm); err != nil {
@ -302,7 +300,7 @@ func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServic
return nil, handleErrorResponse(resp)
}
func (ms *manifests) Put(m *manifest.SignedManifest) error {
func (ms *manifests) Put(m *schema1.SignedManifest) error {
manifestURL, err := ms.ub.BuildManifestURL(ms.name, m.Tag)
if err != nil {
return err

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/storage/cache"
)
@ -26,7 +26,7 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider
}
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
if err := v2.ValidateRepositoryName(repo); err != nil {
if _, err := reference.ParseNamed(repo); err != nil {
return nil, err
}

View file

@ -1,178 +0,0 @@
package cache
import (
"testing"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
)
// CheckBlobDescriptorCache takes a cache implementation through a common set
// of operations. If adding new tests, please add them here so new
// implementations get the benefit. This should be used for unit tests.
func CheckBlobDescriptorCache(t *testing.T, provider BlobDescriptorCacheProvider) {
ctx := context.Background()
checkBlobDescriptorCacheEmptyRepository(t, ctx, provider)
checkBlobDescriptorCacheSetAndRead(t, ctx, provider)
}
func checkBlobDescriptorCacheEmptyRepository(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
if _, err := provider.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
t.Fatalf("expected unknown blob error with empty store: %v", err)
}
cache, err := provider.RepositoryScoped("")
if err == nil {
t.Fatalf("expected an error when asking for invalid repo")
}
cache, err = provider.RepositoryScoped("foo/bar")
if err != nil {
t.Fatalf("unexpected error getting repository: %v", err)
}
if err := cache.SetDescriptor(ctx, "", distribution.Descriptor{
Digest: "sha384:abc",
Size: 10,
MediaType: "application/octet-stream"}); err != digest.ErrDigestInvalidFormat {
t.Fatalf("expected error with invalid digest: %v", err)
}
if err := cache.SetDescriptor(ctx, "sha384:abc", distribution.Descriptor{
Digest: "",
Size: 10,
MediaType: "application/octet-stream"}); err == nil {
t.Fatalf("expected error setting value on invalid descriptor")
}
if _, err := cache.Stat(ctx, ""); err != digest.ErrDigestInvalidFormat {
t.Fatalf("expected error checking for cache item with empty digest: %v", err)
}
if _, err := cache.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
t.Fatalf("expected unknown blob error with empty repo: %v", err)
}
}
func checkBlobDescriptorCacheSetAndRead(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
localDigest := digest.Digest("sha384:abc")
expected := distribution.Descriptor{
Digest: "sha256:abc",
Size: 10,
MediaType: "application/octet-stream"}
cache, err := provider.RepositoryScoped("foo/bar")
if err != nil {
t.Fatalf("unexpected error getting scoped cache: %v", err)
}
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
t.Fatalf("error setting descriptor: %v", err)
}
desc, err := cache.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error statting fake2:abc: %v", err)
}
if expected != desc {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// also check that we set the canonical key ("fake:abc")
desc, err = cache.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("descriptor not returned for canonical key: %v", err)
}
if expected != desc {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// ensure that global gets extra descriptor mapping
desc, err = provider.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("expected blob unknown in global cache: %v, %v", err, desc)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// get at it through canonical descriptor
desc, err = provider.Stat(ctx, expected.Digest)
if err != nil {
t.Fatalf("unexpected error checking glboal descriptor: %v", err)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// now, we set the repo local mediatype to something else and ensure it
// doesn't get changed in the provider cache.
expected.MediaType = "application/json"
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
t.Fatalf("unexpected error setting descriptor: %v", err)
}
desc, err = cache.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error getting descriptor: %v", err)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
}
desc, err = provider.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error getting global descriptor: %v", err)
}
expected.MediaType = "application/octet-stream" // expect original mediatype in global
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
}
}
func checkBlobDescriptorClear(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
localDigest := digest.Digest("sha384:abc")
expected := distribution.Descriptor{
Digest: "sha256:abc",
Size: 10,
MediaType: "application/octet-stream"}
cache, err := provider.RepositoryScoped("foo/bar")
if err != nil {
t.Fatalf("unexpected error getting scoped cache: %v", err)
}
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
t.Fatalf("error setting descriptor: %v", err)
}
desc, err := cache.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error statting fake2:abc: %v", err)
}
if expected != desc {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
err = cache.Clear(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error deleting descriptor")
}
nonExistantDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
err = cache.Clear(ctx, nonExistantDigest)
if err == nil {
t.Fatalf("expected error deleting unknown descriptor")
}
}