Updated deps

This commit is contained in:
Travis Reeder
2017-07-12 14:08:59 -07:00
parent 8d669c202c
commit 98539fba8a
133 changed files with 2224 additions and 1659 deletions

View File

@@ -3,6 +3,8 @@ package datastoreutil
import ( import (
"context" "context"
"github.com/jmoiron/sqlx"
"gitlab-odx.oracle.com/odx/functions/api/models" "gitlab-odx.oracle.com/odx/functions/api/models"
) )
@@ -135,3 +137,12 @@ func (v *validator) GetTask(ctx context.Context, callID string) (*models.FnCall,
} }
return v.Datastore.GetTask(ctx, callID) return v.Datastore.GetTask(ctx, callID)
} }
func (v *validator) DeleteLog(ctx context.Context, callID string) error {
return v.Datastore.DeleteLog(ctx, callID)
}
// GetDatabase returns the underlying sqlx database implementation
func (v *validator) GetDatabase() *sqlx.DB {
return v.Datastore.GetDatabase()
}

View File

@@ -3,6 +3,8 @@ package datastore
import ( import (
"context" "context"
"github.com/jmoiron/sqlx"
"gitlab-odx.oracle.com/odx/functions/api/datastore/internal/datastoreutil" "gitlab-odx.oracle.com/odx/functions/api/datastore/internal/datastoreutil"
"gitlab-odx.oracle.com/odx/functions/api/logs" "gitlab-odx.oracle.com/odx/functions/api/logs"
"gitlab-odx.oracle.com/odx/functions/api/models" "gitlab-odx.oracle.com/odx/functions/api/models"
@@ -167,3 +169,8 @@ func (m *mock) GetTask(ctx context.Context, callID string) (*models.FnCall, erro
func (m *mock) GetTasks(ctx context.Context, filter *models.CallFilter) (models.FnCalls, error) { func (m *mock) GetTasks(ctx context.Context, filter *models.CallFilter) (models.FnCalls, error) {
return m.Calls, nil return m.Calls, nil
} }
// GetDatabase returns nil here since shouldn't really be used
func (m *mock) GetDatabase() *sqlx.DB {
return nil
}

View File

@@ -757,3 +757,8 @@ func scanCall(scanner RowScanner, call *models.FnCall) error {
} }
return nil return nil
} }
// GetDatabase returns the underlying sqlx database implementation
func (ds *sqlStore) GetDatabase() *sqlx.DB {
return ds.db
}

View File

@@ -3,6 +3,8 @@ package models
import ( import (
"context" "context"
"errors" "errors"
"github.com/jmoiron/sqlx"
) )
type Datastore interface { type Datastore interface {
@@ -64,6 +66,9 @@ type Datastore interface {
// Implement FnLog methods for convenience // Implement FnLog methods for convenience
FnLog FnLog
// GetDatabase returns the underlying sqlx database implementation
GetDatabase() *sqlx.DB
} }
var ( var (

2
glide.lock generated
View File

@@ -40,7 +40,7 @@ imports:
subpackages: subpackages:
- cli/config/configfile - cli/config/configfile
- name: github.com/docker/distribution - name: github.com/docker/distribution
version: a25b9ef0c9fe242ac04bb20d3a028442b7d266b6 version: f86db6b22663a27ba4d278220b7e34be528b1e79
subpackages: subpackages:
- context - context
- digest - digest

View File

@@ -28,9 +28,7 @@ import:
subpackages: subpackages:
- cli/config/configfile - cli/config/configfile
- package: github.com/docker/distribution - package: github.com/docker/distribution
version: ^2.6.1 branch: master
subpackages:
- manifest/schema1
- package: github.com/fsouza/go-dockerclient - package: github.com/fsouza/go-dockerclient
- package: github.com/garyburd/redigo - package: github.com/garyburd/redigo
subpackages: subpackages:

View File

@@ -71,9 +71,7 @@ commands, such as `go test`, should work per package (please see
A `Makefile` has been provided as a convenience to support repeatable builds. A `Makefile` has been provided as a convenience to support repeatable builds.
Please install the following into `GOPATH` for it to work: Please install the following into `GOPATH` for it to work:
go get github.com/tools/godep github.com/golang/lint/golint go get github.com/golang/lint/golint
**TODO(stevvooe):** Add a `make setup` command to Makefile to run this. Have to think about how to interact with Godeps properly.
Once these commands are available in the `GOPATH`, run `make` to get a full Once these commands are available in the `GOPATH`, run `make` to get a full
build: build:
@@ -105,8 +103,8 @@ build:
+ /Users/sday/go/src/github.com/docker/distribution/bin/registry-api-descriptor-template + /Users/sday/go/src/github.com/docker/distribution/bin/registry-api-descriptor-template
+ binaries + binaries
The above provides a repeatable build using the contents of the vendored The above provides a repeatable build using the contents of the vendor
Godeps directory. This includes formatting, vetting, linting, building, directory. This includes formatting, vetting, linting, building,
testing and generating tagged binaries. We can verify this worked by running testing and generating tagged binaries. We can verify this worked by running
the registry binary generated in the "./bin" directory: the registry binary generated in the "./bin" directory:

View File

@@ -1,11 +1,5 @@
# Changelog # Changelog
## 2.6.1 (2017-04-05)
#### Registry
- Fix `Forwarded` header handling, revert use of `X-Forwarded-Port`
- Use driver `Stat` for registry health check
## 2.6.0 (2017-01-18) ## 2.6.0 (2017-01-18)
#### Storage #### Storage

View File

@@ -1,8 +1,11 @@
FROM golang:1.7-alpine FROM golang:1.8-alpine
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs ENV DOCKER_BUILDTAGS include_oss include_gcs
ARG GOOS=linux
ARG GOARCH=amd64
RUN set -ex \ RUN set -ex \
&& apk add --no-cache make git && apk add --no-cache make git

View File

@@ -1,458 +0,0 @@
{
"ImportPath": "github.com/docker/distribution",
"GoVersion": "go1.6",
"GodepVersion": "v74",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/Azure/azure-sdk-for-go/storage",
"Comment": "v5.0.0-beta-6-g0b5fe2a",
"Rev": "0b5fe2abe0271ba07049eacaa65922d67c319543"
},
{
"ImportPath": "github.com/Sirupsen/logrus",
"Comment": "v0.7.3",
"Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d"
},
{
"ImportPath": "github.com/Sirupsen/logrus/formatters/logstash",
"Comment": "v0.7.3",
"Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/awserr",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/awsutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/client",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/client/metadata",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/corehandlers",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/defaults",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/ec2metadata",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/request",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/session",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/signer/v4",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/restxml",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/service/cloudfront/sign",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/service/s3",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/go-ini/ini",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/jmespath/go-jmespath",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/bugsnag/bugsnag-go",
"Comment": "v1.0.2-5-gb1d1530",
"Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274"
},
{
"ImportPath": "github.com/bugsnag/bugsnag-go/errors",
"Comment": "v1.0.2-5-gb1d1530",
"Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274"
},
{
"ImportPath": "github.com/bugsnag/osext",
"Rev": "0dd3f918b21bec95ace9dc86c7e70266cfc5c702"
},
{
"ImportPath": "github.com/bugsnag/panicwrap",
"Comment": "1.0.0-2-ge2c2850",
"Rev": "e2c28503fcd0675329da73bf48b33404db873782"
},
{
"ImportPath": "github.com/denverdino/aliyungo/common",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/denverdino/aliyungo/oss",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/denverdino/aliyungo/util",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/docker/goamz/aws",
"Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85"
},
{
"ImportPath": "github.com/docker/goamz/s3",
"Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85"
},
{
"ImportPath": "github.com/docker/libtrust",
"Rev": "fa567046d9b14f6aa788882a950d69651d230b21"
},
{
"ImportPath": "github.com/garyburd/redigo/internal",
"Rev": "535138d7bcd717d6531c701ef5933d98b1866257"
},
{
"ImportPath": "github.com/garyburd/redigo/redis",
"Rev": "535138d7bcd717d6531c701ef5933d98b1866257"
},
{
"ImportPath": "github.com/golang/protobuf/proto",
"Rev": "8d92cf5fc15a4382f8964b08e1f42a75c0591aa3"
},
{
"ImportPath": "github.com/gorilla/context",
"Rev": "14f550f51af52180c2eefed15e5fd18d63c0a64a"
},
{
"ImportPath": "github.com/gorilla/handlers",
"Rev": "60c7bfde3e33c201519a200a4507a158cc03a17b"
},
{
"ImportPath": "github.com/gorilla/mux",
"Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf"
},
{
"ImportPath": "github.com/inconshreveable/mousetrap",
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
},
{
"ImportPath": "github.com/mitchellh/mapstructure",
"Rev": "482a9fd5fa83e8c4e7817413b80f3eb8feec03ef"
},
{
"ImportPath": "github.com/ncw/swift",
"Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86"
},
{
"ImportPath": "github.com/ncw/swift/swifttest",
"Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86"
},
{
"ImportPath": "github.com/spf13/cobra",
"Rev": "312092086bed4968099259622145a0c9ae280064"
},
{
"ImportPath": "github.com/spf13/pflag",
"Rev": "5644820622454e71517561946e3d94b9f9db6842"
},
{
"ImportPath": "github.com/stevvooe/resumable",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/stevvooe/resumable/sha256",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/stevvooe/resumable/sha512",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/yvasiyarov/go-metrics",
"Rev": "57bccd1ccd43f94bb17fdd8bf3007059b802f85e"
},
{
"ImportPath": "github.com/yvasiyarov/gorelic",
"Comment": "v0.0.6-8-ga9bba5b",
"Rev": "a9bba5b9ab508a086f9a12b8c51fab68478e2128"
},
{
"ImportPath": "github.com/yvasiyarov/newrelic_platform_go",
"Rev": "b21fdbd4370f3717f3bbd2bf41c223bc273068e6"
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/crypto/ocsp",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/context/ctxhttp",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/http2",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/http2/hpack",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/internal/timeseries",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/trace",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/oauth2",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/google",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/internal",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/jws",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/jwt",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/time/rate",
"Rev": "a4bde12657593d5e90d0533a3e4fd95e635124cb"
},
{
"ImportPath": "google.golang.org/api/gensupport",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/googleapi",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/googleapi/internal/uritemplates",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/storage/v1",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/appengine",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/app_identity",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/base",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/datastore",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/log",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/modules",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/remote_api",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/cloud",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/compute/metadata",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/internal",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/internal/opts",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/storage",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/grpc",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/codes",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/credentials",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/grpclog",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/internal",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/metadata",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/naming",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/peer",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/transport",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "gopkg.in/check.v1",
"Rev": "64131543e7896d5bcc6bd5a76287eb75ea96c673"
},
{
"ImportPath": "gopkg.in/yaml.v2",
"Rev": "bef53efd0c76e49e6de55ead051f886bea7e9420"
},
{
"ImportPath": "rsc.io/letsencrypt",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
}
]
}

View File

@@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

View File

@@ -35,7 +35,7 @@ PKGS=$(shell go list -tags "${DOCKER_BUILDTAGS}" ./... | grep -v ^github.com/doc
# Resolving binary dependencies for specific targets # Resolving binary dependencies for specific targets
GOLINT=$(shell which golint || echo '') GOLINT=$(shell which golint || echo '')
GODEP=$(shell which godep || echo '') VNDR=$(shell which vndr || echo '')
${PREFIX}/bin/registry: $(GOFILES) ${PREFIX}/bin/registry: $(GOFILES)
@echo "+ $@" @echo "+ $@"
@@ -86,24 +86,14 @@ clean:
@echo "+ $@" @echo "+ $@"
@rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/digest" "${PREFIX}/bin/registry-api-descriptor-template" @rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/digest" "${PREFIX}/bin/registry-api-descriptor-template"
dep-save: dep-validate:
@echo "+ $@"
$(if $(GODEP), , \
$(error Please install godep: go get github.com/tools/godep))
@$(GODEP) save $(PKGS)
dep-restore:
@echo "+ $@"
$(if $(GODEP), , \
$(error Please install godep: go get github.com/tools/godep))
@$(GODEP) restore -v
dep-validate: dep-restore
@echo "+ $@" @echo "+ $@"
$(if $(VNDR), , \
$(error Please install vndr: go get github.com/lk4d4/vndr))
@rm -Rf .vendor.bak @rm -Rf .vendor.bak
@mv vendor .vendor.bak @mv vendor .vendor.bak
@rm -Rf Godeps @$(VNDR)
@$(GODEP) save ./...
@test -z "$$(diff -r vendor .vendor.bak 2>&1 | tee /dev/stderr)" || \ @test -z "$$(diff -r vendor .vendor.bak 2>&1 | tee /dev/stderr)" || \
(echo >&2 "+ borked dependencies! what you have in Godeps/Godeps.json does not match with what you have in vendor" && false) (echo >&2 "+ inconsistent dependencies! what you have in vendor.conf does not match with what you have in vendor" && false)
@rm -Rf .vendor.bak @rm -Rf vendor
@mv .vendor.bak vendor

View File

@@ -76,8 +76,7 @@ may be the better choice.
For those who have previously deployed their own registry based on the Registry 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, 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 data migration is required. A tool to assist with migration efforts has been
created. For more information see [docker/migrator] created. For more information see [docker/migrator](https://github.com/docker/migrator).
(https://github.com/docker/migrator).
## Contribute ## Contribute

View File

@@ -1,6 +1,10 @@
## Registry Release Checklist ## Registry Release Checklist
10. Compile release notes detailing features and since the last release. Update the `CHANGELOG.md` file. 10. Compile release notes detailing features and since the last release.
Update the `CHANGELOG.md` file and create a PR to master with the updates.
Once that PR has been approved by maintainers the change may be cherry-picked
to the release branch (new release branches may be forked from this commit).
20. Update the version file: `https://github.com/docker/distribution/blob/master/version/version.go` 20. Update the version file: `https://github.com/docker/distribution/blob/master/version/version.go`
@@ -12,8 +16,12 @@ make AUTHORS
40. Create a signed tag. 40. Create a signed tag.
Distribution uses semantic versioning. Tags are of the format `vx.y.z[-rcn]` Distribution uses semantic versioning. Tags are of the format
You will need PGP installed and a PGP key which has been added to your Github account. The comment for the tag should include the release notes. `vx.y.z[-rcn]`. You will need PGP installed and a PGP key which has been added
to your Github account. The comment for the tag should include the release
notes, use previous tags as a guide for formatting consistently. Run
`git tag -s vx.y.z[-rcn]` to create tag and `git -v vx.y.z[-rcn]` to verify tag,
check comment and correct commit hash.
50. Push the signed tag 50. Push the signed tag

View File

@@ -8,8 +8,8 @@ import (
"time" "time"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
) )
var ( var (
@@ -152,7 +152,7 @@ type BlobProvider interface {
// BlobServer can serve blobs via http. // BlobServer can serve blobs via http.
type BlobServer interface { type BlobServer interface {
// ServeBlob attempts to serve the blob, identifed by dgst, via http. The // ServeBlob attempts to serve the blob, identified by dgst, via http. The
// service may decide to redirect the client elsewhere or serve the data // service may decide to redirect the client elsewhere or serve the data
// directly. // directly.
// //

View File

@@ -8,7 +8,7 @@ machine:
post: post:
# go # go
- gvm install go1.7 --prefer-binary --name=stable - gvm install go1.8 --prefer-binary --name=stable
environment: environment:
# Convenient shortcuts to "common" locations # Convenient shortcuts to "common" locations
@@ -34,7 +34,7 @@ dependencies:
override: override:
# Install dependencies for every copied clone/go version # Install dependencies for every copied clone/go version
- gvm use stable && go get github.com/tools/godep: - gvm use stable && go get github.com/lk4d4/vndr:
pwd: $BASE_STABLE pwd: $BASE_STABLE
post: post:
@@ -49,14 +49,15 @@ test:
# - gvm use old && go version # - gvm use old && go version
- gvm use stable && go version - gvm use stable && go version
# todo(richard): replace with a more robust vendoring solution. Removed due to a fundamental disagreement in godep philosophies.
# Ensure validation of dependencies # Ensure validation of dependencies
# - gvm use stable && if test -n "`git diff --stat=1000 master | grep -Ei \"vendor|godeps\"`"; then make dep-validate; fi: - git fetch origin:
# pwd: $BASE_STABLE pwd: $BASE_STABLE
- gvm use stable && if test -n "`git diff --stat=1000 origin/master | grep -E \"^[[:space:]]*vendor\"`"; then make dep-validate; fi:
pwd: $BASE_STABLE
# First thing: build everything. This will catch compile errors, and it's # First thing: build everything. This will catch compile errors, and it's
# also necessary for go vet to work properly (see #807). # also necessary for go vet to work properly (see #807).
- gvm use stable && godep go install $(go list ./... | grep -v "/vendor/"): - gvm use stable && go install $(go list ./... | grep -v "/vendor/"):
pwd: $BASE_STABLE pwd: $BASE_STABLE
# FMT # FMT
@@ -73,12 +74,12 @@ test:
override: override:
# Test stable, and report # Test stable, and report
- gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE': - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE':
timeout: 1000 timeout: 1000
pwd: $BASE_STABLE pwd: $BASE_STABLE
# Test stable with race # Test stable with race
- gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | grep -v "registry/handlers" | grep -v "registry/storage/driver" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -race -tags "$DOCKER_BUILDTAGS" -test.short $PACKAGE': - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | grep -v "registry/handlers" | grep -v "registry/storage/driver" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; go test -race -tags "$DOCKER_BUILDTAGS" -test.short $PACKAGE':
timeout: 1000 timeout: 1000
pwd: $BASE_STABLE pwd: $BASE_STABLE
post: post:

View File

@@ -7,8 +7,8 @@ import (
"log" "log"
"os" "os"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/version" "github.com/docker/distribution/version"
"github.com/opencontainers/go-digest"
) )
var ( var (
@@ -32,7 +32,7 @@ func init() {
func usage() { func usage() {
fmt.Fprintf(os.Stderr, "usage: %s [files...]\n", os.Args[0]) fmt.Fprintf(os.Stderr, "usage: %s [files...]\n", os.Args[0])
fmt.Fprintf(os.Stderr, ` fmt.Fprint(os.Stderr, `
Calculate the digest of one or more input files, emitting the result Calculate the digest of one or more input files, emitting the result
to standard out. If no files are provided, the digest of stdin will to standard out. If no files are provided, the digest of stdin will
be calculated. be calculated.

View File

@@ -1,6 +1,7 @@
package configuration package configuration
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -132,7 +133,7 @@ type Configuration struct {
// HTTP2 configuration options // HTTP2 configuration options
HTTP2 struct { HTTP2 struct {
// Specifies wether the registry should disallow clients attempting // Specifies whether the registry should disallow clients attempting
// to connect via http2. If set to true, only http/1.1 is supported. // to connect via http2. If set to true, only http/1.1 is supported.
Disabled bool `yaml:"disabled,omitempty"` Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"http2,omitempty"` } `yaml:"http2,omitempty"`
@@ -188,8 +189,11 @@ type Configuration struct {
// Validation configures validation options for the registry. // Validation configures validation options for the registry.
Validation struct { Validation struct {
// Enabled enables the other options in this section. // Enabled enables the other options in this section. This field is
// deprecated in favor of Disabled.
Enabled bool `yaml:"enabled,omitempty"` Enabled bool `yaml:"enabled,omitempty"`
// Disabled disables the other options in this section.
Disabled bool `yaml:"disabled,omitempty"`
// Manifests configures manifest validation. // Manifests configures manifest validation.
Manifests struct { Manifests struct {
// URLs configures validation for URLs in pushed manifests. // URLs configures validation for URLs in pushed manifests.
@@ -232,7 +236,7 @@ type LogHook struct {
// Levels set which levels of log message will let hook executed. // Levels set which levels of log message will let hook executed.
Levels []string `yaml:"levels,omitempty"` Levels []string `yaml:"levels,omitempty"`
// MailOptions allows user to configurate email parameters. // MailOptions allows user to configure email parameters.
MailOptions MailOptions `yaml:"options,omitempty"` MailOptions MailOptions `yaml:"options,omitempty"`
} }
@@ -326,7 +330,7 @@ type Health struct {
type v0_1Configuration Configuration type v0_1Configuration Configuration
// UnmarshalYAML implements the yaml.Unmarshaler interface // UnmarshalYAML implements the yaml.Unmarshaler interface
// Unmarshals a string of the form X.Y into a Version, validating that X and Y can represent uints // Unmarshals a string of the form X.Y into a Version, validating that X and Y can represent unsigned integers
func (version *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { func (version *Version) UnmarshalYAML(unmarshal func(interface{}) error) error {
var versionString string var versionString string
err := unmarshal(&versionString) err := unmarshal(&versionString)
@@ -624,7 +628,7 @@ func Parse(rd io.Reader) (*Configuration, error) {
v0_1.Loglevel = Loglevel("info") v0_1.Loglevel = Loglevel("info")
} }
if v0_1.Storage.Type() == "" { if v0_1.Storage.Type() == "" {
return nil, fmt.Errorf("No storage configuration provided") return nil, errors.New("No storage configuration provided")
} }
return (*Configuration)(v0_1), nil return (*Configuration)(v0_1), nil
} }

View File

@@ -64,7 +64,7 @@
// Note that this only affects the new context, the previous context, with the // Note that this only affects the new context, the previous context, with the
// version field, can be used independently. Put another way, the new logger, // version field, can be used independently. Put another way, the new logger,
// added to the request context, is unique to that context and can have // added to the request context, is unique to that context and can have
// request scoped varaibles. // request scoped variables.
// //
// HTTP Requests // HTTP Requests
// //

View File

@@ -123,13 +123,13 @@ to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
4. Use `curl` to list the image in the registry. 4. Use `curl` to list the image in the registry.
$ curl -v -X GET http://localhost:32777/v2/registry1/tags/list $ curl -v -X GET http://localhost:5000/v2/registry_one/tags/list
* Hostname was NOT found in DNS cache * Hostname was NOT found in DNS cache
* Trying 127.0.0.1... * Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 32777 (#0) * Connected to localhost (127.0.0.1) port 32777 (#0)
> GET /v2/registry1/tags/list HTTP/1.1 > GET /v2/registry1/tags/list HTTP/1.1
> User-Agent: curl/7.36.0 > User-Agent: curl/7.36.0
> Host: localhost:32777 > Host: localhost:5000
> Accept: */* > Accept: */*
> >
< HTTP/1.1 200 OK < HTTP/1.1 200 OK
@@ -138,7 +138,7 @@ to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
< Date: Tue, 14 Apr 2015 22:34:13 GMT < Date: Tue, 14 Apr 2015 22:34:13 GMT
< Content-Length: 39 < Content-Length: 39
< <
{"name":"registry1","tags":["latest"]} {"name":"registry_one","tags":["latest"]}
* Connection #0 to host localhost left intact * Connection #0 to host localhost left intact
This example refers to the specific port assigned to the 2.0 registry. You saw This example refers to the specific port assigned to the 2.0 registry. You saw

View File

@@ -64,7 +64,7 @@ registryv2tokenoauthnotls:
- ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem - ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem
tokenserveroauth: tokenserveroauth:
build: "tokenserver-oauth" build: "tokenserver-oauth"
command: "--debug -addr 0.0.0.0:5559 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5559" command: "--debug -addr 0.0.0.0:5559 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5559 -enforce-class"
ports: ports:
- "5559" - "5559"
malevolent: malevolent:

View File

@@ -1,6 +1,6 @@
[[suite]] [[suite]]
dind=true dind=true
images=[ "nginx:1.9", "dmcgowan/token-server:simple", "dmcgowan/token-server:oauth", "dmcgowan/malevolent:0.1.0" ] images=[ "nginx:1.9", "dmcgowan/token-server:simple", "dmcgowan/token-server:oauth", "dmcgowan/malevolent:0.1.0", "dmcgowan/ncat:latest" ]
[[suite.pretest]] [[suite.pretest]]
command="sh ./install_certs.sh /etc/generated_certs.d" command="sh ./install_certs.sh /etc/generated_certs.d"

View File

@@ -32,18 +32,44 @@ function basic_auth_version_check() {
fi fi
} }
email="a@nowhere.com"
# docker_t_login calls login with email depending on version
function docker_t_login() {
# Only pass email field pre 1.11, no deprecation warning
parse_version "$GOLEM_DIND_VERSION"
v=$version
parse_version "1.11.0"
if [ "$v" -lt "$version" ]; then
run docker_t login -e $email $@
else
run docker_t login $@
fi
}
# login issues a login to docker to the provided server # login issues a login to docker to the provided server
# uses user, password, and email variables set outside of function # uses user, password, and email variables set outside of function
# requies bats # requies bats
function login() { function login() {
rm -f /root/.docker/config.json rm -f /root/.docker/config.json
run docker_t login -u $user -p $password -e $email $1
docker_t_login -u $user -p $password $1
if [ "$status" -ne 0 ]; then if [ "$status" -ne 0 ]; then
echo $output echo $output
fi fi
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
# Handle different deprecation warnings
parse_version "$GOLEM_DIND_VERSION"
v=$version
parse_version "1.11.0"
if [ "$v" -lt "$version" ]; then
# First line is WARNING about credential save or email deprecation (maybe both) # First line is WARNING about credential save or email deprecation (maybe both)
[ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ] [ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ]
else
[ "${lines[0]}" = "Login Succeeded" ]
fi
} }
function login_oauth() { function login_oauth() {
@@ -92,7 +118,7 @@ function docker_t() {
docker exec dockerdaemon docker $@ docker exec dockerdaemon docker $@
} }
# build reates a new docker image id from another image # build creates a new docker image id from another image
function build() { function build() {
docker exec -i dockerdaemon docker build --no-cache -t $1 - <<DOCKERFILE docker exec -i dockerdaemon docker build --no-cache -t $1 - <<DOCKERFILE
FROM $2 FROM $2

View File

@@ -12,7 +12,7 @@ function setup() {
} }
@test "Test malevolent proxy pass through" { @test "Test malevolent proxy pass through" {
docker_t tag -f $base:latest $host/$base/nochange:latest docker_t tag $base:latest $host/$base/nochange:latest
run docker_t push $host/$base/nochange:latest run docker_t push $host/$base/nochange:latest
echo $output echo $output
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
@@ -26,7 +26,7 @@ function setup() {
@test "Test malevolent image name change" { @test "Test malevolent image name change" {
imagename="$host/$base/rename" imagename="$host/$base/rename"
image="$imagename:lastest" image="$imagename:lastest"
docker_t tag -f $base:latest $image docker_t tag $base:latest $image
run docker_t push $image run docker_t push $image
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
has_digest "$output" has_digest "$output"
@@ -133,7 +133,7 @@ function setup() {
has_digest "$output" has_digest "$output"
image2="$host/$base/image2/alteredid:$poison2" image2="$host/$base/image2/alteredid:$poison2"
docker_t tag -f $image1 $image2 docker_t tag $image1 $image2
run docker_t push $image2 run docker_t push $image2
echo "$output" echo "$output"
[ "$status" -eq 0 ] [ "$status" -eq 0 ]

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bats
# This tests pushing and pulling plugins
load helpers
user="testuser"
password="testpassword"
base="hello-world"
#TODO: Create plugin image
function create_plugin() {
plugindir=$(mktemp -d)
cat - > $plugindir/config.json <<CONFIGJSON
{
"manifestVersion": "v0",
"description": "A test plugin for integration tests",
"entrypoint": ["/usr/bin/ncat", "-l", "-U", "//run/docker/plugins/plugin.sock"],
"interface" : {
"types": ["docker.volumedriver/1.0"],
"socket": "plugin.sock"
}
}
CONFIGJSON
cid=$(docker create dmcgowan/ncat:latest /bin/sh)
mkdir $plugindir/rootfs
docker export $cid | tar -x -C $plugindir/rootfs
docker rm $cid
daemontmp=$(docker exec dockerdaemon mktemp -d)
tar -c -C $plugindir . | docker exec -i dockerdaemon tar -x -C $daemontmp
docker exec dockerdaemon docker plugin create $1 $daemontmp
docker exec dockerdaemon rm -rf $daemontmp
rm -rf $plugindir
}
@test "Test plugin push and pull" {
version_check docker "$GOLEM_DIND_VERSION" "1.13.0-rc3"
version_check docker "$GOLEM_DISTRIBUTION_VERSION" "2.6.0"
login_oauth localregistry:5558
image="localregistry:5558/testuser/plugin1"
create_plugin $image
run docker_t plugin push $image
echo $output
[ "$status" -eq 0 ]
docker_t plugin rm $image
docker_t plugin install --grant-all-permissions $image
}
@test "Test plugin push and failed image pull" {
version_check docker "$GOLEM_DIND_VERSION" "1.13.0-rc3"
version_check docker "$GOLEM_DISTRIBUTION_VERSION" "2.6.0"
login_oauth localregistry:5558
image="localregistry:5558/testuser/plugin-not-image"
create_plugin $image
run docker_t plugin push $image
echo $output
[ "$status" -eq 0 ]
docker_t plugin rm $image
run docker_t pull $image
[ "$status" -ne 0 ]
}
@test "Test image push and failed plugin pull" {
version_check docker "$GOLEM_DIND_VERSION" "1.13.0-rc3"
version_check docker "$GOLEM_DISTRIBUTION_VERSION" "2.6.0"
login_oauth localregistry:5558
image="localregistry:5558/testuser/image-not-plugin"
build $image "$base:latest"
run docker_t push $image
echo $output
[ "$status" -eq 0 ]
docker_t rmi $image
run docker_t plugin install --grant-all-permissions $image
[ "$status" -ne 0 ]
}

View File

@@ -46,7 +46,6 @@ echo "Testing image $distimage with distribution version $distversion"
# These images are defined in golem.conf # These images are defined in golem.conf
time docker pull nginx:1.9 time docker pull nginx:1.9
time docker pull golang:1.6 time docker pull golang:1.6
time docker pull registry:0.9.1
time docker pull dmcgowan/token-server:simple time docker pull dmcgowan/token-server:simple
time docker pull dmcgowan/token-server:oauth time docker pull dmcgowan/token-server:oauth
time docker pull distribution/golem-runner:0.1-bats time docker pull distribution/golem-runner:0.1-bats
@@ -54,11 +53,15 @@ time docker pull distribution/golem-runner:0.1-bats
time docker pull docker:1.9.1-dind time docker pull docker:1.9.1-dind
time docker pull docker:1.10.3-dind time docker pull docker:1.10.3-dind
time docker pull docker:1.11.1-dind time docker pull docker:1.11.1-dind
time docker pull docker:1.12.3-dind
time docker pull docker:1.13.0-rc5-dind
golem -cache $cachedir \ golem -cache $cachedir \
-i "golem-distribution:latest,$distimage,$distversion" \ -i "golem-distribution:latest,$distimage,$distversion" \
-i "golem-dind:latest,docker:1.9.1-dind,1.9.1" \ -i "golem-dind:latest,docker:1.9.1-dind,1.9.1" \
-i "golem-dind:latest,docker:1.10.3-dind,1.10.3" \ -i "golem-dind:latest,docker:1.10.3-dind,1.10.3" \
-i "golem-dind:latest,docker:1.11.1-dind,1.11.1" \ -i "golem-dind:latest,docker:1.11.1-dind,1.11.1" \
-i "golem-dind:latest,docker:1.12.3-dind,1.12.3" \
-i "golem-dind:latest,docker:1.13.0-rc5-dind,1.13.0" \
$DIR $DIR

View File

@@ -12,14 +12,13 @@ image="${base}:latest"
# Login information, should match values in nginx/test.passwd # Login information, should match values in nginx/test.passwd
user=${TEST_USER:-"testuser"} user=${TEST_USER:-"testuser"}
password=${TEST_PASSWORD:-"passpassword"} password=${TEST_PASSWORD:-"passpassword"}
email="distribution@docker.com"
function setup() { function setup() {
tempImage $image tempImage $image
} }
@test "Test valid certificates" { @test "Test valid certificates" {
docker_t tag -f $image $hostname:5440/$image docker_t tag $image $hostname:5440/$image
run docker_t push $hostname:5440/$image run docker_t push $hostname:5440/$image
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
has_digest "$output" has_digest "$output"
@@ -28,7 +27,7 @@ function setup() {
@test "Test basic auth" { @test "Test basic auth" {
basic_auth_version_check basic_auth_version_check
login $hostname:5441 login $hostname:5441
docker_t tag -f $image $hostname:5441/$image docker_t tag $image $hostname:5441/$image
run docker_t push $hostname:5441/$image run docker_t push $hostname:5441/$image
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
has_digest "$output" has_digest "$output"
@@ -60,14 +59,14 @@ function setup() {
} }
@test "Test TLS client auth" { @test "Test TLS client auth" {
docker_t tag -f $image $hostname:5442/$image docker_t tag $image $hostname:5442/$image
run docker_t push $hostname:5442/$image run docker_t push $hostname:5442/$image
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
has_digest "$output" has_digest "$output"
} }
@test "Test TLS client with invalid certificate authority fails" { @test "Test TLS client with invalid certificate authority fails" {
docker_t tag -f $image $hostname:5443/$image docker_t tag $image $hostname:5443/$image
run docker_t push $hostname:5443/$image run docker_t push $hostname:5443/$image
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }
@@ -75,14 +74,14 @@ function setup() {
@test "Test basic auth with TLS client auth" { @test "Test basic auth with TLS client auth" {
basic_auth_version_check basic_auth_version_check
login $hostname:5444 login $hostname:5444
docker_t tag -f $image $hostname:5444/$image docker_t tag $image $hostname:5444/$image
run docker_t push $hostname:5444/$image run docker_t push $hostname:5444/$image
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
has_digest "$output" has_digest "$output"
} }
@test "Test unknown certificate authority fails" { @test "Test unknown certificate authority fails" {
docker_t tag -f $image $hostname:5445/$image docker_t tag $image $hostname:5445/$image
run docker_t push $hostname:5445/$image run docker_t push $hostname:5445/$image
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }
@@ -90,19 +89,19 @@ function setup() {
@test "Test basic auth with unknown certificate authority fails" { @test "Test basic auth with unknown certificate authority fails" {
run login $hostname:5446 run login $hostname:5446
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
docker_t tag -f $image $hostname:5446/$image docker_t tag $image $hostname:5446/$image
run docker_t push $hostname:5446/$image run docker_t push $hostname:5446/$image
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }
@test "Test TLS client auth to server with unknown certificate authority fails" { @test "Test TLS client auth to server with unknown certificate authority fails" {
docker_t tag -f $image $hostname:5447/$image docker_t tag $image $hostname:5447/$image
run docker_t push $hostname:5447/$image run docker_t push $hostname:5447/$image
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }
@test "Test failure to connect to server fails to fallback to SSLv3" { @test "Test failure to connect to server fails to fallback to SSLv3" {
docker_t tag -f $image $hostname:5448/$image docker_t tag $image $hostname:5448/$image
run docker_t push $hostname:5448/$image run docker_t push $hostname:5448/$image
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }

View File

@@ -6,23 +6,17 @@ load helpers
user="testuser" user="testuser"
password="testpassword" password="testpassword"
email="a@nowhere.com"
base="hello-world" base="hello-world"
@test "Test token server login" { @test "Test token server login" {
run docker_t login -u $user -p $password -e $email localregistry:5554 login localregistry:5554
echo $output
[ "$status" -eq 0 ]
# First line is WARNING about credential save or email deprecation
[ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ]
} }
@test "Test token server bad login" { @test "Test token server bad login" {
run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5554 docker_t_login -u "testuser" -p "badpassword" localregistry:5554
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5554 docker_t_login -u "baduser" -p "testpassword" localregistry:5554
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }
@@ -58,10 +52,10 @@ base="hello-world"
@test "Test oauth token server bad login" { @test "Test oauth token server bad login" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0" version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5557 docker_t_login -u "testuser" -p "badpassword" -e $email localregistry:5557
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5557 docker_t_login -u "baduser" -p "testpassword" -e $email localregistry:5557
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
} }

View File

@@ -1,4 +1,4 @@
FROM dmcgowan/token-server:oauth FROM dmcgowan/token-server@sha256:5a6f76d3086cdf63249c77b521108387b49d85a30c5e1c4fe82fdf5ae3b76ba7
WORKDIR / WORKDIR /

View File

@@ -1,4 +1,4 @@
FROM dmcgowan/token-server:simple FROM dmcgowan/token-server@sha256:0eab50ebdff5b6b95b3addf4edbd8bd2f5b940f27b41b43c94afdf05863a81af
WORKDIR / WORKDIR /

View File

@@ -1,139 +0,0 @@
package digest
import (
"fmt"
"hash"
"io"
"regexp"
"strings"
)
const (
// DigestSha256EmptyTar is the canonical sha256 digest of empty data
DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
// Digest allows simple protection of hex formatted digest strings, prefixed
// by their algorithm. Strings of type Digest have some guarantee of being in
// the correct format and it provides quick access to the components of a
// digest string.
//
// The following is an example of the contents of Digest types:
//
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
//
// This allows to abstract the digest behind this type and work only in those
// terms.
type Digest string
// NewDigest returns a Digest from alg and a hash.Hash object.
func NewDigest(alg Algorithm, h hash.Hash) Digest {
return NewDigestFromBytes(alg, h.Sum(nil))
}
// NewDigestFromBytes returns a new digest from the byte contents of p.
// Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...)
// functions. This is also useful for rebuilding digests from binary
// serializations.
func NewDigestFromBytes(alg Algorithm, p []byte) Digest {
return Digest(fmt.Sprintf("%s:%x", alg, p))
}
// NewDigestFromHex returns a Digest from alg and a the hex encoded digest.
func NewDigestFromHex(alg, hex string) Digest {
return Digest(fmt.Sprintf("%s:%s", alg, hex))
}
// DigestRegexp matches valid digest types.
var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+`)
// DigestRegexpAnchored matches valid digest types, anchored to the start and end of the match.
var DigestRegexpAnchored = regexp.MustCompile(`^` + DigestRegexp.String() + `$`)
var (
// ErrDigestInvalidFormat returned when digest format invalid.
ErrDigestInvalidFormat = fmt.Errorf("invalid checksum digest format")
// ErrDigestInvalidLength returned when digest has invalid length.
ErrDigestInvalidLength = fmt.Errorf("invalid checksum digest length")
// ErrDigestUnsupported returned when the digest algorithm is unsupported.
ErrDigestUnsupported = fmt.Errorf("unsupported digest algorithm")
)
// ParseDigest parses s and returns the validated digest object. An error will
// be returned if the format is invalid.
func ParseDigest(s string) (Digest, error) {
d := Digest(s)
return d, d.Validate()
}
// FromReader returns the most valid digest for the underlying content using
// the canonical digest algorithm.
func FromReader(rd io.Reader) (Digest, error) {
return Canonical.FromReader(rd)
}
// FromBytes digests the input and returns a Digest.
func FromBytes(p []byte) Digest {
return Canonical.FromBytes(p)
}
// Validate checks that the contents of d is a valid digest, returning an
// error if not.
func (d Digest) Validate() error {
s := string(d)
if !DigestRegexpAnchored.MatchString(s) {
return ErrDigestInvalidFormat
}
i := strings.Index(s, ":")
if i < 0 {
return ErrDigestInvalidFormat
}
// case: "sha256:" with no hex.
if i+1 == len(s) {
return ErrDigestInvalidFormat
}
switch algorithm := Algorithm(s[:i]); algorithm {
case SHA256, SHA384, SHA512:
if algorithm.Size()*2 != len(s[i+1:]) {
return ErrDigestInvalidLength
}
break
default:
return ErrDigestUnsupported
}
return nil
}
// Algorithm returns the algorithm portion of the digest. This will panic if
// the underlying digest is not in a valid format.
func (d Digest) Algorithm() Algorithm {
return Algorithm(d[:d.sepIndex()])
}
// Hex returns the hex digest portion of the digest. This will panic if the
// underlying digest is not in a valid format.
func (d Digest) Hex() string {
return string(d[d.sepIndex()+1:])
}
func (d Digest) String() string {
return string(d)
}
func (d Digest) sepIndex() int {
i := strings.Index(string(d), ":")
if i < 0 {
panic("could not find ':' in digest: " + d)
}
return i
}

View File

@@ -1,82 +0,0 @@
package digest
import (
"testing"
)
func TestParseDigest(t *testing.T) {
for _, testcase := range []struct {
input string
err error
algorithm Algorithm
hex string
}{
{
input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
algorithm: "sha256",
hex: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
},
{
input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
algorithm: "sha384",
hex: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
},
{
// empty hex
input: "sha256:",
err: ErrDigestInvalidFormat,
},
{
// just hex
input: "d41d8cd98f00b204e9800998ecf8427e",
err: ErrDigestInvalidFormat,
},
{
// not hex
input: "sha256:d41d8cd98f00b204e9800m98ecf8427e",
err: ErrDigestInvalidFormat,
},
{
// too short
input: "sha256:abcdef0123456789",
err: ErrDigestInvalidLength,
},
{
// too short (from different algorithm)
input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
err: ErrDigestInvalidLength,
},
{
input: "foo:d41d8cd98f00b204e9800998ecf8427e",
err: ErrDigestUnsupported,
},
} {
digest, err := ParseDigest(testcase.input)
if err != testcase.err {
t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.input, err, testcase.err)
}
if testcase.err != nil {
continue
}
if digest.Algorithm() != testcase.algorithm {
t.Fatalf("incorrect algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.algorithm)
}
if digest.Hex() != testcase.hex {
t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Hex(), testcase.hex)
}
// Parse string return value and check equality
newParsed, err := ParseDigest(digest.String())
if err != nil {
t.Fatalf("unexpected error parsing input %q: %v", testcase.input, err)
}
if newParsed != digest {
t.Fatalf("expected equal: %q != %q", newParsed, digest)
}
}
}

View File

@@ -1,155 +0,0 @@
package digest
import (
"crypto"
"fmt"
"hash"
"io"
)
// Algorithm identifies and implementation of a digester by an identifier.
// Note the that this defines both the hash algorithm used and the string
// encoding.
type Algorithm string
// supported digest types
const (
SHA256 Algorithm = "sha256" // sha256 with hex encoding
SHA384 Algorithm = "sha384" // sha384 with hex encoding
SHA512 Algorithm = "sha512" // sha512 with hex encoding
// Canonical is the primary digest algorithm used with the distribution
// project. Other digests may be used but this one is the primary storage
// digest.
Canonical = SHA256
)
var (
// TODO(stevvooe): Follow the pattern of the standard crypto package for
// registration of digests. Effectively, we are a registerable set and
// common symbol access.
// algorithms maps values to hash.Hash implementations. Other algorithms
// may be available but they cannot be calculated by the digest package.
algorithms = map[Algorithm]crypto.Hash{
SHA256: crypto.SHA256,
SHA384: crypto.SHA384,
SHA512: crypto.SHA512,
}
)
// Available returns true if the digest type is available for use. If this
// returns false, New and Hash will return nil.
func (a Algorithm) Available() bool {
h, ok := algorithms[a]
if !ok {
return false
}
// check availability of the hash, as well
return h.Available()
}
func (a Algorithm) String() string {
return string(a)
}
// Size returns number of bytes returned by the hash.
func (a Algorithm) Size() int {
h, ok := algorithms[a]
if !ok {
return 0
}
return h.Size()
}
// 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.
func (a Algorithm) New() Digester {
return &digester{
alg: a,
hash: a.Hash(),
}
}
// Hash returns a new hash as used by the algorithm. If not available, the
// method will panic. Check Algorithm.Available() before calling.
func (a Algorithm) Hash() hash.Hash {
if !a.Available() {
// NOTE(stevvooe): A missing hash is usually a programming error that
// must be resolved at compile time. We don't import in the digest
// package to allow users to choose their hash implementation (such as
// when using stevvooe/resumable or a hardware accelerated package).
//
// Applications that may want to resolve the hash at runtime should
// call Algorithm.Available before call Algorithm.Hash().
panic(fmt.Sprintf("%v not available (make sure it is imported)", a))
}
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
}
// FromBytes digests the input and returns a Digest.
func (a Algorithm) FromBytes(p []byte) Digest {
digester := a.New()
if _, err := digester.Hash().Write(p); err != nil {
// Writes to a Hash should never fail. None of the existing
// hash implementations in the stdlib or hashes vendored
// here can return errors from Write. Having a panic in this
// condition instead of having FromBytes return an error value
// avoids unnecessary error handling paths in all callers.
panic("write to hash function returned error: " + err.Error())
}
return digester.Digest()
}
// TODO(stevvooe): Allow resolution of verifiers using the digest type and
// this registration system.
// Digester calculates the digest of written data. Writes should go directly
// to the return value of Hash, while calling Digest will return the current
// value of the digest.
type Digester interface {
Hash() hash.Hash // provides direct access to underlying hash instance.
Digest() Digest
}
// digester provides a simple digester definition that embeds a hasher.
type digester struct {
alg Algorithm
hash hash.Hash
}
func (d *digester) Hash() hash.Hash {
return d.hash
}
func (d *digester) Digest() Digest {
return NewDigest(d.alg, d.hash)
}

View File

@@ -1,42 +0,0 @@
// Package digest provides a generalized type to opaquely represent message
// digests and their operations within the registry. The Digest type is
// designed to serve as a flexible identifier in a content-addressable system.
// More importantly, it provides tools and wrappers to work with
// hash.Hash-based digests with little effort.
//
// Basics
//
// The format of a digest is simply a string with two parts, dubbed the
// "algorithm" and the "digest", separated by a colon:
//
// <algorithm>:<digest>
//
// An example of a sha256 digest representation follows:
//
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
//
// In this case, the string "sha256" is the algorithm and the hex bytes are
// the "digest".
//
// Because the Digest type is simply a string, once a valid Digest is
// obtained, comparisons are cheap, quick and simple to express with the
// standard equality operator.
//
// Verification
//
// The main benefit of using the Digest type is simple verification against a
// given digest. The Verifier interface, modeled after the stdlib hash.Hash
// interface, provides a common write sink for digest verification. After
// writing is complete, calling the Verifier.Verified method will indicate
// whether or not the stream of bytes matches the target digest.
//
// Missing Features
//
// In addition to the above, we intend to add the following features to this
// package:
//
// 1. A Digester type that supports write sink digest calculation.
//
// 2. Suspend and resume of ongoing digest calculations to support efficient digest verification in the registry.
//
package digest

View File

@@ -1,44 +0,0 @@
package digest
import (
"hash"
"io"
)
// Verifier presents a general verification interface to be used with message
// digests and other byte stream verifications. Users instantiate a Verifier
// from one of the various methods, write the data under test to it then check
// the result with the Verified method.
type Verifier interface {
io.Writer
// Verified will return true if the content written to Verifier matches
// the digest.
Verified() bool
}
// NewDigestVerifier returns a verifier that compares the written bytes
// against a passed in digest.
func NewDigestVerifier(d Digest) (Verifier, error) {
if err := d.Validate(); err != nil {
return nil, err
}
return hashVerifier{
hash: d.Algorithm().Hash(),
digest: d,
}, nil
}
type hashVerifier struct {
digest Digest
hash hash.Hash
}
func (hv hashVerifier) Write(p []byte) (n int, err error) {
return hv.hash.Write(p)
}
func (hv hashVerifier) Verified() bool {
return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash)
}

View File

@@ -1,49 +0,0 @@
package digest
import (
"bytes"
"crypto/rand"
"io"
"testing"
)
func TestDigestVerifier(t *testing.T) {
p := make([]byte, 1<<20)
rand.Read(p)
digest := FromBytes(p)
verifier, err := NewDigestVerifier(digest)
if err != nil {
t.Fatalf("unexpected error getting digest verifier: %s", err)
}
io.Copy(verifier, bytes.NewReader(p))
if !verifier.Verified() {
t.Fatalf("bytes not verified")
}
}
// TestVerifierUnsupportedDigest ensures that unsupported digest validation is
// flowing through verifier creation.
func TestVerifierUnsupportedDigest(t *testing.T) {
unsupported := Digest("bean:0123456789abcdef")
_, err := NewDigestVerifier(unsupported)
if err == nil {
t.Fatalf("expected error when creating verifier")
}
if err != ErrDigestUnsupported {
t.Fatalf("incorrect error for unsupported digest: %v", err)
}
}
// TODO(stevvooe): Add benchmarks to measure bytes/second throughput for
// DigestVerifier.
//
// The relevant benchmark for comparison can be run with the following
// commands:
//
// go test -bench . crypto/sha1
//

View File

@@ -1,10 +1,12 @@
package digest package digestset
import ( import (
"errors" "errors"
"sort" "sort"
"strings" "strings"
"sync" "sync"
digest "github.com/opencontainers/go-digest"
) )
var ( var (
@@ -44,7 +46,7 @@ func NewSet() *Set {
// values or short values. This function does not test equality, // values or short values. This function does not test equality,
// rather whether the second value could match against the first // rather whether the second value could match against the first
// value. // value.
func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool { func checkShortMatch(alg digest.Algorithm, hex, shortAlg, shortHex string) bool {
if len(hex) == len(shortHex) { if len(hex) == len(shortHex) {
if hex != shortHex { if hex != shortHex {
return false return false
@@ -64,7 +66,7 @@ func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool {
// If no digests could be found ErrDigestNotFound will be returned // If no digests could be found ErrDigestNotFound will be returned
// with an empty digest value. If multiple matches are found // with an empty digest value. If multiple matches are found
// ErrDigestAmbiguous will be returned with an empty digest value. // ErrDigestAmbiguous will be returned with an empty digest value.
func (dst *Set) Lookup(d string) (Digest, error) { func (dst *Set) Lookup(d string) (digest.Digest, error) {
dst.mutex.RLock() dst.mutex.RLock()
defer dst.mutex.RUnlock() defer dst.mutex.RUnlock()
if len(dst.entries) == 0 { if len(dst.entries) == 0 {
@@ -72,11 +74,11 @@ func (dst *Set) Lookup(d string) (Digest, error) {
} }
var ( var (
searchFunc func(int) bool searchFunc func(int) bool
alg Algorithm alg digest.Algorithm
hex string hex string
) )
dgst, err := ParseDigest(d) dgst, err := digest.Parse(d)
if err == ErrDigestInvalidFormat { if err == digest.ErrDigestInvalidFormat {
hex = d hex = d
searchFunc = func(i int) bool { searchFunc = func(i int) bool {
return dst.entries[i].val >= d return dst.entries[i].val >= d
@@ -108,7 +110,7 @@ func (dst *Set) Lookup(d string) (Digest, error) {
// Add adds the given digest 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 // if the given digest is invalid. If the digest already exists in the
// set, this operation will be a no-op. // set, this operation will be a no-op.
func (dst *Set) Add(d Digest) error { func (dst *Set) Add(d digest.Digest) error {
if err := d.Validate(); err != nil { if err := d.Validate(); err != nil {
return err return err
} }
@@ -139,7 +141,7 @@ func (dst *Set) Add(d Digest) error {
// Remove removes the given digest from the set. An err will be // Remove removes the given digest from the set. An err will be
// returned if the given digest is invalid. If the digest does // returned if the given digest is invalid. If the digest does
// not exist in the set, this operation will be a no-op. // not exist in the set, this operation will be a no-op.
func (dst *Set) Remove(d Digest) error { func (dst *Set) Remove(d digest.Digest) error {
if err := d.Validate(); err != nil { if err := d.Validate(); err != nil {
return err return err
} }
@@ -167,10 +169,10 @@ func (dst *Set) Remove(d Digest) error {
} }
// All returns all the digests in the set // All returns all the digests in the set
func (dst *Set) All() []Digest { func (dst *Set) All() []digest.Digest {
dst.mutex.RLock() dst.mutex.RLock()
defer dst.mutex.RUnlock() defer dst.mutex.RUnlock()
retValues := make([]Digest, len(dst.entries)) retValues := make([]digest.Digest, len(dst.entries))
for i := range dst.entries { for i := range dst.entries {
retValues[i] = dst.entries[i].digest retValues[i] = dst.entries[i].digest
} }
@@ -183,10 +185,10 @@ func (dst *Set) All() []Digest {
// entire value of digest if uniqueness cannot be achieved without the // entire value of digest if uniqueness cannot be achieved without the
// full value. This function will attempt to make short codes as short // full value. This function will attempt to make short codes as short
// as possible to be unique. // as possible to be unique.
func ShortCodeTable(dst *Set, length int) map[Digest]string { func ShortCodeTable(dst *Set, length int) map[digest.Digest]string {
dst.mutex.RLock() dst.mutex.RLock()
defer dst.mutex.RUnlock() defer dst.mutex.RUnlock()
m := make(map[Digest]string, len(dst.entries)) m := make(map[digest.Digest]string, len(dst.entries))
l := length l := length
resetIdx := 0 resetIdx := 0
for i := 0; i < len(dst.entries); i++ { for i := 0; i < len(dst.entries); i++ {
@@ -222,9 +224,9 @@ func ShortCodeTable(dst *Set, length int) map[Digest]string {
} }
type digestEntry struct { type digestEntry struct {
alg Algorithm alg digest.Algorithm
val string val string
digest Digest digest digest.Digest
} }
type digestEntries []*digestEntry type digestEntries []*digestEntry

View File

@@ -1,20 +1,23 @@
package digest package digestset
import ( import (
"crypto/sha256" "crypto/sha256"
_ "crypto/sha512"
"encoding/binary" "encoding/binary"
"math/rand" "math/rand"
"testing" "testing"
digest "github.com/opencontainers/go-digest"
) )
func assertEqualDigests(t *testing.T, d1, d2 Digest) { func assertEqualDigests(t *testing.T, d1, d2 digest.Digest) {
if d1 != d2 { if d1 != d2 {
t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2) t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2)
} }
} }
func TestLookup(t *testing.T) { func TestLookup(t *testing.T) {
digests := []Digest{ digests := []digest.Digest{
"sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234511111111111111111111111111111111111111111111111111111111111",
"sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234111111111111111111111111111111111111111111111111111111111111",
"sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111",
@@ -88,7 +91,7 @@ func TestLookup(t *testing.T) {
} }
func TestAddDuplication(t *testing.T) { func TestAddDuplication(t *testing.T) {
digests := []Digest{ digests := []digest.Digest{
"sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234111111111111111111111111111111111111111111111111111111111111",
"sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234511111111111111111111111111111111111111111111111111111111111",
"sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111",
@@ -110,7 +113,7 @@ func TestAddDuplication(t *testing.T) {
t.Fatal("Invalid dset size") t.Fatal("Invalid dset size")
} }
if err := dset.Add(Digest("sha256:1234511111111111111111111111111111111111111111111111111111111111")); err != nil { if err := dset.Add(digest.Digest("sha256:1234511111111111111111111111111111111111111111111111111111111111")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -118,7 +121,7 @@ func TestAddDuplication(t *testing.T) {
t.Fatal("Duplicate digest insert allowed") t.Fatal("Duplicate digest insert allowed")
} }
if err := dset.Add(Digest("sha384:123451111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")); err != nil { if err := dset.Add(digest.Digest("sha384:123451111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -170,7 +173,7 @@ func TestAll(t *testing.T) {
} }
} }
all := map[Digest]struct{}{} all := map[digest.Digest]struct{}{}
for _, dgst := range dset.All() { for _, dgst := range dset.All() {
all[dgst] = struct{}{} all[dgst] = struct{}{}
} }
@@ -194,7 +197,7 @@ func assertEqualShort(t *testing.T, actual, expected string) {
} }
func TestShortCodeTable(t *testing.T) { func TestShortCodeTable(t *testing.T) {
digests := []Digest{ digests := []digest.Digest{
"sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234111111111111111111111111111111111111111111111111111111111111",
"sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234511111111111111111111111111111111111111111111111111111111111",
"sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111",
@@ -227,15 +230,15 @@ func TestShortCodeTable(t *testing.T) {
assertEqualShort(t, dump[digests[7]], "653") assertEqualShort(t, dump[digests[7]], "653")
} }
func createDigests(count int) ([]Digest, error) { func createDigests(count int) ([]digest.Digest, error) {
r := rand.New(rand.NewSource(25823)) r := rand.New(rand.NewSource(25823))
digests := make([]Digest, count) digests := make([]digest.Digest, count)
for i := range digests { for i := range digests {
h := sha256.New() h := sha256.New()
if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil { if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil {
return nil, err return nil, err
} }
digests[i] = NewDigest("sha256", h) digests[i] = digest.NewDigest("sha256", h)
} }
return digests, nil return digests, nil
} }

View File

@@ -0,0 +1,52 @@
---
published: false
---
# Architecture
## Design
**TODO(stevvooe):** Discuss the architecture of the registry, internally and externally, in a few different deployment scenarios.
### Eventual Consistency
> **NOTE:** This section belongs somewhere, perhaps in a design document. We
> are leaving this here so the information is not lost.
Running the registry on eventually consistent backends has been part of the
design from the beginning. This section covers some of the approaches to
dealing with this reality.
There are a few classes of issues that we need to worry about when
implementing something on top of the storage drivers:
1. Read-After-Write consistency (see this [article on
s3](http://shlomoswidler.com/2009/12/read-after-write-consistency-in-amazon.html)).
2. [Write-Write Conflicts](http://en.wikipedia.org/wiki/Write%E2%80%93write_conflict).
In reality, the registry must worry about these kinds of errors when doing the
following:
1. Accepting data into a temporary upload file may not have latest data block
yet (read-after-write).
2. Moving uploaded data into its blob location (write-write race).
3. Modifying the "current" manifest for given tag (write-write race).
4. A whole slew of operations around deletes (read-after-write, delete-write
races, garbage collection, etc.).
The backend path layout employs a few techniques to avoid these problems:
1. Large writes are done to private upload directories. This alleviates most
of the corruption potential under multiple writers by avoiding multiple
writers.
2. Constraints in storage driver implementations, such as support for writing
after the end of a file to extend it.
3. Digest verification to avoid data corruption.
4. Manifest files are stored by digest and cannot change.
5. All other non-content files (links, hashes, etc.) are written as an atomic
unit. Anything that requires additions and deletions is broken out into
separate "files". Last writer still wins.
Unfortunately, one must play this game when trying to build something like
this on top of eventually consistent storage systems. If we run into serious
problems, we can wrap the storagedrivers in a shared consistency layer but
that would increase complexity and hinder registry cluster performance.

View File

@@ -223,9 +223,9 @@ notifications:
disabled: false disabled: false
url: https://my.listener.com/event url: https://my.listener.com/event
headers: <http.Header> headers: <http.Header>
timeout: 500 timeout: 1s
threshold: 5 threshold: 10
backoff: 1000 backoff: 1s
ignoredmediatypes: ignoredmediatypes:
- application/octet-stream - application/octet-stream
redis: redis:
@@ -268,7 +268,6 @@ compatibility:
schema1: schema1:
signingkeyfile: /etc/registry/key.json signingkeyfile: /etc/registry/key.json
validation: validation:
enabled: true
manifests: manifests:
urls: urls:
allow: allow:
@@ -553,7 +552,7 @@ The `auth` option is **optional**. Possible auth providers include:
- [`silly`](#silly) - [`silly`](#silly)
- [`token`](#token) - [`token`](#token)
- [`htpasswd`](#token) - [`htpasswd`](#htpasswd)
You can configure only one authentication provider. You can configure only one authentication provider.
@@ -817,9 +816,9 @@ notifications:
disabled: false disabled: false
url: https://my.listener.com/event url: https://my.listener.com/event
headers: <http.Header> headers: <http.Header>
timeout: 500 timeout: 1s
threshold: 5 threshold: 10
backoff: 1000 backoff: 1s
ignoredmediatypes: ignoredmediatypes:
- application/octet-stream - application/octet-stream
``` ```
@@ -948,7 +947,7 @@ a file.
| Parameter | Required | Description | | Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------| |-----------|----------|-------------------------------------------------------|
| `file` | yes | The path to check for existence of a file. | | `file` | yes | The path to check for existence of a file. |
| `interval`| no | How long to wait before repeating the check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. | | `interval`| no | How long to wait before repeating the check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. |
### `http` ### `http`
@@ -1028,7 +1027,6 @@ features. Each subsection defines such a feature with configurable behavior.
```none ```none
validation: validation:
enabled: true
manifests: manifests:
urls: urls:
allow: allow:
@@ -1037,14 +1035,15 @@ validation:
- ^https?://www\.example\.com/ - ^https?://www\.example\.com/
``` ```
### `enabled` ### `disabled`
Use the `enabled` flag to enable the other options in the `validation` The `disabled` flag disables the other options in the `validation`
section. They are disabled by default. section. They are enabled by default. This option deprecates the `enabled` flag.
### `manifests` ### `manifests`
Use the `manifest` subsection to configure manifest validation. Use the `manifests` subsection to configure validation of manifests. If
`disabled` is `false`, the validation allows nothing.
#### `urls` #### `urls`
@@ -1110,7 +1109,7 @@ middleware:
baseurl: http://d111111abcdef8.cloudfront.net baseurl: http://d111111abcdef8.cloudfront.net
privatekey: /path/to/asecret.pem privatekey: /path/to/asecret.pem
keypairid: asecret keypairid: asecret
duration: 60 duration: 60s
``` ```
See the configuration reference for [Cloudfront](#cloudfront) for more See the configuration reference for [Cloudfront](#cloudfront) for more

View File

@@ -795,7 +795,7 @@ Note that the upload url will not be available forever. If the upload uuid is
unknown to the registry, a `404 Not Found` response will be returned and the unknown to the registry, a `404 Not Found` response will be returned and the
client must restart the upload process. client must restart the upload process.
### Deleting a Layer #### Deleting a Layer
A layer may be deleted from the registry via its `name` and `digest`. A A layer may be deleted from the registry via its `name` and `digest`. A
delete may be issued with the following request format: delete may be issued with the following request format:

View File

@@ -795,7 +795,7 @@ Note that the upload url will not be available forever. If the upload uuid is
unknown to the registry, a `404 Not Found` response will be returned and the unknown to the registry, a `404 Not Found` response will be returned and the
client must restart the upload process. client must restart the upload process.
### Deleting a Layer #### Deleting a Layer
A layer may be deleted from the registry via its `name` and `digest`. A A layer may be deleted from the registry via its `name` and `digest`. A
delete may be issued with the following request format: delete may be issued with the following request format:

View File

@@ -6,7 +6,7 @@ keywords: ["registry, on-prem, images, tags, repository, distribution, api, adva
# Image Manifest Version 2, Schema 1 # Image Manifest Version 2, Schema 1
This document outlines the format of of the V2 image manifest. The image This document outlines the format of the V2 image manifest. The image
manifest described herein was introduced in the Docker daemon in the [v1.3.0 manifest described herein was introduced in the Docker daemon in the [v1.3.0
release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453). release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453).
It is a provisional manifest to provide a compatibility with the [V1 Image It is a provisional manifest to provide a compatibility with the [V1 Image

View File

@@ -6,7 +6,7 @@ keywords: ["registry, on-prem, images, tags, repository, distribution, api, adva
# Image Manifest Version 2, Schema 2 # Image Manifest Version 2, Schema 2
This document outlines the format of of the V2 image manifest, schema version 2. This document outlines the format of the V2 image manifest, schema version 2.
The original (and provisional) image manifest for V2 (schema 1), was introduced The original (and provisional) image manifest for V2 (schema 1), was introduced
in the Docker daemon in the [v1.3.0 in the Docker daemon in the [v1.3.0
release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453) release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453)
@@ -60,8 +60,8 @@ image manifest based on the Content-Type returned in the HTTP response.
- **`mediaType`** *string* - **`mediaType`** *string*
The MIME type of the referenced object. This will generally be The MIME type of the referenced object. This will generally be
`application/vnd.docker.image.manifest.v2+json`, but it could also `application/vnd.docker.distribution.manifest.v2+json`, but it could also
be `application/vnd.docker.image.manifest.v1+json` if the manifest be `application/vnd.docker.distribution.manifest.v1+json` if the manifest
list references a legacy schema-1 manifest. list references a legacy schema-1 manifest.
- **`size`** *int* - **`size`** *int*
@@ -123,7 +123,7 @@ image manifest based on the Content-Type returned in the HTTP response.
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [ "manifests": [
{ {
"mediaType": "application/vnd.docker.image.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7143, "size": 7143,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
"platform": { "platform": {
@@ -132,7 +132,7 @@ image manifest based on the Content-Type returned in the HTTP response.
} }
}, },
{ {
"mediaType": "application/vnd.docker.image.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7682, "size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
"platform": { "platform": {

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
// ErrAccessDenied is returned when an access to a requested resource is // ErrAccessDenied is returned when an access to a requested resource is
@@ -77,7 +77,7 @@ func (err ErrManifestUnknownRevision) Error() string {
type ErrManifestUnverified struct{} type ErrManifestUnverified struct{}
func (ErrManifestUnverified) Error() string { func (ErrManifestUnverified) Error() string {
return fmt.Sprintf("unverified manifest") return "unverified manifest"
} }
// ErrManifestVerification provides a type to collect errors encountered // ErrManifestVerification provides a type to collect errors encountered

View File

@@ -2,9 +2,11 @@ package checks
import ( import (
"errors" "errors"
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"time" "time"
@@ -15,10 +17,19 @@ import (
// if the file exists. // if the file exists.
func FileChecker(f string) health.Checker { func FileChecker(f string) health.Checker {
return health.CheckFunc(func() error { return health.CheckFunc(func() error {
if _, err := os.Stat(f); err == nil { absoluteFilePath, err := filepath.Abs(f)
return errors.New("file exists") if err != nil {
return fmt.Errorf("failed to get absolute path for %q: %v", f, err)
} }
_, err = os.Stat(absoluteFilePath)
if err == nil {
return errors.New("file exists")
} else if os.IsNotExist(err) {
return nil return nil
}
return err
}) })
} }

View File

@@ -24,7 +24,7 @@
// "manual" checks that allow the service to quickly be brought in/out of // "manual" checks that allow the service to quickly be brought in/out of
// rotation. // rotation.
// //
// import _ "github.com/docker/distribution/registry/health/api" // import _ "github.com/docker/distribution/health/api"
// //
// # curl localhost:5001/debug/health // # curl localhost:5001/debug/health
// {} // {}
@@ -122,6 +122,12 @@
// # curl localhost:5001/debug/health // # curl localhost:5001/debug/health
// {"fileChecker":"file exists"} // {"fileChecker":"file exists"}
// //
// FileChecker only accepts absolute or relative file path. It does not work
// properly with tilde(~). You should make sure that the application has
// proper permission(read and execute permission for directory along with
// the specified file path). Otherwise, the FileChecker will report error
// and file health check is not ok.
//
// You could also test the connectivity to a downstream service by using a // You could also test the connectivity to a downstream service by using a
// "HTTPChecker", but ensure that you only mark the test unhealthy if there // "HTTPChecker", but ensure that you only mark the test unhealthy if there
// are a minimum of two failures in a row: // are a minimum of two failures in a row:

View File

@@ -25,8 +25,8 @@ func TestReturns200IfThereAreNoChecks(t *testing.T) {
} }
} }
// TestReturns500IfThereAreErrorChecks ensures that the result code of the // TestReturns503IfThereAreErrorChecks ensures that the result code of the
// health endpoint is 500 if there are health checks with errors // health endpoint is 503 if there are health checks with errors.
func TestReturns503IfThereAreErrorChecks(t *testing.T) { func TestReturns503IfThereAreErrorChecks(t *testing.T) {
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()

View File

@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
) )
// MediaTypeManifestList specifies the mediaType for manifest lists. // MediaTypeManifestList specifies the mediaType for manifest lists.
@@ -81,7 +81,7 @@ type ManifestList struct {
Manifests []ManifestDescriptor `json:"manifests"` Manifests []ManifestDescriptor `json:"manifests"`
} }
// References returnes the distribution descriptors for the referenced image // References returns the distribution descriptors for the referenced image
// manifests. // manifests.
func (m ManifestList) References() []distribution.Descriptor { func (m ManifestList) References() []distribution.Descriptor {
dependencies := make([]distribution.Descriptor, len(m.Manifests)) dependencies := make([]distribution.Descriptor, len(m.Manifests))

View File

@@ -9,10 +9,10 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
type diffID digest.Digest type diffID digest.Digest
@@ -240,8 +240,13 @@ func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, e
// AppendReference adds a reference to the current ManifestBuilder // AppendReference adds a reference to the current ManifestBuilder
func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error { func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
// todo: verification here? descriptor := d.Descriptor()
mb.descriptors = append(mb.descriptors, d.Descriptor())
if err := descriptor.Digest.Validate(); err != nil {
return err
}
mb.descriptors = append(mb.descriptors, descriptor)
return nil return nil
} }

View File

@@ -9,9 +9,9 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
type mockBlobService struct { type mockBlobService struct {
@@ -197,10 +197,14 @@ func TestConfigBuilder(t *testing.T) {
bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)}
ref, err := reference.ParseNamed("testrepo:testtag") ref, err := reference.WithName("testrepo")
if err != nil { if err != nil {
t.Fatalf("could not parse reference: %v", err) t.Fatalf("could not parse reference: %v", err)
} }
ref, err = reference.WithTag(ref, "testtag")
if err != nil {
t.Fatalf("could not add tag: %v", err)
}
builder := NewConfigManifestBuilder(bs, pk, ref, []byte(imgJSON)) builder := NewConfigManifestBuilder(bs, pk, ref, []byte(imgJSON))

View File

@@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
const ( const (

View File

@@ -6,10 +6,10 @@ import (
"errors" "errors"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
// referenceManifestBuilder is a type for constructing manifests from schema1 // referenceManifestBuilder is a type for constructing manifests from schema1

View File

@@ -4,10 +4,10 @@ import (
"testing" "testing"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest { func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest {
@@ -55,7 +55,7 @@ func TestReferenceBuilder(t *testing.T) {
handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2}) handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2})
ref, err := reference.ParseNamed(handCrafted.Manifest.Name) ref, err := reference.WithName(handCrafted.Manifest.Name)
if err != nil { if err != nil {
t.Fatalf("could not parse reference: %v", err) t.Fatalf("could not parse reference: %v", err)
} }

View File

@@ -3,7 +3,7 @@ package schema2
import ( import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
// builder is a type for constructing manifests. // builder is a type for constructing manifests.
@@ -11,20 +11,24 @@ type builder struct {
// bs is a BlobService used to publish the configuration blob. // bs is a BlobService used to publish the configuration blob.
bs distribution.BlobService bs distribution.BlobService
// configMediaType is media type used to describe configuration
configMediaType string
// configJSON references // configJSON references
configJSON []byte configJSON []byte
// layers is a list of layer descriptors that gets built by successive // dependencies is a list of descriptors that gets built by successive
// calls to AppendReference. // calls to AppendReference. In case of image configuration these are layers.
layers []distribution.Descriptor dependencies []distribution.Descriptor
} }
// NewManifestBuilder is used to build new manifests for the current schema // NewManifestBuilder is used to build new manifests for the current schema
// version. It takes a BlobService so it can publish the configuration blob // version. It takes a BlobService so it can publish the configuration blob
// as part of the Build process. // as part of the Build process.
func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder { func NewManifestBuilder(bs distribution.BlobService, configMediaType string, configJSON []byte) distribution.ManifestBuilder {
mb := &builder{ mb := &builder{
bs: bs, bs: bs,
configMediaType: configMediaType,
configJSON: make([]byte, len(configJSON)), configJSON: make([]byte, len(configJSON)),
} }
copy(mb.configJSON, configJSON) copy(mb.configJSON, configJSON)
@@ -36,9 +40,9 @@ func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribu
func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) { func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
m := Manifest{ m := Manifest{
Versioned: SchemaVersion, Versioned: SchemaVersion,
Layers: make([]distribution.Descriptor, len(mb.layers)), Layers: make([]distribution.Descriptor, len(mb.dependencies)),
} }
copy(m.Layers, mb.layers) copy(m.Layers, mb.dependencies)
configDigest := digest.FromBytes(mb.configJSON) configDigest := digest.FromBytes(mb.configJSON)
@@ -48,7 +52,7 @@ func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
case nil: case nil:
// Override MediaType, since Put always replaces the specified media // Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns. // type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = MediaTypeConfig m.Config.MediaType = mb.configMediaType
return FromStruct(m) return FromStruct(m)
case distribution.ErrBlobUnknown: case distribution.ErrBlobUnknown:
// nop // nop
@@ -57,10 +61,10 @@ func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
} }
// Add config to the blob store // Add config to the blob store
m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) m.Config, err = mb.bs.Put(ctx, mb.configMediaType, mb.configJSON)
// Override MediaType, since Put always replaces the specified media // Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns. // type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = MediaTypeConfig m.Config.MediaType = mb.configMediaType
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -70,11 +74,11 @@ func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
// AppendReference adds a reference to the current ManifestBuilder. // AppendReference adds a reference to the current ManifestBuilder.
func (mb *builder) AppendReference(d distribution.Describable) error { func (mb *builder) AppendReference(d distribution.Describable) error {
mb.layers = append(mb.layers, d.Descriptor()) mb.dependencies = append(mb.dependencies, d.Descriptor())
return nil return nil
} }
// References returns the current references added to this builder. // References returns the current references added to this builder.
func (mb *builder) References() []distribution.Descriptor { func (mb *builder) References() []distribution.Descriptor {
return mb.layers return mb.dependencies
} }

View File

@@ -6,7 +6,7 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
type mockBlobService struct { type mockBlobService struct {
@@ -166,7 +166,7 @@ func TestBuilder(t *testing.T) {
} }
bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)}
builder := NewManifestBuilder(bs, imgJSON) builder := NewManifestBuilder(bs, MediaTypeImageConfig, imgJSON)
for _, d := range descriptors { for _, d := range descriptors {
if err := builder.AppendReference(d); err != nil { if err := builder.AppendReference(d); err != nil {
@@ -195,7 +195,7 @@ func TestBuilder(t *testing.T) {
if target.Digest != configDigest { if target.Digest != configDigest {
t.Fatalf("unexpected digest in target: %s", target.Digest.String()) t.Fatalf("unexpected digest in target: %s", target.Digest.String())
} }
if target.MediaType != MediaTypeConfig { if target.MediaType != MediaTypeImageConfig {
t.Fatalf("unexpected media type in target: %s", target.MediaType) t.Fatalf("unexpected media type in target: %s", target.MediaType)
} }
if target.Size != 3153 { if target.Size != 3153 {

View File

@@ -6,16 +6,16 @@ import (
"fmt" "fmt"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
) )
const ( const (
// MediaTypeManifest specifies the mediaType for the current version. // MediaTypeManifest specifies the mediaType for the current version.
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
// MediaTypeConfig specifies the mediaType for the image configuration. // MediaTypeImageConfig specifies the mediaType for the image configuration.
MediaTypeConfig = "application/vnd.docker.container.image.v1+json" MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json"
// MediaTypePluginConfig specifies the mediaType for plugin configuration. // MediaTypePluginConfig specifies the mediaType for plugin configuration.
MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json" MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"
@@ -27,6 +27,10 @@ const (
// MediaTypeForeignLayer is the mediaType used for layers that must be // MediaTypeForeignLayer is the mediaType used for layers that must be
// downloaded from foreign URLs. // downloaded from foreign URLs.
MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
// MediaTypeUncompressedLayer is the mediaType used for layers which
// are not compressed.
MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
) )
var ( var (

View File

@@ -32,7 +32,7 @@ func TestManifest(t *testing.T) {
Config: distribution.Descriptor{ Config: distribution.Descriptor{
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985, Size: 985,
MediaType: MediaTypeConfig, MediaType: MediaTypeImageConfig,
}, },
Layers: []distribution.Descriptor{ Layers: []distribution.Descriptor{
{ {
@@ -82,7 +82,7 @@ func TestManifest(t *testing.T) {
if target.Digest != "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" { if target.Digest != "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" {
t.Fatalf("unexpected digest in target: %s", target.Digest.String()) t.Fatalf("unexpected digest in target: %s", target.Digest.String())
} }
if target.MediaType != MediaTypeConfig { if target.MediaType != MediaTypeImageConfig {
t.Fatalf("unexpected media type in target: %s", target.MediaType) t.Fatalf("unexpected media type in target: %s", target.MediaType)
} }
if target.Size != 985 { if target.Size != 985 {

View File

@@ -5,7 +5,7 @@ import (
"mime" "mime"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
// Manifest represents a registry object specifying a set of // Manifest represents a registry object specifying a set of
@@ -23,7 +23,7 @@ type Manifest interface {
// Payload provides the serialized format of the manifest, in addition to // Payload provides the serialized format of the manifest, in addition to
// the media type. // the media type.
Payload() (mediatype string, payload []byte, err error) Payload() (mediaType string, payload []byte, err error)
} }
// ManifestBuilder creates a manifest allowing one to include dependencies. // ManifestBuilder creates a manifest allowing one to include dependencies.
@@ -94,20 +94,20 @@ var mappings = make(map[string]UnmarshalFunc, 0)
func UnmarshalManifest(ctHeader string, p []byte) (Manifest, Descriptor, error) { func UnmarshalManifest(ctHeader string, p []byte) (Manifest, Descriptor, error) {
// Need to look up by the actual media type, not the raw contents of // Need to look up by the actual media type, not the raw contents of
// the header. Strip semicolons and anything following them. // the header. Strip semicolons and anything following them.
var mediatype string var mediaType string
if ctHeader != "" { if ctHeader != "" {
var err error var err error
mediatype, _, err = mime.ParseMediaType(ctHeader) mediaType, _, err = mime.ParseMediaType(ctHeader)
if err != nil { if err != nil {
return nil, Descriptor{}, err return nil, Descriptor{}, err
} }
} }
unmarshalFunc, ok := mappings[mediatype] unmarshalFunc, ok := mappings[mediaType]
if !ok { if !ok {
unmarshalFunc, ok = mappings[""] unmarshalFunc, ok = mappings[""]
if !ok { if !ok {
return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype and no default available: %s", mediatype) return nil, Descriptor{}, fmt.Errorf("unsupported manifest media type and no default available: %s", mediaType)
} }
} }
@@ -116,10 +116,10 @@ func UnmarshalManifest(ctHeader string, p []byte) (Manifest, Descriptor, error)
// RegisterManifestSchema registers an UnmarshalFunc for a given schema type. This // RegisterManifestSchema registers an UnmarshalFunc for a given schema type. This
// should be called from specific // should be called from specific
func RegisterManifestSchema(mediatype string, u UnmarshalFunc) error { func RegisterManifestSchema(mediaType string, u UnmarshalFunc) error {
if _, ok := mappings[mediatype]; ok { if _, ok := mappings[mediaType]; ok {
return fmt.Errorf("manifest mediatype registration would overwrite existing: %s", mediatype) return fmt.Errorf("manifest media type registration would overwrite existing: %s", mediaType)
} }
mappings[mediatype] = u mappings[mediaType] = u
return nil return nil
} }

View File

@@ -6,9 +6,9 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/uuid" "github.com/docker/distribution/uuid"
"github.com/opencontainers/go-digest"
) )
type bridge struct { type bridge struct {

View File

@@ -4,12 +4,12 @@ import (
"testing" "testing"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/uuid" "github.com/docker/distribution/uuid"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
var ( var (
@@ -43,7 +43,7 @@ func TestEventBridgeManifestPulled(t *testing.T) {
return nil return nil
})) }))
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
if err := l.ManifestPulled(repoRef, sm); err != nil { if err := l.ManifestPulled(repoRef, sm); err != nil {
t.Fatalf("unexpected error notifying manifest pull: %v", err) t.Fatalf("unexpected error notifying manifest pull: %v", err)
} }
@@ -56,7 +56,7 @@ func TestEventBridgeManifestPushed(t *testing.T) {
return nil return nil
})) }))
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
if err := l.ManifestPushed(repoRef, sm); err != nil { if err := l.ManifestPushed(repoRef, sm); err != nil {
t.Fatalf("unexpected error notifying manifest pull: %v", err) t.Fatalf("unexpected error notifying manifest pull: %v", err)
} }
@@ -72,7 +72,7 @@ func TestEventBridgeManifestPushedWithTag(t *testing.T) {
return nil return nil
})) }))
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
if err := l.ManifestPushed(repoRef, sm, distribution.WithTag(m.Tag)); err != nil { if err := l.ManifestPushed(repoRef, sm, distribution.WithTag(m.Tag)); err != nil {
t.Fatalf("unexpected error notifying manifest pull: %v", err) t.Fatalf("unexpected error notifying manifest pull: %v", err)
} }
@@ -88,7 +88,7 @@ func TestEventBridgeManifestPulledWithTag(t *testing.T) {
return nil return nil
})) }))
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
if err := l.ManifestPulled(repoRef, sm, distribution.WithTag(m.Tag)); err != nil { if err := l.ManifestPulled(repoRef, sm, distribution.WithTag(m.Tag)); err != nil {
t.Fatalf("unexpected error notifying manifest pull: %v", err) t.Fatalf("unexpected error notifying manifest pull: %v", err)
} }
@@ -100,7 +100,7 @@ func TestEventBridgeManifestDeleted(t *testing.T) {
return nil return nil
})) }))
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
if err := l.ManifestDeleted(repoRef, dgst); err != nil { if err := l.ManifestDeleted(repoRef, dgst); err != nil {
t.Fatalf("unexpected error notifying manifest pull: %v", err) t.Fatalf("unexpected error notifying manifest pull: %v", err)
} }
@@ -160,7 +160,7 @@ func checkCommonManifest(t *testing.T, action string, events ...Event) {
t.Fatalf("unexpected event action: %q != %q", event.Action, action) t.Fatalf("unexpected event action: %q != %q", event.Action, action)
} }
repoRef, _ := reference.ParseNamed(repo) repoRef, _ := reference.WithName(repo)
ref, _ := reference.WithDigest(repoRef, dgst) ref, _ := reference.WithDigest(repoRef, dgst)
u, err := ub.BuildManifestURL(ref) u, err := ub.BuildManifestURL(ref)
if err != nil { if err != nil {

View File

@@ -13,7 +13,7 @@ type EndpointConfig struct {
Threshold int Threshold int
Backoff time.Duration Backoff time.Duration
IgnoredMediaTypes []string IgnoredMediaTypes []string
Transport *http.Transport Transport *http.Transport `json:"-"`
} }
// defaults set any zero-valued fields to a reasonable default. // defaults set any zero-valued fields to a reasonable default.

View File

@@ -77,7 +77,7 @@ type Event struct {
Request RequestRecord `json:"request,omitempty"` Request RequestRecord `json:"request,omitempty"`
// Actor specifies the agent that initiated the event. For most // Actor specifies the agent that initiated the event. For most
// situations, this could be from the authorizaton context of the request. // situations, this could be from the authorization context of the request.
Actor ActorRecord `json:"actor,omitempty"` Actor ActorRecord `json:"actor,omitempty"`
// Source identifies the registry node that generated the event. Put // Source identifies the registry node that generated the event. Put
@@ -87,7 +87,7 @@ type Event struct {
} }
// ActorRecord specifies the agent that initiated the event. For most // ActorRecord specifies the agent that initiated the event. For most
// situations, this could be from the authorizaton context of the request. // situations, this could be from the authorization context of the request.
// Data in this record can refer to both the initiating client and the // Data in this record can refer to both the initiating client and the
// generating request. // generating request.
type ActorRecord struct { type ActorRecord struct {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mime" "mime"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@@ -94,6 +95,21 @@ func TestHTTPSink(t *testing.T) {
var expectedMetrics EndpointMetrics var expectedMetrics EndpointMetrics
expectedMetrics.Statuses = make(map[string]int) expectedMetrics.Statuses = make(map[string]int)
closeL, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("unexpected error creating listener: %v", err)
}
defer closeL.Close()
go func() {
for {
c, err := closeL.Accept()
if err != nil {
return
}
c.Close()
}
}()
for _, tc := range []struct { for _, tc := range []struct {
events []Event // events to send events []Event // events to send
url string url string
@@ -121,8 +137,8 @@ func TestHTTPSink(t *testing.T) {
failure: true, failure: true,
}, },
{ {
// Case where connection never goes through. // Case where connection is immediately closed
url: "http://shoudlntresolve/", url: closeL.Addr().String(),
failure: true, failure: true,
}, },
} { } {

View File

@@ -5,8 +5,8 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
) )
// ManifestListener describes a set of methods for listening to events related to manifests. // ManifestListener describes a set of methods for listening to events related to manifests.

View File

@@ -7,7 +7,6 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@@ -16,6 +15,7 @@ import (
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
func TestListener(t *testing.T) { func TestListener(t *testing.T) {
@@ -33,7 +33,7 @@ func TestListener(t *testing.T) {
ops: make(map[string]int), ops: make(map[string]int),
} }
repoRef, _ := reference.ParseNamed("foo/bar") repoRef, _ := reference.WithName("foo/bar")
repository, err := registry.Repository(ctx, repoRef) repository, err := registry.Repository(ctx, repoRef)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting repo: %v", err) t.Fatalf("unexpected error getting repo: %v", err)

View File

@@ -0,0 +1,28 @@
package notifications
import (
"encoding/json"
"expvar"
"testing"
)
func TestMetricsExpvar(t *testing.T) {
endpointsVar := expvar.Get("registry").(*expvar.Map).Get("notifications").(*expvar.Map).Get("endpoints")
var v interface{}
if err := json.Unmarshal([]byte(endpointsVar.String()), &v); err != nil {
t.Fatalf("unexpected error unmarshaling endpoints: %v", err)
}
if v != nil {
t.Fatalf("expected nil, got %#v", v)
}
NewEndpoint("x", "y", EndpointConfig{})
if err := json.Unmarshal([]byte(endpointsVar.String()), &v); err != nil {
t.Fatalf("unexpected error unmarshaling endpoints: %v", err)
}
if slice, ok := v.([]interface{}); !ok || len(slice) != 1 {
t.Logf("expected one-element []interface{}, got %#v", v)
}
}

View File

@@ -0,0 +1,42 @@
package reference
import "path"
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
}
// FamiliarName returns the familiar name string
// for the given named, familiarizing if needed.
func FamiliarName(ref Named) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().Name()
}
return ref.Name()
}
// FamiliarString returns the familiar string representation
// for the given reference, familiarizing if needed.
func FamiliarString(ref Reference) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().String()
}
return ref.String()
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, FamiliarName(namedRef))
}
return matched, err
}

View File

@@ -0,0 +1,170 @@
package reference
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/digestset"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "library"
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, errors.New("invalid reference format: repository name must be lowercase")
}
ref, err := Parse(domain + "/" + remainder)
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
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoName + "/" + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}
// ParseAnyReferenceWithSet parses a reference string as a possible short
// identifier to be matched in a digest set, a full digest, or familiar name.
func ParseAnyReferenceWithSet(ref string, ds *digestset.Set) (Reference, error) {
if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok {
dgst, err := ds.Lookup(ref)
if err == nil {
return digestReference(dgst), nil
}
} else {
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
}
return ParseNormalizedNamed(ref)
}

View File

@@ -0,0 +1,625 @@
package reference
import (
"strconv"
"testing"
"github.com/docker/distribution/digestset"
"github.com/opencontainers/go-digest"
)
func TestValidateReferenceName(t *testing.T) {
validRepoNames := []string{
"docker/docker",
"library/debian",
"debian",
"docker.io/docker/docker",
"docker.io/library/debian",
"docker.io/debian",
"index.docker.io/docker/docker",
"index.docker.io/library/debian",
"index.docker.io/debian",
"127.0.0.1:5000/docker/docker",
"127.0.0.1:5000/library/debian",
"127.0.0.1:5000/debian",
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// This test case was moved from invalid to valid since it is valid input
// when specified with a hostname, it removes the ambiguity from about
// whether the value is an identifier or repository name
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
"docker/Docker",
"-docker",
"-docker/docker",
"-docker.io/docker/docker",
"docker///docker",
"docker.io/docker/Docker",
"docker.io/docker///docker",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
for _, name := range invalidRepoNames {
_, err := ParseNormalizedNamed(name)
if err == nil {
t.Fatalf("Expected invalid repo name for %q", name)
}
}
for _, name := range validRepoNames {
_, err := ParseNormalizedNamed(name)
if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err)
}
}
}
func TestValidateRemoteName(t *testing.T) {
validRepositoryNames := []string{
// Sanity check.
"docker/docker",
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden).
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// Allow embedded hyphens.
"docker-rules/docker",
// Allow multiple hyphens as well.
"docker---rules/docker",
//Username doc and image name docker being tested.
"doc/docker",
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
_, err := ParseNormalizedNamed(repositoryName)
if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
}
}
invalidRepositoryNames := []string{
// Disallow capital letters.
"docker/Docker",
// Only allow one slash.
"docker///docker",
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
//namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNormalizedNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
}
}
}
func TestParseRepositoryInfo(t *testing.T) {
type tcase struct {
RemoteName, FamiliarName, FullName, AmbiguousName, Domain string
}
tcases := []tcase{
{
RemoteName: "fooo/bar",
FamiliarName: "fooo/bar",
FullName: "docker.io/fooo/bar",
AmbiguousName: "index.docker.io/fooo/bar",
Domain: "docker.io",
},
{
RemoteName: "library/ubuntu",
FamiliarName: "ubuntu",
FullName: "docker.io/library/ubuntu",
AmbiguousName: "library/ubuntu",
Domain: "docker.io",
},
{
RemoteName: "nonlibrary/ubuntu",
FamiliarName: "nonlibrary/ubuntu",
FullName: "docker.io/nonlibrary/ubuntu",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "other/library",
FamiliarName: "other/library",
FullName: "docker.io/other/library",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "private/moonbase",
FamiliarName: "127.0.0.1:8000/private/moonbase",
FullName: "127.0.0.1:8000/private/moonbase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
FamiliarName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "privatebase",
FamiliarName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "privatebasee",
FamiliarName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
FamiliarName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Domain: "docker.io",
},
{
RemoteName: "library/foo",
FamiliarName: "foo",
FullName: "docker.io/library/foo",
AmbiguousName: "docker.io/foo",
Domain: "docker.io",
},
{
RemoteName: "library/foo/bar",
FamiliarName: "library/foo/bar",
FullName: "docker.io/library/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "store/foo/bar",
FamiliarName: "store/foo/bar",
FullName: "docker.io/store/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.FamiliarName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []Named
for _, r := range refStrings {
named, err := ParseNormalizedNamed(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
}
for _, r := range refs {
if expected, actual := tcase.FamiliarName, FamiliarName(r); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.FullName, r.String(); expected != actual {
t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Domain, Domain(r); expected != actual {
t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, Path(r); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
}
}
}
func TestParseReferenceWithTagAndDigest(t *testing.T) {
shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"
ref, err := ParseNormalizedNamed(shortRef)
if err != nil {
t.Fatal(err)
}
if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
if _, isTagged := ref.(NamedTagged); !isTagged {
t.Fatalf("Reference from %q should support tag", ref)
}
if _, isCanonical := ref.(Canonical); !isCanonical {
t.Fatalf("Reference from %q should support digest", ref)
}
if expected, actual := shortRef, FamiliarString(ref); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) {
if _, err := ParseNormalizedNamed("-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid name")
}
ref, err := ParseNormalizedNamed("busybox")
if err != nil {
t.Fatal(err)
}
if _, err := WithTag(ref, "-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid tag")
}
if _, err := WithDigest(ref, digest.Digest("foo")); err == nil {
t.Fatal("Expected WithDigest to detect invalid digest")
}
}
func equalReference(r1, r2 Reference) bool {
switch v1 := r1.(type) {
case digestReference:
if v2, ok := r2.(digestReference); ok {
return v1 == v2
}
case repository:
if v2, ok := r2.(repository); ok {
return v1 == v2
}
case taggedReference:
if v2, ok := r2.(taggedReference); ok {
return v1 == v2
}
case canonicalReference:
if v2, ok := r2.(canonicalReference); ok {
return v1 == v2
}
case reference:
if v2, ok := r2.(reference); ok {
return v1 == v2
}
}
return false
}
func TestParseAnyReference(t *testing.T) {
tcases := []struct {
Reference string
Equivalent string
Expected Reference
Digests []digest.Digest
}{
{
Reference: "redis",
Equivalent: "docker.io/library/redis",
},
{
Reference: "redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "docker.io/library/redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dmcgowan/myapp",
Equivalent: "docker.io/dmcgowan/myapp",
},
{
Reference: "dmcgowan/myapp:latest",
Equivalent: "docker.io/dmcgowan/myapp:latest",
},
{
Reference: "docker.io/mcgowan/myapp:latest",
Equivalent: "docker.io/mcgowan/myapp:latest",
},
{
Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Digests: []digest.Digest{
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1",
Equivalent: "docker.io/library/dbcc1",
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c",
Equivalent: "docker.io/library/dbcc1c",
Digests: []digest.Digest{
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
}
for _, tcase := range tcases {
var ref Reference
var err error
if len(tcase.Digests) == 0 {
ref, err = ParseAnyReference(tcase.Reference)
} else {
ds := digestset.NewSet()
for _, dgst := range tcase.Digests {
if err := ds.Add(dgst); err != nil {
t.Fatalf("Error adding digest %s: %v", dgst.String(), err)
}
}
ref, err = ParseAnyReferenceWithSet(tcase.Reference, ds)
}
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err)
}
if ref.String() != tcase.Equivalent {
t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent)
}
expected := tcase.Expected
if expected == nil {
expected, err = Parse(tcase.Equivalent)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err)
}
}
if !equalReference(ref, expected) {
t.Errorf("Unexpected reference %#v, expected %#v", ref, expected)
}
}
}
func TestNormalizedSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "docker.io",
name: "test_com/foo",
},
{
input: "docker/migrator",
domain: "docker.io",
name: "docker/migrator",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "xn--n3h.com/foo",
domain: "xn--n3h.com",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
{
input: "docker.io/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo/bar",
domain: "docker.io",
name: "library/foo/bar",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNormalizedNamed(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}
func TestMatchError(t *testing.T) {
named, err := ParseAnyReference("foo")
if err != nil {
t.Fatal(err)
}
_, err = FamiliarMatch("[-x]", named)
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestMatch(t *testing.T) {
matchCases := []struct {
reference string
pattern string
expected bool
}{
{
reference: "foo",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/any/bat",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/a/bar",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/b/baz",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/*/baz:tag",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/c/baz:tag",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "*/foo/c/baz",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "example.com/foo/c/baz",
expected: true,
},
}
for _, c := range matchCases {
named, err := ParseAnyReference(c.reference)
if err != nil {
t.Fatal(err)
}
actual, err := FamiliarMatch(c.pattern, named)
if err != nil {
t.Fatal(err)
}
if actual != c.expected {
t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual)
}
}
}

View File

@@ -4,30 +4,32 @@
// Grammar // Grammar
// //
// reference := name [ ":" tag ] [ "@" digest ] // reference := name [ ":" tag ] [ "@" digest ]
// name := [hostname '/'] component ['/' component]* // name := [domain '/'] path-component ['/' path-component]*
// hostname := hostcomponent ['.' hostcomponent]* [':' port-number] // domain := domain-component ['.' domain-component]* [':' port-number]
// hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/ // port-number := /[0-9]+/
// component := alpha-numeric [separator alpha-numeric]* // path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/ // alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/ // separator := /[_.]|__|[-]*/
// //
// tag := /[\w][\w.-]{0,127}/ // tag := /[\w][\w.-]{0,127}/
// //
// digest := digest-algorithm ":" digest-hex // digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
// digest-algorithm-separator := /[+.-_]/ // digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package reference package reference
import ( import (
"errors" "errors"
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
const ( const (
@@ -53,6 +55,9 @@ var (
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax. // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax) ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
) )
// Reference is an opaque object reference identifier that may include // Reference is an opaque object reference identifier that may include
@@ -126,23 +131,56 @@ type Digested interface {
} }
// Canonical reference is an object with a fully unique // Canonical reference is an object with a fully unique
// name including a name with hostname and digest // name including a name with domain and digest
type Canonical interface { type Canonical interface {
Named Named
Digest() digest.Digest Digest() digest.Digest
} }
// namedRepository is a reference to a repository with a name.
// A namedRepository has both domain and path components.
type namedRepository interface {
Named
Domain() string
Path() string
}
// Domain returns the domain part of the Named reference
func Domain(named Named) string {
if r, ok := named.(namedRepository); ok {
return r.Domain()
}
domain, _ := splitDomain(named.Name())
return domain
}
// Path returns the name without the domain part of the Named reference
func Path(named Named) (name string) {
if r, ok := named.(namedRepository); ok {
return r.Path()
}
_, path := splitDomain(named.Name())
return path
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// SplitHostname splits a named reference into a // SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is // hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value // found, the hostname is empty and the full value
// is returned as name // is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) { func SplitHostname(named Named) (string, string) {
name := named.Name() if r, ok := named.(namedRepository); ok {
match := anchoredNameRegexp.FindStringSubmatch(name) return r.Domain(), r.Path()
if len(match) != 3 {
return "", name
} }
return match[1], match[2] return splitDomain(named.Name())
} }
// Parse parses s and returns a syntactically valid Reference. // Parse parses s and returns a syntactically valid Reference.
@@ -164,13 +202,24 @@ func Parse(s string) (Reference, error) {
return nil, ErrNameTooLong return nil, ErrNameTooLong
} }
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if nameMatch != nil && len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{ ref := reference{
name: matches[1], namedRepository: repo,
tag: matches[2], tag: matches[2],
} }
if matches[3] != "" { if matches[3] != "" {
var err error var err error
ref.digest, err = digest.ParseDigest(matches[3]) ref.digest, err = digest.Parse(matches[3])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -185,18 +234,17 @@ func Parse(s string) (Reference, error) {
} }
// ParseNamed parses s and returns a syntactically valid reference implementing // ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name, otherwise an error is // the Named interface. The reference must have a name and be in the canonical
// returned. // form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference. // If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests. // NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) { func ParseNamed(s string) (Named, error) {
ref, err := Parse(s) named, err := ParseNormalizedNamed(s)
if err != nil { if err != nil {
return nil, err return nil, err
} }
named, isNamed := ref.(Named) if named.String() != s {
if !isNamed { return nil, ErrNameNotCanonical
return nil, fmt.Errorf("reference %s has no name", ref.String())
} }
return named, nil return named, nil
} }
@@ -207,10 +255,15 @@ func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax { if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong return nil, ErrNameTooLong
} }
if !anchoredNameRegexp.MatchString(name) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat return nil, ErrReferenceInvalidFormat
} }
return repository(name), nil return repository{
domain: match[1],
path: match[2],
}, nil
} }
// WithTag combines the name from "name" and the tag from "tag" to form a // WithTag combines the name from "name" and the tag from "tag" to form a
@@ -219,15 +272,22 @@ func WithTag(name Named, tag string) (NamedTagged, error) {
if !anchoredTagRegexp.MatchString(tag) { if !anchoredTagRegexp.MatchString(tag) {
return nil, ErrTagInvalidFormat return nil, ErrTagInvalidFormat
} }
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if canonical, ok := name.(Canonical); ok { if canonical, ok := name.(Canonical); ok {
return reference{ return reference{
name: name.Name(), namedRepository: repo,
tag: tag, tag: tag,
digest: canonical.Digest(), digest: canonical.Digest(),
}, nil }, nil
} }
return taggedReference{ return taggedReference{
name: name.Name(), namedRepository: repo,
tag: tag, tag: tag,
}, nil }, nil
} }
@@ -238,36 +298,37 @@ func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) { if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat return nil, ErrDigestInvalidFormat
} }
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if tagged, ok := name.(Tagged); ok { if tagged, ok := name.(Tagged); ok {
return reference{ return reference{
name: name.Name(), namedRepository: repo,
tag: tagged.Tag(), tag: tagged.Tag(),
digest: digest, digest: digest,
}, nil }, nil
} }
return canonicalReference{ return canonicalReference{
name: name.Name(), namedRepository: repo,
digest: digest, digest: digest,
}, nil }, nil
} }
// Match reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func Match(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, ref.String())
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, namedRef.Name())
}
return matched, err
}
// TrimNamed removes any tag or digest from the named reference. // TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named { func TrimNamed(ref Named) Named {
return repository(ref.Name()) domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
}
} }
func getBestReferenceType(ref reference) Reference { func getBestReferenceType(ref reference) Reference {
if ref.name == "" { if ref.Name() == "" {
// Allow digest only references // Allow digest only references
if ref.digest != "" { if ref.digest != "" {
return digestReference(ref.digest) return digestReference(ref.digest)
@@ -277,15 +338,15 @@ func getBestReferenceType(ref reference) Reference {
if ref.tag == "" { if ref.tag == "" {
if ref.digest != "" { if ref.digest != "" {
return canonicalReference{ return canonicalReference{
name: ref.name, namedRepository: ref.namedRepository,
digest: ref.digest, digest: ref.digest,
} }
} }
return repository(ref.name) return ref.namedRepository
} }
if ref.digest == "" { if ref.digest == "" {
return taggedReference{ return taggedReference{
name: ref.name, namedRepository: ref.namedRepository,
tag: ref.tag, tag: ref.tag,
} }
} }
@@ -294,17 +355,13 @@ func getBestReferenceType(ref reference) Reference {
} }
type reference struct { type reference struct {
name string namedRepository
tag string tag string
digest digest.Digest digest digest.Digest
} }
func (r reference) String() string { func (r reference) String() string {
return r.name + ":" + r.tag + "@" + r.digest.String() return r.Name() + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Name() string {
return r.name
} }
func (r reference) Tag() string { func (r reference) Tag() string {
@@ -315,20 +372,34 @@ func (r reference) Digest() digest.Digest {
return r.digest return r.digest
} }
type repository string type repository struct {
domain string
path string
}
func (r repository) String() string { func (r repository) String() string {
return string(r) return r.Name()
} }
func (r repository) Name() string { func (r repository) Name() string {
return string(r) if r.domain == "" {
return r.path
}
return r.domain + "/" + r.path
}
func (r repository) Domain() string {
return r.domain
}
func (r repository) Path() string {
return r.path
} }
type digestReference digest.Digest type digestReference digest.Digest
func (d digestReference) String() string { func (d digestReference) String() string {
return d.String() return digest.Digest(d).String()
} }
func (d digestReference) Digest() digest.Digest { func (d digestReference) Digest() digest.Digest {
@@ -336,16 +407,12 @@ func (d digestReference) Digest() digest.Digest {
} }
type taggedReference struct { type taggedReference struct {
name string namedRepository
tag string tag string
} }
func (t taggedReference) String() string { func (t taggedReference) String() string {
return t.name + ":" + t.tag return t.Name() + ":" + t.tag
}
func (t taggedReference) Name() string {
return t.name
} }
func (t taggedReference) Tag() string { func (t taggedReference) Tag() string {
@@ -353,16 +420,12 @@ func (t taggedReference) Tag() string {
} }
type canonicalReference struct { type canonicalReference struct {
name string namedRepository
digest digest.Digest digest digest.Digest
} }
func (c canonicalReference) String() string { func (c canonicalReference) String() string {
return c.name + "@" + c.digest.String() return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Name() string {
return c.name
} }
func (c canonicalReference) Digest() digest.Digest { func (c canonicalReference) Digest() digest.Digest {

View File

@@ -1,12 +1,14 @@
package reference package reference
import ( import (
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/json" "encoding/json"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
) )
func TestReferenceParse(t *testing.T) { func TestReferenceParse(t *testing.T) {
@@ -19,8 +21,8 @@ func TestReferenceParse(t *testing.T) {
err error err error
// repository is the string representation for the reference // repository is the string representation for the reference
repository string repository string
// hostname is the hostname expected in the reference // domain is the domain expected in the reference
hostname string domain string
// tag is the tag for the reference // tag is the tag for the reference
tag string tag string
// digest is the digest for the reference (enforces digest reference) // digest is the digest for the reference (enforces digest reference)
@@ -42,37 +44,37 @@ func TestReferenceParse(t *testing.T) {
}, },
{ {
input: "test.com/repo:tag", input: "test.com/repo:tag",
hostname: "test.com", domain: "test.com",
repository: "test.com/repo", repository: "test.com/repo",
tag: "tag", tag: "tag",
}, },
{ {
input: "test:5000/repo", input: "test:5000/repo",
hostname: "test:5000", domain: "test:5000",
repository: "test:5000/repo", repository: "test:5000/repo",
}, },
{ {
input: "test:5000/repo:tag", input: "test:5000/repo:tag",
hostname: "test:5000", domain: "test:5000",
repository: "test:5000/repo", repository: "test:5000/repo",
tag: "tag", tag: "tag",
}, },
{ {
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
hostname: "test:5000", domain: "test:5000",
repository: "test:5000/repo", repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}, },
{ {
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
hostname: "test:5000", domain: "test:5000",
repository: "test:5000/repo", repository: "test:5000/repo",
tag: "tag", tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}, },
{ {
input: "test:5000/repo", input: "test:5000/repo",
hostname: "test:5000", domain: "test:5000",
repository: "test:5000/repo", repository: "test:5000/repo",
}, },
{ {
@@ -120,7 +122,7 @@ func TestReferenceParse(t *testing.T) {
}, },
{ {
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
hostname: "a", domain: "a",
repository: strings.Repeat("a/", 127) + "a", repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max", tag: "tag-puts-this-over-max",
}, },
@@ -130,30 +132,30 @@ func TestReferenceParse(t *testing.T) {
}, },
{ {
input: "sub-dom1.foo.com/bar/baz/quux", input: "sub-dom1.foo.com/bar/baz/quux",
hostname: "sub-dom1.foo.com", domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux", repository: "sub-dom1.foo.com/bar/baz/quux",
}, },
{ {
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
hostname: "sub-dom1.foo.com", domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux", repository: "sub-dom1.foo.com/bar/baz/quux",
tag: "some-long-tag", tag: "some-long-tag",
}, },
{ {
input: "b.gcr.io/test.example.com/my-app:test.example.com", input: "b.gcr.io/test.example.com/my-app:test.example.com",
hostname: "b.gcr.io", domain: "b.gcr.io",
repository: "b.gcr.io/test.example.com/my-app", repository: "b.gcr.io/test.example.com/my-app",
tag: "test.example.com", tag: "test.example.com",
}, },
{ {
input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
hostname: "xn--n3h.com", domain: "xn--n3h.com",
repository: "xn--n3h.com/myimage", repository: "xn--n3h.com/myimage",
tag: "xn--n3h.com", tag: "xn--n3h.com",
}, },
{ {
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
hostname: "xn--7o8h.com", domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage", repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com", tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@@ -165,7 +167,7 @@ func TestReferenceParse(t *testing.T) {
}, },
{ {
input: "foo/foo_bar.com:8080", input: "foo/foo_bar.com:8080",
hostname: "foo", domain: "foo",
repository: "foo/foo_bar.com", repository: "foo/foo_bar.com",
tag: "8080", tag: "8080",
}, },
@@ -196,11 +198,11 @@ func TestReferenceParse(t *testing.T) {
if named.Name() != testcase.repository { if named.Name() != testcase.repository {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository) failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
} }
hostname, _ := SplitHostname(named) domain, _ := SplitHostname(named)
if hostname != testcase.hostname { if domain != testcase.domain {
failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
} }
} else if testcase.repository != "" || testcase.hostname != "" { } else if testcase.repository != "" || testcase.domain != "" {
failf("expected named type, got %T", repo) failf("expected named type, got %T", repo)
} }
@@ -281,37 +283,37 @@ func TestWithNameFailure(t *testing.T) {
func TestSplitHostname(t *testing.T) { func TestSplitHostname(t *testing.T) {
testcases := []struct { testcases := []struct {
input string input string
hostname string domain string
name string name string
}{ }{
{ {
input: "test.com/foo", input: "test.com/foo",
hostname: "test.com", domain: "test.com",
name: "foo", name: "foo",
}, },
{ {
input: "test_com/foo", input: "test_com/foo",
hostname: "", domain: "",
name: "test_com/foo", name: "test_com/foo",
}, },
{ {
input: "test:8080/foo", input: "test:8080/foo",
hostname: "test:8080", domain: "test:8080",
name: "foo", name: "foo",
}, },
{ {
input: "test.com:8080/foo", input: "test.com:8080/foo",
hostname: "test.com:8080", domain: "test.com:8080",
name: "foo", name: "foo",
}, },
{ {
input: "test-com:8080/foo", input: "test-com:8080/foo",
hostname: "test-com:8080", domain: "test-com:8080",
name: "foo", name: "foo",
}, },
{ {
input: "xn--n3h.com:18080/foo", input: "xn--n3h.com:18080/foo",
hostname: "xn--n3h.com:18080", domain: "xn--n3h.com:18080",
name: "foo", name: "foo",
}, },
} }
@@ -325,9 +327,9 @@ func TestSplitHostname(t *testing.T) {
if err != nil { if err != nil {
failf("error parsing name: %s", err) failf("error parsing name: %s", err)
} }
hostname, name := SplitHostname(named) domain, name := SplitHostname(named)
if hostname != testcase.hostname { if domain != testcase.domain {
failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
} }
if name != testcase.name { if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name) failf("unexpected name: got %q, expected %q", name, testcase.name)
@@ -582,80 +584,76 @@ func TestWithDigest(t *testing.T) {
} }
} }
func TestMatchError(t *testing.T) { func TestParseNamed(t *testing.T) {
named, err := Parse("foo") testcases := []struct {
if err != nil { input string
t.Fatal(err) domain string
} name string
_, err = Match("[-x]", named) err error
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestMatch(t *testing.T) {
matchCases := []struct {
reference string
pattern string
expected bool
}{ }{
{ {
reference: "foo", input: "test.com/foo",
pattern: "foo/**/ba[rz]", domain: "test.com",
expected: false, name: "foo",
}, },
{ {
reference: "foo/any/bat", input: "test:8080/foo",
pattern: "foo/**/ba[rz]", domain: "test:8080",
expected: false, name: "foo",
}, },
{ {
reference: "foo/a/bar", input: "test_com/foo",
pattern: "foo/**/ba[rz]", err: ErrNameNotCanonical,
expected: true,
}, },
{ {
reference: "foo/b/baz", input: "test.com",
pattern: "foo/**/ba[rz]", err: ErrNameNotCanonical,
expected: true,
}, },
{ {
reference: "foo/c/baz:tag", input: "foo",
pattern: "foo/**/ba[rz]", err: ErrNameNotCanonical,
expected: true,
}, },
{ {
reference: "foo/c/baz:tag", input: "library/foo",
pattern: "foo/*/baz:tag", err: ErrNameNotCanonical,
expected: true,
}, },
{ {
reference: "foo/c/baz:tag", input: "docker.io/library/foo",
pattern: "foo/c/baz:tag", domain: "docker.io",
expected: true, name: "library/foo",
}, },
// Ambiguous case, parser will add "library/" to foo
{ {
reference: "example.com/foo/c/baz:tag", input: "docker.io/foo",
pattern: "*/foo/c/baz", err: ErrNameNotCanonical,
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "example.com/foo/c/baz",
expected: true,
}, },
} }
for _, c := range matchCases { for _, testcase := range testcases {
named, err := Parse(c.reference) failf := func(format string, v ...interface{}) {
if err != nil { t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fatal(err) t.Fail()
} }
actual, err := Match(c.pattern, named)
if err != nil { named, err := ParseNamed(testcase.input)
t.Fatal(err) if err != nil && testcase.err == nil {
failf("error parsing name: %s", err)
continue
} else if err == nil && testcase.err != nil {
failf("parsing succeded: expected error %v", testcase.err)
continue
} else if err != testcase.err {
failf("unexpected error %v, expected %v", err, testcase.err)
continue
} else if err != nil {
continue
} }
if actual != c.expected {
t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual) domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
} }
} }
} }

View File

@@ -19,18 +19,18 @@ var (
alphaNumericRegexp, alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp))) optional(repeated(separatorRegexp, alphaNumericRegexp)))
// hostnameComponentRegexp restricts the registry hostname component of a // domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by hostnameRegexp // repository name to start with a component as defined by DomainRegexp
// and followed by an optional port. // and followed by an optional port.
hostnameComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// hostnameRegexp defines the structure of potential hostname components // DomainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is // that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image // allowed by DNS to ensure backwards compatibility with Docker image
// names. // names.
hostnameRegexp = expression( DomainRegexp = expression(
hostnameComponentRegexp, domainComponentRegexp,
optional(repeated(literal(`.`), hostnameComponentRegexp)), optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`))) optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go. // TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
@@ -48,17 +48,17 @@ var (
anchoredDigestRegexp = anchored(DigestRegexp) anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The // NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the hostname and name part omitting // regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either. // the separating forward slash from either.
NameRegexp = expression( NameRegexp = expression(
optional(hostnameRegexp, literal(`/`)), optional(DomainRegexp, literal(`/`)),
nameComponentRegexp, nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))) optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the // anchoredNameRegexp is used to parse a name value, capturing the
// hostname and trailing components. // domain and trailing components.
anchoredNameRegexp = anchored( anchoredNameRegexp = anchored(
optional(capture(hostnameRegexp), literal(`/`)), optional(capture(DomainRegexp), literal(`/`)),
capture(nameComponentRegexp, capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))) optional(repeated(literal(`/`), nameComponentRegexp))))
@@ -68,6 +68,25 @@ var (
ReferenceRegexp = anchored(capture(NameRegexp), ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)), optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp))) optional(literal("@"), capture(DigestRegexp)))
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = match(`([a-f0-9]{64})`)
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = anchored(IdentifierRegexp)
// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)
) )
// match compiles the string to a regular expression. // match compiles the string to a regular expression.

View File

@@ -33,7 +33,7 @@ func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
} }
} }
func TestHostRegexp(t *testing.T) { func TestDomainRegexp(t *testing.T) {
hostcases := []regexpMatch{ hostcases := []regexpMatch{
{ {
input: "test.com", input: "test.com",
@@ -116,7 +116,7 @@ func TestHostRegexp(t *testing.T) {
match: true, match: true,
}, },
} }
r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`) r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`)
for i := range hostcases { for i := range hostcases {
checkRegexp(t, r, hostcases[i]) checkRegexp(t, r, hostcases[i])
} }
@@ -487,3 +487,67 @@ func TestReferenceRegexp(t *testing.T) {
} }
} }
func TestIdentifierRegexp(t *testing.T) {
fullCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: false,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
}
shortCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: true,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
{
input: "da304",
match: false,
},
{
input: "da304e",
match: true,
},
}
for i := range fullCases {
checkRegexp(t, anchoredIdentifierRegexp, fullCases[i])
}
for i := range shortCases {
checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i])
}
}

View File

@@ -35,7 +35,7 @@ type Namespace interface {
// reference. // reference.
Repository(ctx context.Context, name reference.Named) (Repository, error) Repository(ctx context.Context, name reference.Named) (Repository, error)
// Repositories fills 'repos' with a lexigraphically sorted catalog of repositories // Repositories fills 'repos' with a lexicographically sorted catalog of repositories
// up to the size of 'repos' and returns the value 'n' for the number of entries // up to the size of 'repos' and returns the value 'n' for the number of entries
// which were filled. 'last' contains an offset in the catalog, and 'err' will be // which were filled. 'last' contains an offset in the catalog, and 'err' will be
// set to io.EOF if there are no more entries to obtain. // set to io.EOF if there are no more entries to obtain.

View File

@@ -4,9 +4,9 @@ import (
"net/http" "net/http"
"regexp" "regexp"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/opencontainers/go-digest"
) )
var ( var (

View File

@@ -1,6 +1,7 @@
package v2 package v2
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -149,6 +150,8 @@ func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) {
tagOrDigest = v.Tag() tagOrDigest = v.Tag()
case reference.Digested: case reference.Digested:
tagOrDigest = v.Digest().String() tagOrDigest = v.Digest().String()
default:
return "", fmt.Errorf("reference must have a tag or digest")
} }
manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest) manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest)

View File

@@ -1,8 +1,10 @@
package v2 package v2
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"reflect"
"testing" "testing"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@@ -11,35 +13,48 @@ import (
type urlBuilderTestCase struct { type urlBuilderTestCase struct {
description string description string
expectedPath string expectedPath string
expectedErr error
build func() (string, error) build func() (string, error)
} }
func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase { func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
fooBarRef, _ := reference.ParseNamed("foo/bar") fooBarRef, _ := reference.WithName("foo/bar")
return []urlBuilderTestCase{ return []urlBuilderTestCase{
{ {
description: "test base url", description: "test base url",
expectedPath: "/v2/", expectedPath: "/v2/",
expectedErr: nil,
build: urlBuilder.BuildBaseURL, build: urlBuilder.BuildBaseURL,
}, },
{ {
description: "test tags url", description: "test tags url",
expectedPath: "/v2/foo/bar/tags/list", expectedPath: "/v2/foo/bar/tags/list",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
return urlBuilder.BuildTagsURL(fooBarRef) return urlBuilder.BuildTagsURL(fooBarRef)
}, },
}, },
{ {
description: "test manifest url", description: "test manifest url tagged ref",
expectedPath: "/v2/foo/bar/manifests/tag", expectedPath: "/v2/foo/bar/manifests/tag",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
ref, _ := reference.WithTag(fooBarRef, "tag") ref, _ := reference.WithTag(fooBarRef, "tag")
return urlBuilder.BuildManifestURL(ref) return urlBuilder.BuildManifestURL(ref)
}, },
}, },
{
description: "test manifest url bare ref",
expectedPath: "",
expectedErr: fmt.Errorf("reference must have a tag or digest"),
build: func() (string, error) {
return urlBuilder.BuildManifestURL(fooBarRef)
},
},
{ {
description: "build blob url", description: "build blob url",
expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5", expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
ref, _ := reference.WithDigest(fooBarRef, "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5") ref, _ := reference.WithDigest(fooBarRef, "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5")
return urlBuilder.BuildBlobURL(ref) return urlBuilder.BuildBlobURL(ref)
@@ -48,6 +63,7 @@ func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
{ {
description: "build blob upload url", description: "build blob upload url",
expectedPath: "/v2/foo/bar/blobs/uploads/", expectedPath: "/v2/foo/bar/blobs/uploads/",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL(fooBarRef) return urlBuilder.BuildBlobUploadURL(fooBarRef)
}, },
@@ -55,6 +71,7 @@ func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
{ {
description: "build blob upload url with digest and size", description: "build blob upload url with digest and size",
expectedPath: "/v2/foo/bar/blobs/uploads/?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000", expectedPath: "/v2/foo/bar/blobs/uploads/?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL(fooBarRef, url.Values{ return urlBuilder.BuildBlobUploadURL(fooBarRef, url.Values{
"size": []string{"10000"}, "size": []string{"10000"},
@@ -65,6 +82,7 @@ func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
{ {
description: "build blob upload chunk url", description: "build blob upload chunk url",
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part", expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part") return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part")
}, },
@@ -72,6 +90,7 @@ func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
{ {
description: "build blob upload chunk url with digest and size", description: "build blob upload chunk url with digest and size",
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000", expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
expectedErr: nil,
build: func() (string, error) { build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part", url.Values{ return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part", url.Values{
"size": []string{"10000"}, "size": []string{"10000"},
@@ -101,9 +120,14 @@ func TestURLBuilder(t *testing.T) {
for _, testCase := range makeURLBuilderTestCases(urlBuilder) { for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
url, err := testCase.build() url, err := testCase.build()
if err != nil { expectedErr := testCase.expectedErr
t.Fatalf("%s: error building url: %v", testCase.description, err) if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
} }
if expectedErr != nil {
continue
}
expectedURL := testCase.expectedPath expectedURL := testCase.expectedPath
if !relative { if !relative {
expectedURL = root + expectedURL expectedURL = root + expectedURL
@@ -136,8 +160,12 @@ func TestURLBuilderWithPrefix(t *testing.T) {
for _, testCase := range makeURLBuilderTestCases(urlBuilder) { for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
url, err := testCase.build() url, err := testCase.build()
if err != nil { expectedErr := testCase.expectedErr
t.Fatalf("%s: error building url: %v", testCase.description, err) if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
}
if expectedErr != nil {
continue
} }
expectedURL := testCase.expectedPath expectedURL := testCase.expectedPath
@@ -383,8 +411,12 @@ func TestBuilderFromRequest(t *testing.T) {
for _, testCase := range makeURLBuilderTestCases(builder) { for _, testCase := range makeURLBuilderTestCases(builder) {
buildURL, err := testCase.build() buildURL, err := testCase.build()
if err != nil { expectedErr := testCase.expectedErr
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err) if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
}
if expectedErr != nil {
continue
} }
expectedURL := testCase.expectedPath expectedURL := testCase.expectedPath
@@ -452,8 +484,12 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) {
for _, testCase := range makeURLBuilderTestCases(builder) { for _, testCase := range makeURLBuilderTestCases(builder) {
buildURL, err := testCase.build() buildURL, err := testCase.build()
if err != nil { expectedErr := testCase.expectedErr
t.Fatalf("%s: error building url: %v", testCase.description, err) if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
}
if expectedErr != nil {
continue
} }
var expectedURL string var expectedURL string

View File

@@ -454,6 +454,27 @@ func TestAccessController(t *testing.T) {
if userInfo.Name != "foo" { if userInfo.Name != "foo" {
t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name) t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name)
} }
// 5. Supply a token with full admin rights, which is represented as "*".
token, err = makeTestToken(
issuer, service,
[]*ResourceActions{{
Type: testAccess.Type,
Name: testAccess.Name,
Actions: []string{"*"},
}},
rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
_, err = accessController.Authorized(ctx, testAccess)
if err != nil {
t.Fatalf("accessController returned unexpected error: %s", err)
}
} }
// This tests that newAccessController can handle PEM blocks in the certificate // This tests that newAccessController can handle PEM blocks in the certificate

View File

@@ -155,7 +155,9 @@ type RepositoryScope struct {
// using the scope grammar // using the scope grammar
func (rs RepositoryScope) String() string { func (rs RepositoryScope) String() string {
repoType := "repository" repoType := "repository"
if rs.Class != "" { // Keep existing format for image class to maintain backwards compatibility
// with authorization servers which do not support the expanded grammar.
if rs.Class != "" && rs.Class != "image" {
repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class) repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class)
} }
return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ",")) return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ","))

View File

@@ -15,12 +15,12 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/client/transport"
"github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/cache/memory"
"github.com/opencontainers/go-digest"
) )
// Registry provides an interface for calling Repositories, which returns a catalog of repositories. // Registry provides an interface for calling Repositories, which returns a catalog of repositories.
@@ -268,7 +268,7 @@ func descriptorFromResponse(response *http.Response) (distribution.Descriptor, e
return desc, nil return desc, nil
} }
dgst, err := digest.ParseDigest(digestHeader) dgst, err := digest.Parse(digestHeader)
if err != nil { if err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err
} }
@@ -475,7 +475,7 @@ func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...dis
return nil, distribution.ErrManifestNotModified return nil, distribution.ErrManifestNotModified
} else if SuccessStatus(resp.StatusCode) { } else if SuccessStatus(resp.StatusCode) {
if contentDgst != nil { if contentDgst != nil {
dgst, err := digest.ParseDigest(resp.Header.Get("Docker-Content-Digest")) dgst, err := digest.Parse(resp.Header.Get("Docker-Content-Digest"))
if err == nil { if err == nil {
*contentDgst = dgst *contentDgst = dgst
} }
@@ -553,7 +553,7 @@ func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options .
if SuccessStatus(resp.StatusCode) { if SuccessStatus(resp.StatusCode) {
dgstHeader := resp.Header.Get("Docker-Content-Digest") dgstHeader := resp.Header.Get("Docker-Content-Digest")
dgst, err := digest.ParseDigest(dgstHeader) dgst, err := digest.Parse(dgstHeader)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -661,7 +661,7 @@ func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribut
if err != nil { if err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err
} }
dgstr := digest.Canonical.New() dgstr := digest.Canonical.Digester()
n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash())) n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash()))
if err != nil { if err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err

View File

@@ -16,7 +16,6 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@@ -25,6 +24,7 @@ import (
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/docker/distribution/uuid" "github.com/docker/distribution/uuid"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
func testServer(rrm testutil.RequestResponseMap) (string, func()) { func testServer(rrm testutil.RequestResponseMap) (string, func()) {
@@ -100,7 +100,7 @@ func addTestCatalog(route string, content []byte, link string, m *testutil.Reque
func TestBlobDelete(t *testing.T) { func TestBlobDelete(t *testing.T) {
dgst, _ := newRandomBlob(1024) dgst, _ := newRandomBlob(1024)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
repo, _ := reference.ParseNamed("test.example.com/repo1") repo, _ := reference.WithName("test.example.com/repo1")
m = append(m, testutil.RequestResponseMapping{ m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
Method: "DELETE", Method: "DELETE",
@@ -139,7 +139,7 @@ func TestBlobFetch(t *testing.T) {
defer c() defer c()
ctx := context.Background() ctx := context.Background()
repo, _ := reference.ParseNamed("test.example.com/repo1") repo, _ := reference.WithName("test.example.com/repo1")
r, err := NewRepository(ctx, repo, e, nil) r, err := NewRepository(ctx, repo, e, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -160,7 +160,7 @@ func TestBlobFetch(t *testing.T) {
func TestBlobExistsNoContentLength(t *testing.T) { func TestBlobExistsNoContentLength(t *testing.T) {
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
repo, _ := reference.ParseNamed("biff") repo, _ := reference.WithName("biff")
dgst, content := newRandomBlob(1024) dgst, content := newRandomBlob(1024)
m = append(m, testutil.RequestResponseMapping{ m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
@@ -219,7 +219,7 @@ func TestBlobExists(t *testing.T) {
defer c() defer c()
ctx := context.Background() ctx := context.Background()
repo, _ := reference.ParseNamed("test.example.com/repo1") repo, _ := reference.WithName("test.example.com/repo1")
r, err := NewRepository(ctx, repo, e, nil) r, err := NewRepository(ctx, repo, e, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -251,7 +251,7 @@ func TestBlobUploadChunked(t *testing.T) {
b1[512:513], b1[512:513],
b1[513:1024], b1[513:1024],
} }
repo, _ := reference.ParseNamed("test.example.com/uploadrepo") repo, _ := reference.WithName("test.example.com/uploadrepo")
uuids := []string{uuid.Generate().String()} uuids := []string{uuid.Generate().String()}
m = append(m, testutil.RequestResponseMapping{ m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
@@ -366,7 +366,7 @@ func TestBlobUploadChunked(t *testing.T) {
func TestBlobUploadMonolithic(t *testing.T) { func TestBlobUploadMonolithic(t *testing.T) {
dgst, b1 := newRandomBlob(1024) dgst, b1 := newRandomBlob(1024)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
repo, _ := reference.ParseNamed("test.example.com/uploadrepo") repo, _ := reference.WithName("test.example.com/uploadrepo")
uploadID := uuid.Generate().String() uploadID := uuid.Generate().String()
m = append(m, testutil.RequestResponseMapping{ m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
@@ -474,9 +474,9 @@ func TestBlobUploadMonolithic(t *testing.T) {
func TestBlobMount(t *testing.T) { func TestBlobMount(t *testing.T) {
dgst, content := newRandomBlob(1024) dgst, content := newRandomBlob(1024)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
repo, _ := reference.ParseNamed("test.example.com/uploadrepo") repo, _ := reference.WithName("test.example.com/uploadrepo")
sourceRepo, _ := reference.ParseNamed("test.example.com/sourcerepo") sourceRepo, _ := reference.WithName("test.example.com/sourcerepo")
canonicalRef, _ := reference.WithDigest(sourceRepo, dgst) canonicalRef, _ := reference.WithDigest(sourceRepo, dgst)
m = append(m, testutil.RequestResponseMapping{ m = append(m, testutil.RequestResponseMapping{
@@ -678,7 +678,7 @@ func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
func TestV1ManifestFetch(t *testing.T) { func TestV1ManifestFetch(t *testing.T) {
ctx := context.Background() ctx := context.Background()
repo, _ := reference.ParseNamed("test.example.com/repo") repo, _ := reference.WithName("test.example.com/repo")
m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
_, pl, err := m1.Payload() _, pl, err := m1.Payload()
@@ -755,7 +755,7 @@ func TestV1ManifestFetch(t *testing.T) {
} }
func TestManifestFetchWithEtag(t *testing.T) { func TestManifestFetchWithEtag(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo/by/tag") repo, _ := reference.WithName("test.example.com/repo/by/tag")
_, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6) _, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
addTestManifestWithEtag(repo, "latest", p1, &m, d1.String()) addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
@@ -785,7 +785,7 @@ func TestManifestFetchWithEtag(t *testing.T) {
} }
func TestManifestDelete(t *testing.T) { func TestManifestDelete(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo/delete") repo, _ := reference.WithName("test.example.com/repo/delete")
_, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6) _, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
_, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6) _, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
@@ -825,7 +825,7 @@ func TestManifestDelete(t *testing.T) {
} }
func TestManifestPut(t *testing.T) { func TestManifestPut(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo/delete") repo, _ := reference.WithName("test.example.com/repo/delete")
m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6) m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
_, payload, err := m1.Payload() _, payload, err := m1.Payload()
@@ -890,7 +890,7 @@ func TestManifestPut(t *testing.T) {
} }
func TestManifestTags(t *testing.T) { func TestManifestTags(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo/tags/list") repo, _ := reference.WithName("test.example.com/repo/tags/list")
tagsList := []byte(strings.TrimSpace(` tagsList := []byte(strings.TrimSpace(`
{ {
"name": "test.example.com/repo/tags/list", "name": "test.example.com/repo/tags/list",
@@ -952,7 +952,7 @@ func TestManifestTags(t *testing.T) {
} }
func TestObtainsErrorForMissingTag(t *testing.T) { func TestObtainsErrorForMissingTag(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo") repo, _ := reference.WithName("test.example.com/repo")
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
var errors errcode.Errors var errors errcode.Errors
@@ -998,7 +998,7 @@ func TestManifestTagsPaginated(t *testing.T) {
s := httptest.NewServer(http.NotFoundHandler()) s := httptest.NewServer(http.NotFoundHandler())
defer s.Close() defer s.Close()
repo, _ := reference.ParseNamed("test.example.com/repo/tags/list") repo, _ := reference.WithName("test.example.com/repo/tags/list")
tagsList := []string{"tag1", "tag2", "funtag"} tagsList := []string{"tag1", "tag2", "funtag"}
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@@ -1067,7 +1067,7 @@ func TestManifestTagsPaginated(t *testing.T) {
} }
func TestManifestUnauthorized(t *testing.T) { func TestManifestUnauthorized(t *testing.T) {
repo, _ := reference.ParseNamed("test.example.com/repo") repo, _ := reference.WithName("test.example.com/repo")
_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) _, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -21,7 +22,6 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
@@ -29,16 +29,24 @@ import (
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/factory"
_ "github.com/docker/distribution/registry/storage/driver/testdriver" _ "github.com/docker/distribution/registry/storage/driver/testdriver"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
) )
var headerConfig = http.Header{ var headerConfig = http.Header{
"X-Content-Type-Options": []string{"nosniff"}, "X-Content-Type-Options": []string{"nosniff"},
} }
const (
// digestSha256EmptyTar is the canonical sha256 digest of empty data
digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
// TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified // TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified
// 200 OK response. // 200 OK response.
func TestCheckAPI(t *testing.T) { func TestCheckAPI(t *testing.T) {
@@ -272,7 +280,7 @@ func makeBlobArgs(t *testing.T) blobArgs {
layerFile: layerFile, layerFile: layerFile,
layerDigest: layerDigest, layerDigest: layerDigest,
} }
args.imageName, _ = reference.ParseNamed("foo/bar") args.imageName, _ = reference.WithName("foo/bar")
return args return args
} }
@@ -514,7 +522,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
// Now, push just a chunk // Now, push just a chunk
layerFile.Seek(0, 0) layerFile.Seek(0, 0)
canonicalDigester := digest.Canonical.New() canonicalDigester := digest.Canonical.Digester()
if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil {
t.Fatalf("error copying to digest: %v", err) t.Fatalf("error copying to digest: %v", err)
} }
@@ -552,10 +560,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
}) })
// Verify the body // Verify the body
verifier, err := digest.NewDigestVerifier(layerDigest) verifier := layerDigest.Verifier()
if err != nil {
t.Fatalf("unexpected error getting digest verifier: %s", err)
}
io.Copy(verifier, resp.Body) io.Copy(verifier, resp.Body)
if !verifier.Verified() { if !verifier.Verified() {
@@ -675,7 +680,7 @@ func testBlobDelete(t *testing.T, env *testEnv, args blobArgs) {
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
layerFile.Seek(0, os.SEEK_SET) layerFile.Seek(0, os.SEEK_SET)
canonicalDigester := digest.Canonical.New() canonicalDigester := digest.Canonical.Digester()
if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil {
t.Fatalf("error copying to digest: %v", err) t.Fatalf("error copying to digest: %v", err)
} }
@@ -700,7 +705,7 @@ func TestDeleteDisabled(t *testing.T) {
env := newTestEnv(t, false) env := newTestEnv(t, false)
defer env.Shutdown() defer env.Shutdown()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
// "build" our layer file // "build" our layer file
layerFile, layerDigest, err := testutil.CreateRandomTarFile() layerFile, layerDigest, err := testutil.CreateRandomTarFile()
if err != nil { if err != nil {
@@ -727,7 +732,7 @@ func TestDeleteReadOnly(t *testing.T) {
env := newTestEnv(t, true) env := newTestEnv(t, true)
defer env.Shutdown() defer env.Shutdown()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
// "build" our layer file // "build" our layer file
layerFile, layerDigest, err := testutil.CreateRandomTarFile() layerFile, layerDigest, err := testutil.CreateRandomTarFile()
if err != nil { if err != nil {
@@ -757,7 +762,7 @@ func TestStartPushReadOnly(t *testing.T) {
defer env.Shutdown() defer env.Shutdown()
env.app.readOnly = true env.app.readOnly = true
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName)
if err != nil { if err != nil {
@@ -795,8 +800,8 @@ type manifestArgs struct {
} }
func TestManifestAPI(t *testing.T) { func TestManifestAPI(t *testing.T) {
schema1Repo, _ := reference.ParseNamed("foo/schema1") schema1Repo, _ := reference.WithName("foo/schema1")
schema2Repo, _ := reference.ParseNamed("foo/schema2") schema2Repo, _ := reference.WithName("foo/schema2")
deleteEnabled := false deleteEnabled := false
env1 := newTestEnv(t, deleteEnabled) env1 := newTestEnv(t, deleteEnabled)
@@ -813,9 +818,96 @@ func TestManifestAPI(t *testing.T) {
testManifestAPIManifestList(t, env2, schema2Args) testManifestAPIManifestList(t, env2, schema2Args)
} }
// storageManifestErrDriverFactory implements the factory.StorageDriverFactory interface.
type storageManifestErrDriverFactory struct{}
const (
repositoryWithManifestNotFound = "manifesttagnotfound"
repositoryWithManifestInvalidPath = "manifestinvalidpath"
repositoryWithManifestBadLink = "manifestbadlink"
repositoryWithGenericStorageError = "genericstorageerr"
)
func (factory *storageManifestErrDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
// Initialize the mock driver
var errGenericStorage = errors.New("generic storage error")
return &mockErrorDriver{
returnErrs: []mockErrorMapping{
{
pathMatch: fmt.Sprintf("%s/_manifests/tags", repositoryWithManifestNotFound),
content: nil,
err: storagedriver.PathNotFoundError{},
},
{
pathMatch: fmt.Sprintf("%s/_manifests/tags", repositoryWithManifestInvalidPath),
content: nil,
err: storagedriver.InvalidPathError{},
},
{
pathMatch: fmt.Sprintf("%s/_manifests/tags", repositoryWithManifestBadLink),
content: []byte("this is a bad sha"),
err: nil,
},
{
pathMatch: fmt.Sprintf("%s/_manifests/tags", repositoryWithGenericStorageError),
content: nil,
err: errGenericStorage,
},
},
}, nil
}
type mockErrorMapping struct {
pathMatch string
content []byte
err error
}
// mockErrorDriver implements StorageDriver to force storage error on manifest request
type mockErrorDriver struct {
storagedriver.StorageDriver
returnErrs []mockErrorMapping
}
func (dr *mockErrorDriver) GetContent(ctx context.Context, path string) ([]byte, error) {
for _, returns := range dr.returnErrs {
if strings.Contains(path, returns.pathMatch) {
return returns.content, returns.err
}
}
return nil, errors.New("Unknown storage error")
}
func TestGetManifestWithStorageError(t *testing.T) {
factory.Register("storagemanifesterror", &storageManifestErrDriverFactory{})
config := configuration.Configuration{
Storage: configuration.Storage{
"storagemanifesterror": configuration.Parameters{},
"maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{
"enabled": false,
}},
},
}
config.HTTP.Headers = headerConfig
env1 := newTestEnvWithConfig(t, &config)
defer env1.Shutdown()
repo, _ := reference.WithName(repositoryWithManifestNotFound)
testManifestWithStorageError(t, env1, repo, http.StatusNotFound, v2.ErrorCodeManifestUnknown)
repo, _ = reference.WithName(repositoryWithGenericStorageError)
testManifestWithStorageError(t, env1, repo, http.StatusInternalServerError, errcode.ErrorCodeUnknown)
repo, _ = reference.WithName(repositoryWithManifestInvalidPath)
testManifestWithStorageError(t, env1, repo, http.StatusInternalServerError, errcode.ErrorCodeUnknown)
repo, _ = reference.WithName(repositoryWithManifestBadLink)
testManifestWithStorageError(t, env1, repo, http.StatusInternalServerError, errcode.ErrorCodeUnknown)
}
func TestManifestDelete(t *testing.T) { func TestManifestDelete(t *testing.T) {
schema1Repo, _ := reference.ParseNamed("foo/schema1") schema1Repo, _ := reference.WithName("foo/schema1")
schema2Repo, _ := reference.ParseNamed("foo/schema2") schema2Repo, _ := reference.WithName("foo/schema2")
deleteEnabled := true deleteEnabled := true
env := newTestEnv(t, deleteEnabled) env := newTestEnv(t, deleteEnabled)
@@ -827,7 +919,7 @@ func TestManifestDelete(t *testing.T) {
} }
func TestManifestDeleteDisabled(t *testing.T) { func TestManifestDeleteDisabled(t *testing.T) {
schema1Repo, _ := reference.ParseNamed("foo/schema1") schema1Repo, _ := reference.WithName("foo/schema1")
deleteEnabled := false deleteEnabled := false
env := newTestEnv(t, deleteEnabled) env := newTestEnv(t, deleteEnabled)
defer env.Shutdown() defer env.Shutdown()
@@ -835,7 +927,7 @@ func TestManifestDeleteDisabled(t *testing.T) {
} }
func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.Named) { func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.Named) {
ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) ref, _ := reference.WithDigest(imageName, digestSha256EmptyTar)
manifestURL, err := env.builder.BuildManifestURL(ref) manifestURL, err := env.builder.BuildManifestURL(ref)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err) t.Fatalf("unexpected error getting manifest url: %v", err)
@@ -850,6 +942,26 @@ func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.
checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed) checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed)
} }
func testManifestWithStorageError(t *testing.T, env *testEnv, imageName reference.Named, expectedStatusCode int, expectedErrorCode errcode.ErrorCode) {
tag := "latest"
tagRef, _ := reference.WithTag(imageName, tag)
manifestURL, err := env.builder.BuildManifestURL(tagRef)
if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err)
}
// -----------------------------
// Attempt to fetch the manifest
resp, err := http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error getting manifest: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "getting non-existent manifest", resp, expectedStatusCode)
checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, expectedErrorCode)
return
}
func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs {
tag := "thetag" tag := "thetag"
args := manifestArgs{imageName: imageName} args := manifestArgs{imageName: imageName}
@@ -1218,7 +1330,7 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Name
Config: distribution.Descriptor{ Config: distribution.Descriptor{
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 3253, Size: 3253,
MediaType: schema2.MediaTypeConfig, MediaType: schema2.MediaTypeImageConfig,
}, },
Layers: []distribution.Descriptor{ Layers: []distribution.Descriptor{
{ {
@@ -2075,7 +2187,7 @@ func doPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst dig
// pushLayer pushes the layer content returning the url on success. // pushLayer pushes the layer content returning the url on success.
func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string { func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string {
digester := digest.Canonical.New() digester := digest.Canonical.Digester()
resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash())) resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash()))
if err != nil { if err != nil {
@@ -2142,7 +2254,7 @@ func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Resp
uploadURL := u.String() uploadURL := u.String()
digester := digest.Canonical.New() digester := digest.Canonical.Digester()
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash())) req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash()))
if err != nil { if err != nil {
@@ -2291,7 +2403,7 @@ func checkErr(t *testing.T, err error, msg string) {
} }
func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest { func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest {
imageNameRef, err := reference.ParseNamed(imageName) imageNameRef, err := reference.WithName(imageName)
if err != nil { if err != nil {
t.Fatalf("unable to parse reference: %v", err) t.Fatalf("unable to parse reference: %v", err)
} }
@@ -2362,7 +2474,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) {
env := newTestEnvMirror(t, deleteEnabled) env := newTestEnvMirror(t, deleteEnabled)
defer env.Shutdown() defer env.Shutdown()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
tag := "latest" tag := "latest"
tagRef, _ := reference.WithTag(imageName, tag) tagRef, _ := reference.WithTag(imageName, tag)
manifestURL, err := env.builder.BuildManifestURL(tagRef) manifestURL, err := env.builder.BuildManifestURL(tagRef)
@@ -2408,7 +2520,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) {
checkResponse(t, fmt.Sprintf("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) checkResponse(t, fmt.Sprintf("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
// Blob Delete // Blob Delete
ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) ref, _ := reference.WithDigest(imageName, digestSha256EmptyTar)
blobURL, err := env.builder.BuildBlobURL(ref) blobURL, err := env.builder.BuildBlobURL(ref)
resp, err = httpDelete(blobURL) resp, err = httpDelete(blobURL)
checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
@@ -2455,7 +2567,7 @@ func TestProxyManifestGetByTag(t *testing.T) {
} }
truthConfig.HTTP.Headers = headerConfig truthConfig.HTTP.Headers = headerConfig
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
tag := "latest" tag := "latest"
truthEnv := newTestEnvWithConfig(t, &truthConfig) truthEnv := newTestEnvWithConfig(t, &truthConfig)

View File

@@ -100,7 +100,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
return http.HandlerFunc(apiBase) return http.HandlerFunc(apiBase)
}) })
app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameManifest, manifestDispatcher)
app.register(v2.RouteNameCatalog, catalogDispatcher) app.register(v2.RouteNameCatalog, catalogDispatcher)
app.register(v2.RouteNameTags, tagsDispatcher) app.register(v2.RouteNameTags, tagsDispatcher)
app.register(v2.RouteNameBlob, blobDispatcher) app.register(v2.RouteNameBlob, blobDispatcher)
@@ -213,6 +213,10 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
options = append(options, storage.EnableRedirect) options = append(options, storage.EnableRedirect)
} }
if !config.Validation.Enabled {
config.Validation.Enabled = !config.Validation.Disabled
}
// configure validation // configure validation
if config.Validation.Enabled { if config.Validation.Enabled {
if len(config.Validation.Manifests.URLs.Allow) == 0 && len(config.Validation.Manifests.URLs.Deny) == 0 { if len(config.Validation.Manifests.URLs.Allow) == 0 && len(config.Validation.Manifests.URLs.Deny) == 0 {
@@ -592,24 +596,19 @@ func (app *App) configureSecret(configuration *configuration.Configuration) {
func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() // ensure that request body is always closed. defer r.Body.Close() // ensure that request body is always closed.
// Instantiate an http context here so we can track the error codes // Prepare the context with our own little decorations.
// returned by the request router. ctx := r.Context()
ctx := defaultContextManager.context(app, w, r) ctx = ctxu.WithRequest(ctx, r)
ctx, w = ctxu.WithResponseWriter(ctx, w)
ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx))
r = r.WithContext(ctx)
defer func() { defer func() {
status, ok := ctx.Value("http.response.status").(int) status, ok := ctx.Value("http.response.status").(int)
if ok && status >= 200 && status <= 399 { if ok && status >= 200 && status <= 399 {
ctxu.GetResponseLogger(ctx).Infof("response completed") ctxu.GetResponseLogger(r.Context()).Infof("response completed")
} }
}() }()
defer defaultContextManager.release(ctx)
// NOTE(stevvooe): Total hack to get instrumented responsewriter from context.
var err error
w, err = ctxu.GetResponseWriter(ctx)
if err != nil {
ctxu.GetLogger(ctx).Warnf("response writer not found in context")
}
// Set a header with the Docker Distribution API Version for all responses. // Set a header with the Docker Distribution API Version for all responses.
w.Header().Add("Docker-Distribution-API-Version", "registry/2.0") w.Header().Add("Docker-Distribution-API-Version", "registry/2.0")
@@ -645,8 +644,11 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
// Add username to request logging // Add username to request logging
context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, auth.UserNameKey)) context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, auth.UserNameKey))
// sync up context on the request.
r = r.WithContext(context)
if app.nameRequired(r) { if app.nameRequired(r) {
nameRef, err := reference.ParseNamed(getName(context)) nameRef, err := reference.WithName(getName(context))
if err != nil { if err != nil {
ctxu.GetLogger(context).Errorf("error parsing reference from context: %v", err) ctxu.GetLogger(context).Errorf("error parsing reference from context: %v", err)
context.Errors = append(context.Errors, distribution.ErrRepositoryNameInvalid{ context.Errors = append(context.Errors, distribution.ErrRepositoryNameInvalid{
@@ -752,7 +754,7 @@ func (app *App) logError(context context.Context, errors errcode.Errors) {
// context constructs the context object for the application. This only be // context constructs the context object for the application. This only be
// called once per request. // called once per request.
func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { func (app *App) context(w http.ResponseWriter, r *http.Request) *Context {
ctx := defaultContextManager.context(app, w, r) ctx := r.Context()
ctx = ctxu.WithVars(ctx, r) ctx = ctxu.WithVars(ctx, r)
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx,
"vars.name", "vars.name",
@@ -857,8 +859,11 @@ func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listene
// nameRequired returns true if the route requires a name. // nameRequired returns true if the route requires a name.
func (app *App) nameRequired(r *http.Request) bool { func (app *App) nameRequired(r *http.Request) bool {
route := mux.CurrentRoute(r) route := mux.CurrentRoute(r)
if route == nil {
return true
}
routeName := route.GetName() routeName := route.GetName()
return route == nil || (routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog) return routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog
} }
// apiBase implements a simple yes-man for doing overall checks against the // apiBase implements a simple yes-man for doing overall checks against the
@@ -897,12 +902,10 @@ func appendAccessRecords(records []auth.Access, method string, repo string) []au
Action: "push", Action: "push",
}) })
case "DELETE": case "DELETE":
// DELETE access requires full admin rights, which is represented
// as "*". This may not be ideal.
records = append(records, records = append(records,
auth.Access{ auth.Access{
Resource: resource, Resource: resource,
Action: "*", Action: "delete",
}) })
} }
return records return records

View File

@@ -229,9 +229,9 @@ func TestAppendAccessRecords(t *testing.T) {
Resource: expectedResource, Resource: expectedResource,
Action: "push", Action: "push",
} }
expectedAllRecord := auth.Access{ expectedDeleteRecord := auth.Access{
Resource: expectedResource, Resource: expectedResource,
Action: "*", Action: "delete",
} }
records := []auth.Access{} records := []auth.Access{}
@@ -271,7 +271,7 @@ func TestAppendAccessRecords(t *testing.T) {
records = []auth.Access{} records = []auth.Access{}
result = appendAccessRecords(records, "DELETE", repo) result = appendAccessRecords(records, "DELETE", repo)
expectedResult = []auth.Access{expectedAllRecord} expectedResult = []auth.Access{expectedDeleteRecord}
if ok := reflect.DeepEqual(result, expectedResult); !ok { if ok := reflect.DeepEqual(result, expectedResult); !ok {
t.Fatalf("Actual access record differs from expected") t.Fatalf("Actual access record differs from expected")
} }

View File

@@ -5,10 +5,10 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
) )
// blobDispatcher uses the request context to build a blobHandler. // blobDispatcher uses the request context to build a blobHandler.

View File

@@ -7,12 +7,12 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
) )
// blobUploadDispatcher constructs and returns the blob upload handler for the // blobUploadDispatcher constructs and returns the blob upload handler for the
@@ -211,7 +211,7 @@ func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *ht
return return
} }
dgst, err := digest.ParseDigest(dgstStr) dgst, err := digest.Parse(dgstStr)
if err != nil { if err != nil {
// no digest? return error, but allow retry. // no digest? return error, but allow retry.
buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed")) buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed"))
@@ -329,12 +329,12 @@ func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.
// successful, the blob is linked into the blob store and 201 Created is // successful, the blob is linked into the blob store and 201 Created is
// returned with the canonical url of the blob. // returned with the canonical url of the blob.
func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) { func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) {
dgst, err := digest.ParseDigest(mountDigest) dgst, err := digest.Parse(mountDigest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ref, err := reference.ParseNamed(fromRepo) ref, err := reference.WithName(fromRepo)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,14 +3,13 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"sync"
"github.com/docker/distribution" "github.com/docker/distribution"
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth" "github.com/docker/distribution/registry/auth"
"github.com/opencontainers/go-digest"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@@ -62,7 +61,7 @@ func getDigest(ctx context.Context) (dgst digest.Digest, err error) {
return "", errDigestNotAvailable return "", errDigestNotAvailable
} }
d, err := digest.ParseDigest(dgstStr) d, err := digest.Parse(dgstStr)
if err != nil { if err != nil {
ctxu.GetLogger(ctx).Errorf("error parsing digest=%q: %v", dgstStr, err) ctxu.GetLogger(ctx).Errorf("error parsing digest=%q: %v", dgstStr, err)
return "", err return "", err
@@ -91,62 +90,3 @@ func getUserName(ctx context.Context, r *http.Request) string {
return username return username
} }
// contextManager allows us to associate net/context.Context instances with a
// request, based on the memory identity of http.Request. This prepares http-
// level context, which is not application specific. If this is called,
// (*contextManager).release must be called on the context when the request is
// completed.
//
// Providing this circumvents a lot of necessity for dispatchers with the
// benefit of instantiating the request context much earlier.
//
// TODO(stevvooe): Consider making this facility a part of the context package.
type contextManager struct {
contexts map[*http.Request]context.Context
mu sync.Mutex
}
// defaultContextManager is just a global instance to register request contexts.
var defaultContextManager = newContextManager()
func newContextManager() *contextManager {
return &contextManager{
contexts: make(map[*http.Request]context.Context),
}
}
// context either returns a new context or looks it up in the manager.
func (cm *contextManager) context(parent context.Context, w http.ResponseWriter, r *http.Request) context.Context {
cm.mu.Lock()
defer cm.mu.Unlock()
ctx, ok := cm.contexts[r]
if ok {
return ctx
}
if parent == nil {
parent = ctxu.Background()
}
ctx = ctxu.WithRequest(parent, r)
ctx, w = ctxu.WithResponseWriter(ctx, w)
ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx))
cm.contexts[r] = ctx
return ctx
}
// releases frees any associated with resources from request.
func (cm *contextManager) release(ctx context.Context) {
cm.mu.Lock()
defer cm.mu.Unlock()
r, err := ctxu.GetRequest(ctx)
if err != nil {
ctxu.GetLogger(ctx).Errorf("no request found in context during release")
return
}
delete(cm.contexts, r)
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
@@ -17,6 +16,7 @@ import (
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth" "github.com/docker/distribution/registry/auth"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
) )
// These constants determine which architecture and OS to choose from a // These constants determine which architecture and OS to choose from a
@@ -26,36 +26,36 @@ const (
defaultOS = "linux" defaultOS = "linux"
) )
// imageManifestDispatcher takes the request context and builds the // manifestDispatcher takes the request context and builds the
// appropriate handler for handling image manifest requests. // appropriate handler for handling manifest requests.
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { func manifestDispatcher(ctx *Context, r *http.Request) http.Handler {
imageManifestHandler := &imageManifestHandler{ manifestHandler := &manifestHandler{
Context: ctx, Context: ctx,
} }
reference := getReference(ctx) reference := getReference(ctx)
dgst, err := digest.ParseDigest(reference) dgst, err := digest.Parse(reference)
if err != nil { if err != nil {
// We just have a tag // We just have a tag
imageManifestHandler.Tag = reference manifestHandler.Tag = reference
} else { } else {
imageManifestHandler.Digest = dgst manifestHandler.Digest = dgst
} }
mhandler := handlers.MethodHandler{ mhandler := handlers.MethodHandler{
"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), "GET": http.HandlerFunc(manifestHandler.GetManifest),
"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest), "HEAD": http.HandlerFunc(manifestHandler.GetManifest),
} }
if !ctx.readOnly { if !ctx.readOnly {
mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest) mhandler["PUT"] = http.HandlerFunc(manifestHandler.PutManifest)
mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest) mhandler["DELETE"] = http.HandlerFunc(manifestHandler.DeleteManifest)
} }
return mhandler return mhandler
} }
// imageManifestHandler handles http operations on image manifests. // manifestHandler handles http operations on image manifests.
type imageManifestHandler struct { type manifestHandler struct {
*Context *Context
// One of tag or digest gets set, depending on what is present in context. // One of tag or digest gets set, depending on what is present in context.
@@ -63,8 +63,8 @@ type imageManifestHandler struct {
Digest digest.Digest Digest digest.Digest
} }
// GetImageManifest fetches the image manifest from the storage backend, if it exists. // GetManifest fetches the image manifest from the storage backend, if it exists.
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(imh).Debug("GetImageManifest") ctxu.GetLogger(imh).Debug("GetImageManifest")
manifests, err := imh.Repository.Manifests(imh) manifests, err := imh.Repository.Manifests(imh)
if err != nil { if err != nil {
@@ -77,7 +77,11 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
tags := imh.Repository.Tags(imh) tags := imh.Repository.Tags(imh)
desc, err := tags.Get(imh, imh.Tag) desc, err := tags.Get(imh, imh.Tag)
if err != nil { if err != nil {
if _, ok := err.(distribution.ErrTagUnknown); ok {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
} else {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
return return
} }
imh.Digest = desc.Digest imh.Digest = desc.Digest
@@ -94,7 +98,11 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
} }
manifest, err = manifests.Get(imh, imh.Digest, options...) manifest, err = manifests.Get(imh, imh.Digest, options...)
if err != nil { if err != nil {
if _, ok := err.(distribution.ErrManifestUnknownRevision); ok {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
} else {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
return return
} }
@@ -161,7 +169,11 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
manifest, err = manifests.Get(imh, manifestDigest) manifest, err = manifests.Get(imh, manifestDigest)
if err != nil { if err != nil {
if _, ok := err.(distribution.ErrManifestUnknownRevision); ok {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
} else {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
return return
} }
@@ -171,6 +183,8 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
if err != nil { if err != nil {
return return
} }
} else {
imh.Digest = manifestDigest
} }
} }
@@ -186,12 +200,16 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
w.Write(p) w.Write(p)
} }
func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { func (imh *manifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
targetDescriptor := schema2Manifest.Target() targetDescriptor := schema2Manifest.Target()
blobs := imh.Repository.Blobs(imh) blobs := imh.Repository.Blobs(imh)
configJSON, err := blobs.Get(imh, targetDescriptor.Digest) configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
if err != nil { if err != nil {
if err == distribution.ErrBlobUnknown {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
} else {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
return nil, err return nil, err
} }
@@ -231,8 +249,8 @@ func etagMatch(r *http.Request, etag string) bool {
return false return false
} }
// PutImageManifest validates and stores an image in the registry. // PutManifest validates and stores a manifest in the registry.
func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(imh).Debug("PutImageManifest") ctxu.GetLogger(imh).Debug("PutImageManifest")
manifests, err := imh.Repository.Manifests(imh) manifests, err := imh.Repository.Manifests(imh)
if err != nil { if err != nil {
@@ -348,7 +366,7 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
// applyResourcePolicy checks whether the resource class matches what has // applyResourcePolicy checks whether the resource class matches what has
// been authorized and allowed by the policy configuration. // been authorized and allowed by the policy configuration.
func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manifest) error { func (imh *manifestHandler) applyResourcePolicy(manifest distribution.Manifest) error {
allowedClasses := imh.App.Config.Policy.Repository.Classes allowedClasses := imh.App.Config.Policy.Repository.Classes
if len(allowedClasses) == 0 { if len(allowedClasses) == 0 {
return nil return nil
@@ -360,7 +378,7 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
class = "image" class = "image"
case *schema2.DeserializedManifest: case *schema2.DeserializedManifest:
switch m.Config.MediaType { switch m.Config.MediaType {
case schema2.MediaTypeConfig: case schema2.MediaTypeImageConfig:
class = "image" class = "image"
case schema2.MediaTypePluginConfig: case schema2.MediaTypePluginConfig:
class = "plugin" class = "plugin"
@@ -413,8 +431,8 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
} }
// DeleteImageManifest removes the manifest with the given digest from the registry. // DeleteManifest removes the manifest with the given digest from the registry.
func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(imh).Debug("DeleteImageManifest") ctxu.GetLogger(imh).Debug("DeleteImageManifest")
manifests, err := imh.Repository.Manifests(imh) manifests, err := imh.Repository.Manifests(imh)

View File

@@ -9,9 +9,9 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/proxy/scheduler"
"github.com/opencontainers/go-digest"
) )
// todo(richardscothern): from cache control header or config file // todo(richardscothern): from cache control header or config file

View File

@@ -11,13 +11,13 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/proxy/scheduler"
"github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage"
"github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/cache/memory"
"github.com/docker/distribution/registry/storage/driver/filesystem" "github.com/docker/distribution/registry/storage/driver/filesystem"
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/opencontainers/go-digest"
) )
var sbsMu sync.Mutex var sbsMu sync.Mutex
@@ -115,7 +115,7 @@ func (te *testEnv) RemoteStats() *map[string]int {
// Populate remote store and record the digests // Populate remote store and record the digests
func makeTestEnv(t *testing.T, name string) *testEnv { func makeTestEnv(t *testing.T, name string) *testEnv {
nameRef, err := reference.ParseNamed(name) nameRef, err := reference.WithName(name)
if err != nil { if err != nil {
t.Fatalf("unable to parse reference: %s", err) t.Fatalf("unable to parse reference: %s", err)
} }

View File

@@ -5,9 +5,9 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/proxy/scheduler"
"github.com/opencontainers/go-digest"
) )
// todo(richardscothern): from cache control header or config // todo(richardscothern): from cache control header or config

View File

@@ -7,7 +7,6 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@@ -19,6 +18,7 @@ import (
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
) )
type statsManifest struct { type statsManifest struct {
@@ -83,7 +83,7 @@ func (m *mockChallenger) challengeManager() challenge.Manager {
} }
func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv {
nameRef, err := reference.ParseNamed(name) nameRef, err := reference.WithName(name)
if err != nil { if err != nil {
t.Fatalf("unable to parse reference: %s", err) t.Fatalf("unable to parse reference: %s", err)
} }

View File

@@ -12,7 +12,7 @@ import (
"rsc.io/letsencrypt" "rsc.io/letsencrypt"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/formatters/logstash" logstash "github.com/bshuster-repo/logrus-logstash-hook"
"github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"

View File

@@ -13,18 +13,18 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/cache/memory"
"github.com/docker/distribution/registry/storage/driver/testdriver" "github.com/docker/distribution/registry/storage/driver/testdriver"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/opencontainers/go-digest"
) )
// TestWriteSeek tests that the current file size can be // TestWriteSeek tests that the current file size can be
// obtained using Seek // obtained using Seek
func TestWriteSeek(t *testing.T) { func TestWriteSeek(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
driver := testdriver.New() driver := testdriver.New()
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
if err != nil { if err != nil {
@@ -60,7 +60,7 @@ func TestSimpleBlobUpload(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
driver := testdriver.New() driver := testdriver.New()
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
if err != nil { if err != nil {
@@ -255,7 +255,7 @@ func TestSimpleBlobUpload(t *testing.T) {
// other tests. // other tests.
func TestSimpleBlobRead(t *testing.T) { func TestSimpleBlobRead(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
driver := testdriver.New() driver := testdriver.New()
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
if err != nil { if err != nil {
@@ -366,8 +366,8 @@ func TestBlobMount(t *testing.T) {
} }
ctx := context.Background() ctx := context.Background()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
sourceImageName, _ := reference.ParseNamed("foo/source") sourceImageName, _ := reference.WithName("foo/source")
driver := testdriver.New() driver := testdriver.New()
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
if err != nil { if err != nil {
@@ -518,7 +518,7 @@ func TestBlobMount(t *testing.T) {
// TestLayerUploadZeroLength uploads zero-length // TestLayerUploadZeroLength uploads zero-length
func TestLayerUploadZeroLength(t *testing.T) { func TestLayerUploadZeroLength(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName, _ := reference.ParseNamed("foo/bar") imageName, _ := reference.WithName("foo/bar")
driver := testdriver.New() driver := testdriver.New()
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
if err != nil { if err != nil {
@@ -530,7 +530,7 @@ func TestLayerUploadZeroLength(t *testing.T) {
} }
bs := repository.Blobs(ctx) bs := repository.Blobs(ctx)
simpleUpload(t, bs, []byte{}, digest.DigestSha256EmptyTar) simpleUpload(t, bs, []byte{}, digestSha256Empty)
} }
func simpleUpload(t *testing.T, bs distribution.BlobIngester, blob []byte, expectedDigest digest.Digest) { func simpleUpload(t *testing.T, bs distribution.BlobIngester, blob []byte, expectedDigest digest.Digest) {

View File

@@ -7,8 +7,8 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver"
"github.com/opencontainers/go-digest"
) )
// TODO(stevvooe): This should configurable in the future. // TODO(stevvooe): This should configurable in the future.

View File

@@ -5,8 +5,8 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver"
"github.com/opencontainers/go-digest"
) )
// blobStore implements the read side of the blob store interface over a // blobStore implements the read side of the blob store interface over a
@@ -145,7 +145,7 @@ func (bs *blobStore) readlink(ctx context.Context, path string) (digest.Digest,
return "", err return "", err
} }
linked, err := digest.ParseDigest(string(content)) linked, err := digest.Parse(string(content))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -10,14 +10,19 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
storagedriver "github.com/docker/distribution/registry/storage/driver" storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/opencontainers/go-digest"
) )
var ( var (
errResumableDigestNotAvailable = errors.New("resumable digest not available") errResumableDigestNotAvailable = errors.New("resumable digest not available")
) )
const (
// digestSha256Empty is the canonical sha256 digest of empty data
digestSha256Empty = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
// blobWriter is used to control the various aspects of resumable // blobWriter is used to control the various aspects of resumable
// blob upload. // blob upload.
type blobWriter struct { type blobWriter struct {
@@ -234,12 +239,8 @@ func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descri
// paths. We may be able to make the size-based check a stronger // paths. We may be able to make the size-based check a stronger
// guarantee, so this may be defensive. // guarantee, so this may be defensive.
if !verified { if !verified {
digester := digest.Canonical.New() digester := digest.Canonical.Digester()
verifier := desc.Digest.Verifier()
digestVerifier, err := digest.NewDigestVerifier(desc.Digest)
if err != nil {
return distribution.Descriptor{}, err
}
// Read the file from the backend driver and validate it. // Read the file from the backend driver and validate it.
fr, err := newFileReader(ctx, bw.driver, bw.path, desc.Size) fr, err := newFileReader(ctx, bw.driver, bw.path, desc.Size)
@@ -250,12 +251,12 @@ func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descri
tr := io.TeeReader(fr, digester.Hash()) tr := io.TeeReader(fr, digester.Hash())
if _, err := io.Copy(digestVerifier, tr); err != nil { if _, err := io.Copy(verifier, tr); err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err
} }
canonical = digester.Digest() canonical = digester.Digest()
verified = digestVerifier.Verified() verified = verifier.Verified()
} }
} }
@@ -313,7 +314,7 @@ func (bw *blobWriter) moveBlob(ctx context.Context, desc distribution.Descriptor
// If no data was received, we may not actually have a file on disk. Check // If no data was received, we may not actually have a file on disk. Check
// the size here and write a zero-length file to blobPath if this is the // the size here and write a zero-length file to blobPath if this is the
// case. For the most part, this should only ever happen with zero-length // case. For the most part, this should only ever happen with zero-length
// tars. // blobs.
if _, err := bw.blobStore.driver.Stat(ctx, bw.path); err != nil { if _, err := bw.blobStore.driver.Stat(ctx, bw.path); err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError: case storagedriver.PathNotFoundError:
@@ -321,8 +322,8 @@ func (bw *blobWriter) moveBlob(ctx context.Context, desc distribution.Descriptor
// get a hash, then the underlying file is deleted, we risk moving // get a hash, then the underlying file is deleted, we risk moving
// a zero-length blob into a nonzero-length blob location. To // a zero-length blob into a nonzero-length blob location. To
// prevent this horrid thing, we employ the hack of only allowing // prevent this horrid thing, we employ the hack of only allowing
// to this happen for the digest of an empty tar. // to this happen for the digest of an empty blob.
if desc.Digest == digest.DigestSha256EmptyTar { if desc.Digest == digestSha256Empty {
return bw.blobStore.driver.PutContent(ctx, blobPath, []byte{}) return bw.blobStore.driver.PutContent(ctx, blobPath, []byte{})
} }

View File

@@ -6,8 +6,8 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache"
"github.com/opencontainers/go-digest"
) )
// CheckBlobDescriptorCache takes a cache implementation through a common set // CheckBlobDescriptorCache takes a cache implementation through a common set

View File

@@ -2,7 +2,7 @@ package cache
import ( import (
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/opencontainers/go-digest"
"github.com/docker/distribution" "github.com/docker/distribution"
) )

Some files were not shown because too many files have changed in this diff Show More