Compare commits

...

179 commits

Author SHA1 Message Date
Arko Dasgupta
f5cdc24dd3
Merge pull request #3245 from olegburov/reopen-pr-2973
Reopen PR #2973 (Support ECS TaskRole in S3 storage driver).
2020-10-28 17:30:56 -07:00
olegburov
34f1322664 Fix hardcoded credential provides.
Signed-off-by: olegburov <oleg.burov@outlook.com>
2020-09-02 17:32:34 -07:00
Manish Tomar
2800ab0224
Merge pull request #2973 from redmatter/support-ecs-instance-profile-in-s3-driver
Support ECS TaskRole in S3 storage driver
2020-08-26 15:24:20 -07:00
Manish Tomar
5dc1f65acc
Merge pull request #3224 from AndreasHassing/fix/bad-pointer-windows-EnableVirtualTerminalProcessing
Update logrus to v1.6.0, fixes #3223
2020-08-24 15:04:52 -07:00
Manish Tomar
fe1ffa86bc
Merge pull request #3226 from thaJeztah/bump_go_digest
vendor: opencontainers/go-digest v1.0.0
2020-08-24 10:50:21 -07:00
Sebastiaan van Stijn
8a8d91529d
vendor: opencontainers/go-digest v1.0.0
full diff: https://github.com/opencontainers/go-digest/compare/v1.0.0-rc1...v1.0.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-08-24 18:46:53 +02:00
Andreas Hassing
9466dd4e5a Update logrus to v1.6.0
Fixes #3223 by bumping logrus to v1.6.0, which in turn bumps
github.com/konsorten/go-windows-terminal-sequences to v1.0.3
wherein the fix to bad pointer is found.

Signed-off-by: Andreas Hassing <andreas@famhassing.dk>
2020-08-24 13:14:04 +02:00
Manish Tomar
0316f34bf2
Merge pull request #3118 from lawliet89/bump-aws-sdk
Bump AWS SDK to v1.34.9
2020-08-21 15:57:08 -07:00
Yong Wen Chua
e1464fd317
Bump AWS SDK
Fixes https://github.com/docker/distribution/issues/3097

Signed-off-by: Yong Wen Chua <lawliet89@users.noreply.github.com>
2020-08-21 17:35:24 +08:00
Derek McGowan
35b26def43
Merge pull request #3173 from adamwg/catalog-no-layers-upstream
catalog: List repositories with no unique layers
2020-08-07 23:12:18 -07:00
Adam Wolfe Gordon
a784441b62 catalog: List repositories with no unique layers
A repository need not contain any unique layers, if its images use only layers
mounted from other repositories. But, the catalog endpoint was looking for the
_layers directory to indicate that a directory was a repository.

Use the _manifests directory as the marker instead, since any repository with
revisions will contain a _manifests directory.

Signed-off-by: Adam Wolfe Gordon <awg@digitalocean.com>
2020-08-04 11:38:49 -06:00
Manish Tomar
1d0ea8ed7b
Merge pull request #3140 from linux-on-ibm-z/travis-s390x
Add s390x support for travis
2020-07-30 16:15:55 -07:00
Derek McGowan
208d68bd5c
Merge pull request #3209 from zhipengzuo/master
clean up code because err is always nil
2020-07-22 09:18:06 -07:00
zhipengzuo
f361d443b7 clean up code because err is always nil
Signed-off-by: zhipengzuo <zuozhipeng@baidu.com>
2020-07-22 10:54:46 +08:00
Derek McGowan
53e18a9d9b
Merge pull request #3196 from thaJeztah/add_redirect
docs: add redirect for old URL
2020-07-08 16:08:24 -07:00
Sebastiaan van Stijn
7728c5e445
docs: add redirect for old URL
Looks like there's some projects refering to this old URL:
https://grep.app/search?q=https%3A//docs.docker.com/reference/api/registry_api/

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-07-08 12:18:31 +02:00
Andrew Bulford
9690d843fa Support ECS TaskRole in S3 storage driver
Instead of constructing the list of credential providers manually, if we
use the default list we can take advantage of the AWS SDK checking the
environment and returning either the EC2RoleProvider or the generic HTTP
credentials provider, configured to use the ECS credentials endpoint.

Also, use the `defaults.Config()` function instead of `aws.NewConfig()`,
as this results in an initialised HTTP client which prevents a fatal
error when retrieving credentials from the ECS credentials endpoint.

Fixes #2960

Signed-off-by: Andrew Bulford <andrew.bulford@redmatter.com>
2020-07-01 08:42:56 +01:00
Sebastiaan van Stijn
62d0fd45e7
Merge pull request #3187 from bloodorangeio/fix-ci
Fix CI failures, upgrade to Go 1.14+
2020-06-29 19:14:34 +02:00
jdolitsky
81ba770eff Fix CI failures, upgrade to Go 1.14+
- Modify Travis manifest to use Go 1.14.x
- Hardcode version of golangci-lint in setup script
- Hardcode version of go-md2man in setup script

Signed-off-by: jdolitsky <393494+jdolitsky@users.noreply.github.com>
2020-06-26 10:59:50 -05:00
srajmane
a2ed1b5e80 Added dist: bionic, updated go version to 1.14.x and set GO111MODULE=on
Signed-off-by: Snehal Rajmane <srajmane@us.ibm.com>
2020-04-29 03:25:24 -07:00
srajmane
55f88c35b9 Adding s390x support.
Signed-off-by: Snehal Rajmane <srajmane@us.ibm.com>
2020-04-07 07:59:19 -07:00
Derek McGowan
742aab907b
Merge pull request #3127 from dmage/err-shadow
Fix err shadowing in gcs driver
2020-03-19 10:36:57 -07:00
Derek McGowan
17a394f9af
Merge pull request #3128 from dmcgowan/gosimple-fix
Fix gosimple checks
2020-03-18 16:06:17 -07:00
Derek McGowan
78c2ab6646
Fix gosimple checks
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-18 15:52:34 -07:00
Oleg Bulatov
cdb4ba947a
Fix err shadowing in gcs driver
Signed-off-by: Oleg Bulatov <oleg@bulatov.me>
2020-03-18 11:41:34 +01:00
Derek McGowan
303f1899bb
Merge pull request #3121 from wy65701436/upgrade-oci-library
Update oci library
2020-03-10 14:58:43 -07:00
wang yan
bf56f348be Update oci library
1, update go-digest to v1.0.0-rc1
2, update image-spec to v1.0.1

Signed-off-by: wang yan <wangyan@vmware.com>
2020-03-10 23:19:29 +08:00
Derek McGowan
581be91482
Merge pull request #3113 from dmcgowan/upstream-redis-fixes
Redis cache fixes and metrics
2020-03-09 13:36:53 -07:00
Derek McGowan
be29c05a1e
Remove deprecated cache metrics code
The metrics tracker in cached blob statter was replaced with prometheus
metrics and no longer needed.
Remove unused log wrapping.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-09 13:11:54 -07:00
Derek McGowan
495a4af7cf
Fix goimports
Separate fix for cherry-picked code

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 17:51:37 -08:00
Grant Watters
74d442a058
Consider redis.ErrNil as distribution.ErrBlobUnknown for Stat HGET
* Update redis.go

If the dgst key does not exist in the cache when calling HGET, `redis.String` will return an `ErrNil` which we need to translate into `distribution.ErrBlobUnknown` so that the error being returned can be properly handled. This will ensure that `SetDescriptor` is properly called from `cachedBlobStatter::Stat` for `repositoryScopedRedisBlobDescriptorService` which will update the redis cache and be considered as a Miss rather than an Error.

cc @manishtomar

* Update suite.go

Add unit test to ensure missing blobs for scoped repo properly return ErrBlobUnknown when HGET returns redis.ErrNil.

(cherry picked from commit dca6b9526a1d30dd218a9f321c4f84ecc4b5e62e)
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 17:51:37 -08:00
Manish Tomar
795892662b
redis metrics
* redis metrics

it is working but metrics are not very useful since default buckets
start from 5ms and almost all of them are in that range.

* remove extra comment

(cherry picked from commit ba1a1d74e7eb047dd1056548ccf0695e8846782c)
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 17:51:37 -08:00
Manish Tomar
ce101280fe
fix redis caching issue
* fix redis caching issue

earlier redis cache was updated when there was any error including any
temporary connectivity issue. This would trigger set calls which would
further increase load and possibly connectivity errors from redis
leaving the system with continuous errors and high latency. Now the
cache is updated only when it is genuine cache miss. Other errors do not
trigger a cache update.

* add back tracker Hit() and Miss() calls

*squashed commits*
(cherry picked from commit 6f3e1c10260ef59ba4e9c42e939329fad9fdd8c3)
(cherry picked from commit 6738ff3320cf82cc2df919a95a1bde2f7789a501)

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 17:51:37 -08:00
Derek McGowan
4c7c63b557
Add cache unit test
Test base functionality of the cache statter

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 17:51:37 -08:00
Derek McGowan
492a10376c
Merge pull request #3111 from dmcgowan/update-report-issues
Update reporting issues guidelines
2020-03-04 14:39:19 -08:00
Derek McGowan
c79aa81772
Merge pull request #2550 from halcyonCorsair/halcyonCorsair/use-go-events-1
Use go-events package
2020-03-04 14:38:15 -08:00
Elliot Pahl
800cb95821
Use go-events package
TBD: Queue not converted yet

Signed-off-by: Elliot Pahl <elliot.pahl@gmail.com>
2020-03-04 12:49:32 -08:00
Derek McGowan
2d1126ecc1
Merge pull request #3080 from thaJeztah/bump_go_metrics
vendor: update docker/go-metrics v0.0.1
2020-03-04 10:21:48 -08:00
Derek McGowan
3b4d1e3ddf
Merge pull request #3110 from dmcgowan/update-maintainers
Update governance and maintainers
2020-03-04 10:19:46 -08:00
Derek McGowan
1e25ecefe4
Update governance and maintainers
Update format of governance and bring in language from containerd.
Update maintainers to reflect active maintainers.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-04 10:04:38 -08:00
Derek McGowan
db3c418ada
Update reporting issues guidelines
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-03-03 16:56:46 -08:00
Sebastiaan van Stijn
98dcc5195e
vendor: update docker/go-metrics v0.0.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-03-02 20:14:15 +01:00
Derek McGowan
6b972e50fe
Merge pull request #2272 from naveedjamil/fips
Increase Unit Test Code Coverage
2020-02-22 17:40:41 -08:00
Derek McGowan
e65b3f1316
Fix CI for test updates
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2020-02-22 17:27:55 -08:00
Naveed Jamil
efdba4f210
Increase Unit Test Code Coverage
Unit test coverge was increased to cover the usages of crypto. This helps to ensure that everything is working fine with fips mode enabled.
Also updated sha1 to sha256 in registry/storage/driver/testsuites/testsuites.go because sha1 is not supported in fips mode.

Signed-off-by: Naveed Jamil <naveed.jamil@tenpearl.com>
2020-02-22 17:09:00 -08:00
Derek McGowan
e9a023180f
Merge pull request #3071 from dmcgowan/update-readme-docs
Update readme and contributing docs
2020-02-22 16:58:53 -08:00
Derek McGowan
f1cb8a56c8
Merge pull request #3105 from thaJeztah/bump_golang_1.13.8
Update Golang 1.13.8
2020-02-22 16:54:49 -08:00
Derek McGowan
bfa6b923e7
Merge pull request #3020 from kd7lxl/updatefrequency
Fix typo cloudfront updatefrenquency
2020-02-22 16:54:03 -08:00
Derek McGowan
f593b8d975
Merge pull request #2446 from legionus/docker-configuration-ptr
Fix the pointer initialization
2020-02-22 16:42:17 -08:00
Sebastiaan van Stijn
1f77c9a57f
Update Golang 1.13.8
full diff: https://github.com/golang/go/compare/go1.13.7...go1.13.8

go1.13.8 (released 2020/02/12) includes fixes to the runtime, the crypto/x509,
and net/http packages. See the Go 1.13.8 milestone on the issue tracker for details.

https://github.com/golang/go/issues?q=milestone%3AGo1.13.8+label%3ACherryPickApproved

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-22 17:59:03 +01:00
Derek McGowan
861aa2a621
Merge pull request #3089 from thaJeztah/bump_golang_1.13.7
Update Golang 1.13.7, golang.org/x/crypto (CVE-2020-0601, CVE-2020-7919)
2020-02-21 18:08:27 -08:00
Sebastiaan van Stijn
9b6a019081
prevent dev-tools from updating go.mod
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-21 23:47:14 +01:00
Sebastiaan van Stijn
7e290869e7
vendor: update golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d (CVE-2020-7919)
Includes 69ecbb4d6d
(forward-port of 8b5121be2f),
which fixes CVE-2020-7919:

- Panic in crypto/x509 certificate parsing and golang.org/x/crypto/cryptobyte
  On 32-bit architectures, a malformed input to crypto/x509 or the ASN.1 parsing
  functions of golang.org/x/crypto/cryptobyte can lead to a panic.
  The malformed certificate can be delivered via a crypto/tls connection to a
  client, or to a server that accepts client certificates. net/http clients can
  be made to crash by an HTTPS server, while net/http servers that accept client
  certificates will recover the panic and are unaffected.
  Thanks to Project Wycheproof for providing the test cases that led to the
  discovery of this issue. The issue is CVE-2020-7919 and Go issue golang.org/issue/36837.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-21 23:45:56 +01:00
Sebastiaan van Stijn
016549532f
Dockerfile: use alpine 3.11
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-21 23:45:54 +01:00
Sebastiaan van Stijn
974375f66c
re-vendor with go 1.13
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-21 23:45:52 +01:00
Sebastiaan van Stijn
4ae059c714
Update Golang 1.13.7 (CVE-2020-0601, CVE-2020-7919)
full diff: https://github.com/golang/go/compare/go1.13.4...go1.13.7

go1.13.7 (released 2020/01/28) includes two security fixes. One mitigates
the CVE-2020-0601 certificate verification bypass on Windows. The other affects
only 32-bit architectures.

https://github.com/golang/go/issues?q=milestone%3AGo1.13.7+label%3ACherryPickApproved

- X.509 certificate validation bypass on Windows 10
  A Windows vulnerability allows attackers to spoof valid certificate chains when
  the system root store is in use. These releases include a mitigation for Go
  applications, but it’s strongly recommended that affected users install the
  Windows security update to protect their system.
  This issue is CVE-2020-0601 and Go issue golang.org/issue/36834.
- Panic in crypto/x509 certificate parsing and golang.org/x/crypto/cryptobyte
  On 32-bit architectures, a malformed input to crypto/x509 or the ASN.1 parsing
  functions of golang.org/x/crypto/cryptobyte can lead to a panic.
  The malformed certificate can be delivered via a crypto/tls connection to a
  client, or to a server that accepts client certificates. net/http clients can
  be made to crash by an HTTPS server, while net/http servers that accept client
  certificates will recover the panic and are unaffected.
  Thanks to Project Wycheproof for providing the test cases that led to the
  discovery of this issue. The issue is CVE-2020-7919 and Go issue golang.org/issue/36837.
  This is also fixed in version v0.0.0-20200124225646-8b5121be2f68 of golang.org/x/crypto/cryptobyte.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-02-21 23:45:49 +01:00
Derek McGowan
d054b13dc3
Merge pull request #3086 from thaJeztah/forward_port_ISS-2819
[master] Use same env var in Dockerfile and Makefile
2020-02-21 10:24:25 -08:00
Derek McGowan
244d5246c2
Merge pull request #3023 from sayboras/feature/golangci-lint
Migrate to golangci-lint
2020-02-21 10:21:15 -08:00
sayboras
66809646d9 Migrate to golangci-lint
Signed-off-by: Tam Mach <sayboras@yahoo.com>
2020-02-14 08:11:16 +11:00
Ryan Abrams
23f6bdd743
Use same env var in Dockerfile and Makefile
Ensures that build tags get set in the Dockerfile so that OSS and GCS drivers
are built into the official registry binary.

Closes #2819

Signed-off-by: Ryan Abrams <rdabrams@gmail.com>
(cherry picked from commit bf74e4f91d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2020-01-22 10:54:02 +01:00
Derek McGowan
a837179414
Merge pull request #3072 from fermayo/fix-TestRegistryAsCacheMutationAPIs
Fix TestRegistryAsCacheMutationAPIs
2019-12-15 20:48:56 -08:00
Fernando Mayo Fernandez
6ca7b9e9fa
Fix TestRegistryAsCacheMutationAPIs
Use a synthetic upstream registry when creating the testing mirror configuration
to avoid the test fail when trying to reach http://example.com

Signed-off-by: Fernando Mayo Fernandez <fernando@undefinedlabs.com>
2019-12-15 13:51:25 +01:00
Derek McGowan
f5e84a4939
Update readme and contributing docs
Update project details to align more closely with community
updates in OCI and containerd.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2019-12-13 17:20:45 -08:00
Tom Hayward
0f5e2753a6 Fix typo cloudfront updatefrenquency
Signed-off-by: Tom Hayward <thayward@infoblox.com>
2019-12-13 12:11:42 -08:00
Derek McGowan
bdf3438b52
Merge pull request #2985 from novas0x2a/default-transport
make it possible to wrap the client transport in another one
2019-12-13 11:36:12 -08:00
Derek McGowan
29c300c106
Merge pull request #3050 from ducksecops/patch-1
bump golang to 1.13.4
2019-12-13 11:09:05 -08:00
Derek McGowan
4798651387
Merge pull request #3053 from adrian-plata/table-fix
Fixing broken table
2019-12-13 11:08:00 -08:00
Derek McGowan
f18781257e
Merge pull request #3061 from guillaumerose/reconciliate
Add pathspec for repo _layers directory and allow Repository.BlobStore to enumerate over blobs
2019-12-06 11:27:13 -08:00
Guillaume Rose
c9c3324300 Add unit tests for BlobEnumerator
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-12-06 09:38:13 +01:00
Manish Tomar
5538da4923 fixes to make layersPathSpec work
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-12-05 20:48:33 +01:00
Manish Tomar
fa7d949408 allow Repository.BlobStore to enumerate over blobs
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-12-05 20:48:33 +01:00
Manish Tomar
cf77113795 add pathspec for repo _layers directory
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-12-05 20:48:33 +01:00
Adrian Plata
b4694b0d2d Fixing broken table
Signed-off-by: Adrian Plata <adrian.plata@docker.com>
2019-11-26 17:38:40 -08:00
ducksecops
070cc010f7 bump golang to 1.13.4
Signed-off-by: Daniel Sutton <daniel@ducksecops.uk>
2019-11-13 23:15:11 +00:00
Derek McGowan
dee21c0394
Merge pull request #2991 from ad-m/patch-1
use latest version of alpine when building the Docker container
2019-10-24 15:54:08 -07:00
Ryan Abrams
ae2e973db9
Merge pull request #2748 from manishtomar/tag-digests
API to retrive tag's digests
2019-10-08 12:14:56 -07:00
Ryan Abrams
14b96e55d8
Merge pull request #3003 from laverya/fix-build-badges
use travis, not circle, build badge
2019-09-05 08:29:32 -07:00
Ryan Abrams
f656e60de5
Merge pull request #2984 from bouk/rempo
registry: Fix typo in RepositoryRemover warning
2019-09-05 08:23:18 -07:00
Ryan Abrams
740d4d1211
Merge pull request #2918 from dmathieu/test-blob-writer-write
Test httpBlobUpload.Write method
2019-09-05 08:16:36 -07:00
Andrew Lavery
cc97b94f5d
use travis, not circle, build badge
Signed-off-by: Andrew Lavery <laverya@umich.edu>
2019-09-04 15:38:24 -07:00
Derek McGowan
aeaeb84407
Merge pull request #3000 from adrian-plata/master
Adding deprecated schema instructions
2019-09-03 11:54:37 -07:00
Adrian Plata
07a50201c9 Adding deprecated schema instructions
Signed-off-by: Adrian Plata <adrian.plata@docker.com>
2019-09-03 11:44:28 -07:00
Bouke van der Bijl
1c481d34d9 registry: Fix typo in RepositoryRemover warning
Signed-off-by: Bouke van der Bijl <me@bou.ke>
2019-09-02 16:07:34 +00:00
Adam Dobrawy
a994bb839d use latest version of alpine when building the Docker container
Signed-off-by: Adam Dobrawy <naczelnik@jawnosc.tk>
2019-08-30 00:58:36 +02:00
Mike Lundy
c486db2d71
make it possible to wrap the client transport in another one
Signed-off-by: Mike Lundy <mike@fluffypenguin.org>
2019-08-22 17:37:47 -04:00
Ryan Abrams
1fb7fffdb2
Merge pull request #2950 from terinjokes/patches/swift-segment-hash
swift: correct segment path generation
2019-07-11 15:35:31 -07:00
Ryan Abrams
10f726344d
Merge pull request #2955 from alex-laties/master
allow for VERSION and REVISION to be passed in during docker builds
2019-07-11 15:30:23 -07:00
Ryan Abrams
8063102951
Merge pull request #2917 from dmathieu/repository-blob-resume
Implement Repository Blobs upload resuming
2019-07-11 15:28:05 -07:00
Derek McGowan
438b67feef
Merge pull request #2947 from tariq1890/update_versions
Update the versions of several dependencies
2019-07-08 13:33:36 -07:00
Ryan Abrams
be07be9904
Merge pull request #2522 from tifayuki/notification_metrics
Add notification metrics
2019-06-28 11:10:51 -07:00
Alex Laties
92d213d2c1 allow for VERSION and REVISION to be passed in during docker builds
Signed-off-by: Alex Laties <agl@tumblr.com>
2019-06-26 18:06:51 -04:00
Damien Mathieu
dd3bdee21c implement Repository Blobs upload resuming
Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-26 09:46:49 +02:00
Ryan Abrams
90dfea7952
Merge pull request #2921 from dmathieu/repository-serve-blob
Implement Repository ServeBlob
2019-06-25 19:07:38 -07:00
Ryan Abrams
6c72ec2e85
Merge pull request #2927 from dmathieu/blob-create-uuid
Handle Blob Create when the underlying registry doesn't provide 'Docker-Upload-UUID'
2019-06-25 19:06:46 -07:00
Ryan Abrams
ec84b86013
Merge pull request #2930 from dmathieu/extract-blob-resume
Extract blob upload resume into its own method
2019-06-25 19:06:16 -07:00
Damien Mathieu
898b1f2a53 test httpBlobUpload.Write method
Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:32:58 +02:00
Damien Mathieu
c5d5f938e3 fast-stop ServeBlob if we're doing a HEAD request
A registry pointing to ECR is having issues if we try loading the blob

Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:30:22 +02:00
Damien Mathieu
3800c47fd2 Implement Repository ServeBlob
Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:30:22 +02:00
Damien Mathieu
a45e5cb13f handle create blob if the uuid couldn't be retrieved from headers or URL
Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:29:38 +02:00
Damien Mathieu
8b31a894bd deduce blob UUID from location if it wasn't provided in the headers
Some registries (ECR) don't provide a `Docker-Upload-UUID` when creating
a blob. So we can't rely on that header. Fallback to reading it from the
URL.

Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:29:38 +02:00
Damien Mathieu
94097512db extract blob upload resume into its own method
I've found this logic being in a single method to be quite hard to get.
I believe extracting it makes it easier to read, as we can then more
easily see what the main method does and possibly ignore the intricacies
of `ResumeBlobUpload`.

Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-06-25 09:25:56 +02:00
Terin Stock
b23dd1ef37 swift: correct segment path generation
When uploading segments to Swift, the registry generates a random file,
by taking the hash of the container path and 32-bytes of random data.
The registry attempts to shard across multiple directory paths, by
taking the first three hex characters as leader.

The implementation in registry, unfortunately, takes the hash of
nothing, and appends it to the path and random data. This results in all
segments being created in one directory.

Fixes: #2407
Fixes: #2311
Signed-off-by: Terin Stock <terinjokes@gmail.com>
2019-06-22 23:44:43 -07:00
Ryan Abrams
79f6bcbe16
Merge pull request #2946 from tariq1890/upd_alpine
use latest version of alpine when building the Docker container
2019-06-20 10:38:41 -07:00
Tariq Ibrahim
afe29bb697
update the golang compiler version and the versions of several dependencies
Signed-off-by: Tariq Ibrahim <tariq181290@gmail.com>
2019-06-19 22:43:52 -07:00
Tariq Ibrahim
45b2d0498d
use latest version of alpine when building the Docker container
Signed-off-by: Tariq Ibrahim <tariq181290@gmail.com>
2019-06-19 22:21:07 -07:00
Ryan Abrams
84f47e7bb3
Merge pull request #2900 from sevki/no-token-err-fix
fix no error returned in fetchTokenWithOAuth
2019-06-19 19:16:18 -07:00
Ryan Abrams
6c329e56a2
Merge pull request #2920 from dmathieu/blob_writer_offset
Append the written bytes to the blob writer's size
2019-06-19 19:14:49 -07:00
Ryan Abrams
71309e96e4
Merge pull request #2809 from andyzhangx/ssl
change default Dockerfile to install ssl utils
2019-06-19 19:14:14 -07:00
Ryan Abrams
0c394fdd84
Merge pull request #2861 from yuwaMSFT2/master
Fixes #2835 Process Accept header MIME types in case-insensitive way
2019-06-19 19:09:29 -07:00
Ryan Abrams
d80a17d8e0
Merge pull request #2879 from tbe/fix-s3-ceph
Fix s3 driver for supporting ceph radosgw
2019-06-19 19:09:07 -07:00
Ryan Abrams
55287010ce
Merge pull request #2894 from jabrown85/fix-offset-typos
Fix typo: offest -> offset
2019-06-19 19:08:22 -07:00
Ryan Abrams
c2254191d4
Merge pull request #2941 from tariq1890/gomod
migrate to go modules from vndr
2019-06-19 18:41:26 -07:00
Tariq Ibrahim
5223c27422
migrate to go modules from vndr
Signed-off-by: Tariq Ibrahim <tariq181290@gmail.com>
2019-06-19 12:24:07 -07:00
Ryan Abrams
dcfe05ce6c
Merge pull request #2926 from tariq1890/no_rsc
replace rsc.io/letsencrypt in favour of golang.org/x/crypto
2019-06-05 15:34:28 -07:00
Tariq Ibrahim
8f9c8094fb
replace rsc.io/letsencrypt in favour of golang.org/x/crypto
Signed-off-by: Tariq Ibrahim <tariq181290@gmail.com>
2019-06-04 12:04:18 -07:00
Damien Mathieu
0e2d080a8a append the written bytes to the blob writer's size
Any byte written should append to the size. Otherwise, the full Size is
always zero.

Signed-off-by: Damien Mathieu <dmathieu@salesforce.com>
2019-05-09 14:17:58 +02:00
Ryan Abrams
3226863cbc
Merge pull request #2849 from Shawnpku/master
support Alibaba Cloud CDN storage middleware
2019-04-16 18:43:06 -07:00
Sevki Hasirci
5afbf32400 fix no error returned in fetchTokenWithOAuth
fetchTokenWithBasicAuth checks if a token is in the token response
but fetchTokenWithOAuth does not

these changes implements the same behaviour for the latter
returning a `ErrNoToken` if a token is not found in the resposne

Signed-off-by: Sevki Hasirci <sevki@cloudflare.com>
2019-04-14 11:05:59 +01:00
Jesse Brown
74f429a5ad Fix typo: offest -> offset
Signed-off-by: Jesse Brown <jabrown85@gmail.com>
2019-04-05 14:20:20 -05:00
Shawn Chen
51bb5cee5b import alicdn package
Signed-off-by: Shawn Chen <chen8132@gmail.com>
2019-03-19 15:06:02 +08:00
Shawn Chen
fd77cf43a6 change package name & format document
Signed-off-by: Shawn Chen <chen8132@gmail.com>
2019-03-18 11:35:46 +08:00
Thomas Berger
c18c6c33b2 S3 Driver: added comment for missing KeyCount workaround
Signed-off-by: Thomas Berger <loki@lokis-chaos.de>
2019-03-15 21:05:21 +01:00
Eohyung Lee
f877726503 Fix s3 driver for supporting ceph radosgw
Radosgw does not support S3 `GET Bucket` API v2 API but v1.
This API has backward compatibility, so most of this API is working
correctly but we can not get `KeyCount` in v1 API and which is only
for v2 API.

Signed-off-by: Eohyung Lee <liquidnuker@gmail.com>
2019-03-08 12:45:20 +01:00
Ryan Abrams
6d62eb1d4a
Merge pull request #2837 from vishesh92/fix-cloudfront-middleware
Fix cloudfront middleware
2019-03-04 16:42:08 -08:00
Shawn Chen
3390f32aec fix Context issue
Signed-off-by: Shawn Chen <chen8132@gmail.com>
2019-03-04 17:48:32 +08:00
Shawn Chen
ae91d1f429 fix ci issue
Signed-off-by: Shawn Chen <chen8132@gmail.com>
2019-03-04 17:17:57 +08:00
Shawnpku
6e10631d9c fix default cdn auth duration
Signed-off-by: Shawnpku <chen8132@gmail.com>
2019-03-04 14:53:48 +08:00
Vishesh Jindal
e1e72e9563
Fix cloudfront documentation formatting
Signed-off-by: Vishesh Jindal <vishesh92@gmail.com>
2019-03-02 08:58:52 +05:30
Vishesh Jindal
f9a0506191
Bugfix: Make ipfilteredby not required
Signed-off-by: Vishesh Jindal <vishesh92@gmail.com>
2019-03-02 08:58:52 +05:30
Ryan Abrams
d3ddc3572c
Merge pull request #2854 from manishtomar/log-authed-name
Log authorized username
2019-03-01 14:45:43 -08:00
Ryan Abrams
c192a281f8
Merge pull request #2813 from lucab/ups/spec-json-binary
registry: fix binary JSON content-type
2019-03-01 14:26:54 -08:00
Yu Wang
a683c7c235 Fixes #2835 Process Accept header MIME types in case-insensitive way
Use mime.ParseMediaType to parse the media types in Accept header in manifest request. Ignore the failed ones.

Signed-off-by: Yu Wang <yuwa@microsoft.com>
2019-02-21 15:11:41 -08:00
Shawnpku
bbc9885aa2 fix func name
Signed-off-by: Shawnpku <chen8132@gmail.com>
2019-02-20 15:54:21 +08:00
Honglin Feng
92a6436714 rename the metrics label
Signed-off-by: Honglin Feng <tifayuki@gmail.com>
2019-02-17 12:46:10 +08:00
Honglin Feng
d5a615b8c9 update the event number
Signed-off-by: Honglin Feng <tifayuki@gmail.com>
2019-02-15 21:55:17 +08:00
Honglin Feng
09a63caa37 run go fmt and goimports
Signed-off-by: Honglin Feng <tifayuki@gmail.com>
2019-02-15 21:14:57 +08:00
Honglin Feng
228bafca0b run go fmt
Signed-off-by: Honglin Feng <tifayuki@gmail.com>
2019-02-15 21:14:57 +08:00
Honglin Feng
76da6290b0 add label to the metrics
Signed-off-by: Honglin Feng <tifayuki@gmail.com>
2019-02-15 21:14:57 +08:00
tifayuki
8b70616846 Add notification metrics
It adds notification related prometheus metrics, including:
  - total count for events/success/failure/error
  - total count for notification per each status code
  - gauge of the pending notification queue

Signed-off-by: tifayuki <tifayuki@gmail.com>
2019-02-15 21:14:57 +08:00
Manish Tomar
ec6566c02b Log authorized username
This is useful to know which user pulled/pushed which repo.

Signed-off-by: Manish Tomar <manish.tomar@docker.com>
2019-02-13 08:49:37 -08:00
Shawnpku
3aa2a282f7 support alicdn middleware
Signed-off-by: Shawnpku <chen8132@gmail.com>
2019-02-11 15:11:26 +08:00
Derek McGowan
0d3efadf01
Merge pull request #2840 from manishtomar/fix-lint
Fix gometalint errors
2019-02-04 16:58:09 -08:00
Manish Tomar
48818fdea7 Remove err nil check
since type checking nil will not panic and return appropriately

Signed-off-by: Manish Tomar <manish.tomar@docker.com>
2019-02-04 16:42:44 -08:00
Ryan Abrams
411d6bcfd2
Merge pull request #2786 from thaJeztah/add_normalize_util
Add reference. ParseDockerRef utility function
2019-02-04 16:19:21 -08:00
Manish Tomar
da8db4666b Fix gometalint errors
Signed-off-by: Manish Tomar <manish.tomar@docker.com>
2019-02-04 16:01:04 -08:00
Derek McGowan
b75069ef13
Merge pull request #2791 from AliyunContainerService/support-oss-byok
Support BYOK for OSS storage driver
2019-01-16 16:37:37 -08:00
Ryan Abrams
b1fd12d0c1
Merge pull request #2808 from gregrebholz/feat/tls1.2only
Registry - make minimum TLS version user configurable
2019-01-14 09:31:36 -08:00
Luca Bruno
15b0204758
registry: fix binary JSON content-type
This fixes registry endpoints to return the proper `application/json`
content-type for JSON content, also updating spec examples for that.

As per IETF specification and IANA registry [0], the `application/json`
type is a binary media, so the content-type label does not need any
text-charset selector. Additionally, the media type definition
explicitly states that it has no required nor optional parameters,
which makes the current registry headers non-compliant.

[0]: https://www.iana.org/assignments/media-types/application/json

Signed-off-by: Luca Bruno <lucab@debian.org>
2019-01-14 09:04:42 +00:00
Greg Rebholz
cdb62b2b77 Registry - make minimum TLS version user configurable
Signed-off-by: J. Gregory Rebholz <gregrebholz@gmail.com>
2019-01-11 18:11:03 -05:00
andyzhangx
7df881dcbe change default Dockerfile to install ssl utils
Signed-off-by: andyzhangx <xiazhang@microsoft.com>
2019-01-10 03:56:42 +00:00
Derek McGowan
91b0f0559e
Merge pull request #2801 from caervs/ISS-2793
Add docs for autoredirect config parameter
2019-01-07 10:58:55 -08:00
Derek McGowan
980d1a696b
Merge pull request #2800 from davidswu/autoredirect
default autoredirect to false
2019-01-07 10:26:29 -08:00
Ryan Abrams
d1abdeb623 Add docs for autoredirect config parameter
Config parameter is user facing so should be documented.

Signed-off-by: Ryan Abrams <rdabrams@gmail.com>
2019-01-04 13:11:20 -08:00
Sebastiaan van Stijn
0ac367fd6b
Add reference.ParseDockerRef utility function
ParseDockerRef normalizes the image reference following the docker
convention. This is added mainly for backward compatibility. The reference
returned can only be either tagged or digested. For reference contains both tag
and digest, the function returns digested reference, e.g.

    docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa

will be returned as

    docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-01-04 21:26:06 +01:00
David Wu
eb1a2cd911 default autoredirect to false
Signed-off-by: David Wu <david.wu@docker.com>
2019-01-04 11:05:12 -08:00
Li Yi
b7839211af Update doc for BYOK support in OSS storage driver
Change-Id: I195f29195f999e3e14ba921fd3435a6dc4788417
Signed-off-by: Li Yi <denverdino@gmail.com>
2018-12-25 09:32:16 +08:00
Li Yi
90bed67126 Support BYOK for OSS storage driver
Change-Id: I423ad03e63bd38aded3abfcba49079ff2fbb3b74
Signed-off-by: Li Yi <denverdino@gmail.com>
2018-12-25 08:30:40 +08:00
Derek McGowan
40b7b5830a
Merge pull request #2775 from caervs/release_notes_2.7
Release notes for 2.7
2018-12-03 16:05:39 -08:00
Ryan Abrams
08c6bbed05 Release notes for 2.7
Signed-off-by: Ryan Abrams <rdabrams@gmail.com>
2018-11-29 16:35:06 -08:00
Derek McGowan
d9e1218235
Merge pull request #2772 from dmcgowan/add-arm-flag
Add GOARM flag to dockerfile
2018-11-29 15:15:00 -08:00
Derek McGowan
63f6c1205d
Add GOARM flag to dockerfile
When building with arm on alpine, GOARM should
be set to 6 by default.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2018-11-28 10:40:29 -08:00
Derek McGowan
aa985ba889
Merge pull request #2711 from davidswu/autoredirect
add autoredirect auth config
2018-11-27 15:48:25 -08:00
Derek McGowan
dd36fd3622
Merge pull request #2742 from tescherm/configure-bugsnag-logging
add bugsnag logrus hook
2018-11-27 15:08:17 -08:00
Manish Tomar
1251e51ad0 better name and updated tests
- use ManifestDigests name instead of Indexes
- update tests to validate against multiple tags

Signed-off-by: Manish Tomar <manish.tomar@docker.com>
2018-11-01 10:31:08 -07:00
Manish Tomar
9ebf151ac2 API to retrive tag's digests
Add an interface alongside TagStore that provides API to retreive
digests of all manifests that a tag historically pointed to. It also
includes currently linked tag.

Signed-off-by: Manish Tomar <manish.tomar@docker.com>
2018-10-29 18:19:05 -07:00
Matt Tescher
7c4d584e58 add bugsnag logrus hook
Signed-off-by: Matt Tescher <matthew.tescher@docker.com>
2018-10-25 14:52:10 -07:00
Olivier Gambier
93e082742a
Merge pull request #2734 from mgrachev/patch-1
Fix typo
2018-10-24 10:01:56 -07:00
Olivier Gambier
f7046a6d68
Merge pull request #2735 from tonistiigi/update-dockerfile
update Dockerfile to multi-stage
2018-10-24 10:01:01 -07:00
Grachev Mikhail
cd1648d62c Fix typo
Signed-off-by: Mikhail Grachev <work@mgrachev.com>
2018-10-18 17:54:19 +03:00
Tonis Tiigi
8a800e1292 update Dockerfile to multi-stage
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2018-10-15 15:48:05 -07:00
Olivier Gambier
1cb4180b1a
Merge pull request #2729 from liyongxin/master
typo fix about overridden
2018-10-02 15:04:33 -07:00
Olivier Gambier
451cd548a0
Merge pull request #2731 from mirake/fix-typos
Fix typo: commmand -> command
2018-10-02 15:03:57 -07:00
Rui Cao
6335cc258f Fix typo: commmand -> command
Signed-off-by: Rui Cao <ruicao@alauda.io>
2018-09-29 15:12:10 +08:00
Yongxin Li
de8636b78c typo fix about overridden
Signed-off-by: Yongxin Li <yxli@alauda.io>
2018-09-27 20:27:09 +08:00
David Wu
2e1e6307dd add autoredirect to option
Signed-off-by: David Wu <david.wu@docker.com>
2018-09-20 19:33:06 -07:00
David Wu
b2bd465760 fix checks
Signed-off-by: David Wu <david.wu@docker.com>
2018-09-20 15:24:27 -07:00
Viktor Stanchev
f730f3ab77 add autoredirect auth config
It redirects the user to to the Host header's domain whenever they try to use
token auth.

Signed-off-by: David Wu <david.wu@docker.com>
2018-09-20 14:47:43 -07:00
Gladkov Alexey
e69837454a Add tests for configuration parser
Signed-off-by: Gladkov Alexey <agladkov@redhat.com>
2017-11-15 17:01:19 +01:00
Gladkov Alexey
c9eba1a5bb Fix the pointer initialization
If the overwriteStruct() finds an uninitialized pointer, it tries to initialize it,
but does it incorrectly. It tries to assign a pointer to pointer, instead of pointer.

Signed-off-by: Gladkov Alexey <agladkov@redhat.com>
2017-11-14 15:32:05 +01:00
1193 changed files with 366507 additions and 62757 deletions

3
.github/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Docker Distribution Community Code of Conduct
Docker Distribution follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).

20
.golangci.yml Normal file
View file

@ -0,0 +1,20 @@
linters:
enable:
- structcheck
- varcheck
- staticcheck
- unconvert
- gofmt
- goimports
- golint
- ineffassign
- vet
- unused
- misspell
disable:
- errcheck
run:
deadline: 2m
skip-dirs:
- vendor

View file

@ -1,16 +0,0 @@
{
"Vendor": true,
"Deadline": "2m",
"Sort": ["linter", "severity", "path", "line"],
"EnableGC": true,
"Enable": [
"structcheck",
"staticcheck",
"unconvert",
"gofmt",
"goimports",
"golint",
"vet"
]
}

View file

@ -1,13 +1,18 @@
dist: trusty
dist: bionic
sudo: required
# setup travis so that we can run containers for integration tests
services:
- docker
jobs:
include:
- arch: amd64
- arch: s390x
language: go
go:
- "1.11.x"
- "1.14.x"
go_import_path: github.com/docker/distribution
@ -25,7 +30,7 @@ before_install:
- sudo apt-get -q update
install:
- go get -u github.com/vbatts/git-validation
- cd /tmp && go get -u github.com/vbatts/git-validation
# TODO: Add enforcement of license
# - go get -u github.com/kunalkushwaha/ltag
- cd $TRAVIS_BUILD_DIR
@ -34,7 +39,7 @@ script:
- export GOOS=$TRAVIS_GOOS
- export CGO_ENABLED=$TRAVIS_CGO_ENABLED
- DCO_VERBOSITY=-q script/validate/dco
- GOOS=linux script/setup/install-dev-tools
- GOOS=linux GO111MODULE=on script/setup/install-dev-tools
- script/validate/vendor
- go build -i .
- make check

View file

@ -4,11 +4,12 @@
### If your problem is with...
- automated builds
- your account on the [Docker Hub](https://hub.docker.com/)
- any other [Docker Hub](https://hub.docker.com/) issue
Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com)
- automated builds or your [Docker Hub](https://hub.docker.com/) account
- Report it to [Hub Support](https://hub.docker.com/support/)
- Distributions of Docker for desktop or Linux
- Report [Mac Desktop issues](https://github.com/docker/for-mac)
- Report [Windows Desktop issues](https://github.com/docker/for-win)
- Report [Linux issues](https://github.com/docker/for-linux)
### If you...
@ -16,10 +17,8 @@ Then please do not report your issue here - you should instead report it to [htt
- can't figure out something
- are not sure what's going on or what your problem is
Then please do not open an issue here yet - you should first try one of the following support forums:
- irc: #docker-distribution on freenode
- mailing-list: <distribution@dockerproject.org> or https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
Please ask first in the #distribution channel on Docker community slack.
[Click here for an invite to Docker community slack](https://dockr.ly/slack)
### Reporting security issues
@ -59,90 +58,72 @@ By following these simple rules you will get better and faster feedback on your
7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used)
8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry
## Contributing a patch for a known bug, or a small correction
## Contributing Code
Contributions should be made via pull requests. Pull requests will be reviewed
by one or more maintainers or reviewers and merged when acceptable.
You should follow the basic GitHub workflow:
1. fork
2. commit a change
3. make sure the tests pass
4. PR
1. Use your own [fork](https://help.github.com/en/articles/about-forks)
2. Create your [change](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
3. Test your code
4. [Commit](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) your work, always [sign your commits](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages)
5. Push your change to your fork and create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)
Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple:
Refer to [containerd's contribution guide](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
for tips on creating a successful contribution.
- configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com`
- sign your commits using `-s`: `git commit -s -m "My commit"`
## Sign your work
Some simple rules to ensure quick merge:
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
- clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`)
- prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once
- if you need to amend your PR following comments, please squash instead of adding more commits
```
Developer Certificate of Origin
Version 1.1
## Contributing new features
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning.
If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work.
Developer's Certificate of Origin 1.1
Then you should submit your implementation, clearly linking to the issue (and possible proposal).
By making a contribution to this project, I certify that:
Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged.
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
It's mandatory to:
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
- interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines)
- address maintainers' comments and modify your submission accordingly
- write tests for any new code
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Have a look at a great, successful contribution: the [Swift driver PR](https://github.com/docker/distribution/pull/493)
Then you just add a line to every git commit message:
## Coding Style
Signed-off-by: Joe Smith <joe.smith@email.com>
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
Use your real name (sorry, no pseudonyms or anonymous contributions.)
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributions should make a
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](http://golang.org/doc/effective_go.html). The
[Go Blog](http://blog.golang.org/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.

View file

@ -1,20 +1,30 @@
FROM golang:1.10-alpine
ARG GO_VERSION=1.13.8
FROM golang:${GO_VERSION}-alpine3.11 AS build
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs
ENV BUILDTAGS include_oss include_gcs
ARG GOOS=linux
ARG GOARCH=amd64
ARG GOARM=6
ARG VERSION
ARG REVISION
RUN set -ex \
&& apk add --no-cache make git
&& apk add --no-cache make git file
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR
RUN CGO_ENABLED=0 make PREFIX=/go clean binaries && file ./bin/registry | grep "statically linked"
FROM alpine:3.11
RUN set -ex \
&& apk add --no-cache ca-certificates apache2-utils
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml
RUN make PREFIX=/go clean binaries
COPY --from=build /go/src/github.com/docker/distribution/bin/registry /bin/registry
VOLUME ["/var/lib/registry"]
EXPOSE 5000
ENTRYPOINT ["registry"]

144
GOVERNANCE.md Normal file
View file

@ -0,0 +1,144 @@
# docker/distribution Project Governance
Docker distribution abides by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
For specific guidance on practical contribution steps please
see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide.
## Maintainership
There are different types of maintainers, with different responsibilities, but
all maintainers have 3 things in common:
1) They share responsibility in the project's success.
2) They have made a long-term, recurring time investment to improve the project.
3) They spend that time doing whatever needs to be done, not necessarily what
is the most interesting or fun.
Maintainers are often under-appreciated, because their work is harder to appreciate.
It's easy to appreciate a really cool and technically advanced feature. It's harder
to appreciate the absence of bugs, the slow but steady improvement in stability,
or the reliability of a release process. But those things distinguish a good
project from a great one.
## Reviewers
A reviewer is a core role within the project.
They share in reviewing issues and pull requests and their LGTM counts towards the
required LGTM count to merge a code change into the project.
Reviewers are part of the organization but do not have write access.
Becoming a reviewer is a core aspect in the journey to becoming a maintainer.
## Adding maintainers
Maintainers are first and foremost contributors that have shown they are
committed to the long term success of a project. Contributors wanting to become
maintainers are expected to be deeply involved in contributing code, pull
request review, and triage of issues in the project for more than three months.
Just contributing does not make you a maintainer, it is about building trust
with the current maintainers of the project and being a person that they can
depend on and trust to make decisions in the best interest of the project.
Periodically, the existing maintainers curate a list of contributors that have
shown regular activity on the project over the prior months. From this list,
maintainer candidates are selected and proposed in a pull request or a
maintainers communication channel.
After a candidate has been announced to the maintainers, the existing
maintainers are given five business days to discuss the candidate, raise
objections and cast their vote. Votes may take place on the communication
channel or via pull request comment. Candidates must be approved by at least 66%
of the current maintainers by adding their vote on the mailing list. The
reviewer role has the same process but only requires 33% of current maintainers.
Only maintainers of the repository that the candidate is proposed for are
allowed to vote.
If a candidate is approved, a maintainer will contact the candidate to invite
the candidate to open a pull request that adds the contributor to the
MAINTAINERS file. The voting process may take place inside a pull request if a
maintainer has already discussed the candidacy with the candidate and a
maintainer is willing to be a sponsor by opening the pull request. The candidate
becomes a maintainer once the pull request is merged.
## Stepping down policy
Life priorities, interests, and passions can change. If you're a maintainer but
feel you must remove yourself from the list, inform other maintainers that you
intend to step down, and if possible, help find someone to pick up your work.
At the very least, ensure your work can be continued where you left off.
After you've informed other maintainers, create a pull request to remove
yourself from the MAINTAINERS file.
## Removal of inactive maintainers
Similar to the procedure for adding new maintainers, existing maintainers can
be removed from the list if they do not show significant activity on the
project. Periodically, the maintainers review the list of maintainers and their
activity over the last three months.
If a maintainer has shown insufficient activity over this period, a neutral
person will contact the maintainer to ask if they want to continue being
a maintainer. If the maintainer decides to step down as a maintainer, they
open a pull request to be removed from the MAINTAINERS file.
If the maintainer wants to remain a maintainer, but is unable to perform the
required duties they can be removed with a vote of at least 66% of the current
maintainers. In this case, maintainers should first propose the change to
maintainers via the maintainers communication channel, then open a pull request
for voting. The voting period is five business days. The voting pull request
should not come as a surpise to any maintainer and any discussion related to
performance must not be discussed on the pull request.
## How are decisions made?
Docker distribution is an open-source project with an open design philosophy.
This means that the repository is the source of truth for EVERY aspect of the
project, including its philosophy, design, road map, and APIs. *If it's part of
the project, it's in the repo. If it's in the repo, it's part of the project.*
As a result, all decisions can be expressed as changes to the repository. An
implementation change is a change to the source code. An API change is a change
to the API specification. A philosophy change is a change to the philosophy
manifesto, and so on.
All decisions affecting distribution, big and small, follow the same 3 steps:
* Step 1: Open a pull request. Anyone can do this.
* Step 2: Discuss the pull request. Anyone can do this.
* Step 3: Merge or refuse the pull request. Who does this depends on the nature
of the pull request and which areas of the project it affects.
## Helping contributors with the DCO
The [DCO or `Sign your work`](./CONTRIBUTING.md#sign-your-work)
requirement is not intended as a roadblock or speed bump.
Some contributors are not as familiar with `git`, or have used a web
based editor, and thus asking them to `git commit --amend -s` is not the best
way forward.
In this case, maintainers can update the commits based on clause (c) of the DCO.
The most trivial way for a contributor to allow the maintainer to do this, is to
add a DCO signature in a pull requests's comment, or a maintainer can simply
note that the change is sufficiently trivial that it does not substantially
change the existing contribution - i.e., a spelling change.
When you add someone's DCO, please also add your own to keep a log.
## I'm a maintainer. Should I make pull requests too?
Yes. Nobody should ever push to master directly. All changes should be
made through a pull request.
## Conflict Resolution
If you have a technical dispute that you feel has reached an impasse with a
subset of the community, any contributor may open an issue, specifically
calling for a resolution vote of the current core maintainers to resolve the
dispute. The same voting quorums required (2/3) for adding and removing
maintainers will apply to conflict resolution.

View file

@ -1,243 +1,16 @@
# Distribution maintainers file
# Docker distribution project maintainers & reviewers
#
# This file describes who runs the docker/distribution project and how.
# This is a living document - if you see something out of date or missing, speak up!
# See GOVERNANCE.md for maintainer versus reviewer roles
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant parser.
# MAINTAINERS
# GitHub ID, Name, Email address
"dmcgowan","Derek McGowan","derek@mcgstyle.net"
"manishtomar","Manish Tomar","manish.tomar@docker.com"
"stevvooe","Stephen Day","stevvooe@gmail.com"
#
[Rules]
[Rules.maintainers]
title = "What is a maintainer?"
text = """
There are different types of maintainers, with different responsibilities, but
all maintainers have 3 things in common:
1) They share responsibility in the project's success.
2) They have made a long-term, recurring time investment to improve the project.
3) They spend that time doing whatever needs to be done, not necessarily what
is the most interesting or fun.
Maintainers are often under-appreciated, because their work is harder to appreciate.
It's easy to appreciate a really cool and technically advanced feature. It's harder
to appreciate the absence of bugs, the slow but steady improvement in stability,
or the reliability of a release process. But those things distinguish a good
project from a great one.
"""
[Rules.reviewer]
title = "What is a reviewer?"
text = """
A reviewer is a core role within the project.
They share in reviewing issues and pull requests and their LGTM count towards the
required LGTM count to merge a code change into the project.
Reviewers are part of the organization but do not have write access.
Becoming a reviewer is a core aspect in the journey to becoming a maintainer.
"""
[Rules.adding-maintainers]
title = "How are maintainers added?"
text = """
Maintainers are first and foremost contributors that have shown they are
committed to the long term success of a project. Contributors wanting to become
maintainers are expected to be deeply involved in contributing code, pull
request review, and triage of issues in the project for more than three months.
Just contributing does not make you a maintainer, it is about building trust
with the current maintainers of the project and being a person that they can
depend on and trust to make decisions in the best interest of the project.
Periodically, the existing maintainers curate a list of contributors that have
shown regular activity on the project over the prior months. From this list,
maintainer candidates are selected and proposed on the maintainers mailing list.
After a candidate has been announced on the maintainers mailing list, the
existing maintainers are given five business days to discuss the candidate,
raise objections and cast their vote. Candidates must be approved by at least 66% of the current maintainers by adding their vote on the mailing
list. Only maintainers of the repository that the candidate is proposed for are
allowed to vote.
If a candidate is approved, a maintainer will contact the candidate to invite
the candidate to open a pull request that adds the contributor to the
MAINTAINERS file. The candidate becomes a maintainer once the pull request is
merged.
"""
[Rules.stepping-down-policy]
title = "Stepping down policy"
text = """
Life priorities, interests, and passions can change. If you're a maintainer but
feel you must remove yourself from the list, inform other maintainers that you
intend to step down, and if possible, help find someone to pick up your work.
At the very least, ensure your work can be continued where you left off.
After you've informed other maintainers, create a pull request to remove
yourself from the MAINTAINERS file.
"""
[Rules.inactive-maintainers]
title = "Removal of inactive maintainers"
text = """
Similar to the procedure for adding new maintainers, existing maintainers can
be removed from the list if they do not show significant activity on the
project. Periodically, the maintainers review the list of maintainers and their
activity over the last three months.
If a maintainer has shown insufficient activity over this period, a neutral
person will contact the maintainer to ask if they want to continue being
a maintainer. If the maintainer decides to step down as a maintainer, they
open a pull request to be removed from the MAINTAINERS file.
If the maintainer wants to remain a maintainer, but is unable to perform the
required duties they can be removed with a vote of at least 66% of
the current maintainers. An e-mail is sent to the
mailing list, inviting maintainers of the project to vote. The voting period is
five business days. Issues related to a maintainer's performance should be
discussed with them among the other maintainers so that they are not surprised
by a pull request removing them.
"""
[Rules.decisions]
title = "How are decisions made?"
text = """
Short answer: EVERYTHING IS A PULL REQUEST.
distribution is an open-source project with an open design philosophy. This means
that the repository is the source of truth for EVERY aspect of the project,
including its philosophy, design, road map, and APIs. *If it's part of the
project, it's in the repo. If it's in the repo, it's part of the project.*
As a result, all decisions can be expressed as changes to the repository. An
implementation change is a change to the source code. An API change is a change
to the API specification. A philosophy change is a change to the philosophy
manifesto, and so on.
All decisions affecting distribution, big and small, follow the same 3 steps:
* Step 1: Open a pull request. Anyone can do this.
* Step 2: Discuss the pull request. Anyone can do this.
* Step 3: Merge or refuse the pull request. Who does this depends on the nature
of the pull request and which areas of the project it affects.
"""
[Rules.DCO]
title = "Helping contributors with the DCO"
text = """
The [DCO or `Sign your work`](
https://github.com/moby/moby/blob/master/CONTRIBUTING.md#sign-your-work)
requirement is not intended as a roadblock or speed bump.
Some distribution contributors are not as familiar with `git`, or have used a web
based editor, and thus asking them to `git commit --amend -s` is not the best
way forward.
In this case, maintainers can update the commits based on clause (c) of the DCO.
The most trivial way for a contributor to allow the maintainer to do this, is to
add a DCO signature in a pull requests's comment, or a maintainer can simply
note that the change is sufficiently trivial that it does not substantially
change the existing contribution - i.e., a spelling change.
When you add someone's DCO, please also add your own to keep a log.
"""
[Rules."no direct push"]
title = "I'm a maintainer. Should I make pull requests too?"
text = """
Yes. Nobody should ever push to master directly. All changes should be
made through a pull request.
"""
[Rules.tsc]
title = "Conflict Resolution and technical disputes"
text = """
distribution defers to the [Technical Steering Committee](https://github.com/moby/tsc) for escalations and resolution on disputes for technical matters."
"""
[Rules.meta]
title = "How is this process changed?"
text = "Just like everything else: by making a pull request :)"
# Current project organization
[Org]
[Org.Maintainers]
people = [
"dmcgowan",
"dmp42",
"stevvooe",
]
[Org.Reviewers]
people = [
"manishtomar",
"caervs",
"davidswu",
"RobbKistler"
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.caervs]
Name = "Ryan Abrams"
Email = "rdabrams@gmail.com"
GitHub = "caervs"
[people.davidswu]
Name = "David Wu"
Email = "dwu7401@gmail.com"
GitHub = "davidswu"
[people.dmcgowan]
Name = "Derek McGowan"
Email = "derek@mcgstyle.net"
GitHub = "dmcgowan"
[people.dmp42]
Name = "Olivier Gambier"
Email = "olivier@docker.com"
GitHub = "dmp42"
[people.manishtomar]
Name = "Manish Tomar"
Email = "manish.tomar@docker.com"
GitHub = "manishtomar"
[people.RobbKistler]
Name = "Robb Kistler"
Email = "robb.kistler@docker.com"
GitHub = "RobbKistler"
[people.stevvooe]
Name = "Stephen Day"
Email = "stephen.day@docker.com"
GitHub = "stevvooe"
# REVIEWERS
# GitHub ID, Name, Email address
"caervs","Ryan Abrams","rdabrams@gmail.com"
"davidswu","David Wu","dwu7401@gmail.com"
"RobbKistler","Robb Kistler","robb.kistler@docker.com"
"thajeztah","Sebastiaan van Stijn","github@gone.nl"

View file

@ -2,8 +2,8 @@
ROOTDIR=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Used to populate version variable in main package.
VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always)
REVISION=$(shell git rev-parse HEAD)$(shell if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always)
REVISION ?= $(shell git rev-parse HEAD)$(shell if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
PKG=github.com/docker/distribution
@ -50,7 +50,7 @@ version/version.go:
check: ## run all linters (TODO: enable "unused", "varcheck", "ineffassign", "unconvert", "staticheck", "goimports", "structcheck")
@echo "$(WHALE) $@"
gometalinter --config .gometalinter.json ./...
@GO111MODULE=off golangci-lint run
test: ## run tests, except integration test with test.short
@echo "$(WHALE) $@"

View file

@ -2,31 +2,32 @@
The Docker toolset to pack, ship, store, and deliver content.
This repository's main product is the Docker Registry 2.0 implementation
for storing and distributing Docker images. It supersedes the
[docker/docker-registry](https://github.com/docker/docker-registry)
project with a new API design, focused around security and performance.
This repository's main product is the Open Source Docker Registry implementation
for storing and distributing Docker and OCI images using the
[OCI Distribution Specification](https://github.com/opencontainers/distribution-spec).
The goal of this project is to provide a simple, secure, and scalable base
for building a registry solution or running a simple private registry.
<img src="https://www.docker.com/sites/default/files/oyster-registry-3.png" width=200px/>
[![Circle CI](https://circleci.com/gh/docker/distribution/tree/master.svg?style=svg)](https://circleci.com/gh/docker/distribution/tree/master)
[![Build Status](https://travis-ci.org/docker/distribution.svg?branch=master)](https://travis-ci.org/docker/distribution)
[![GoDoc](https://godoc.org/github.com/docker/distribution?status.svg)](https://godoc.org/github.com/docker/distribution)
This repository contains the following components:
|**Component** |Description |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. |
| **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. |
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) |
| **registry** | An implementation of the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). |
| **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: The interfaces for these libraries are **unstable**. |
| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/) related just to the registry. |
### How does this integrate with Docker engine?
### How does this integrate with Docker, containerd, and other OCI client?
This project should provide an implementation to a V2 API for use in the [Docker
core project](https://github.com/docker/docker). The API should be embeddable
and simplify the process of securely pulling and pushing content from `docker`
daemons.
Clients implement against the OCI specification and communicate with the
registry using HTTP. This project contains an client implementation which
is currently in use by Docker, however, it is deprecated for the
[implementation in containerd](https://github.com/containerd/containerd/tree/master/remotes/docker)
and will not support new features.
### What are the long term goals of the Distribution project?
@ -43,18 +44,6 @@ system that allow users to:
* Implement their own home made solution through good specs, and solid
extensions mechanism.
## More about Registry 2.0
The new registry implementation provides the following benefits:
- faster push and pull
- new, more efficient implementation
- simplified deployment
- pluggable storage backend
- webhook notifications
For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md).
### Who needs to deploy a registry?
By default, Docker users pull images from Docker's public registry instance.
@ -78,53 +67,25 @@ For those who have previously deployed their own registry based on the Registry
data migration is required. A tool to assist with migration efforts has been
created. For more information see [docker/migrator](https://github.com/docker/migrator).
## Contribute
## Contribution
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
issues, fixes, and patches to this project. If you are contributing code, see
the instructions for [building a development environment](BUILDING.md).
## Support
## Communication
If any issues are encountered while using the _Distribution_ project, several
avenues are available for support:
For async communication and long running discussions please use issues and pull requests on the github repo.
This will be the best place to discuss design and implementation.
<table>
<tr>
<th align="left">
IRC
</th>
<td>
#docker-distribution on FreeNode
</td>
</tr>
<tr>
<th align="left">
Issue Tracker
</th>
<td>
github.com/docker/distribution/issues
</td>
</tr>
<tr>
<th align="left">
Google Groups
</th>
<td>
https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
</td>
</tr>
<tr>
<th align="left">
Mailing List
</th>
<td>
docker@dockerproject.org
</td>
</tr>
</table>
For sync communication we have a community slack with a #distribution channel that everyone is welcome to join and chat about development.
**Slack:** Catch us in the #distribution channels on dockercommunity.slack.com.
[Click here for an invite to Docker community slack.](https://dockr.ly/slack)
## License
## Licenses
This project is distributed under [Apache License, Version 2.0](LICENSE).
The distribution codebase is released under the [Apache 2.0 license](LICENSE).
The README.md file, and files in the "docs" folder are licensed under the
Creative Commons Attribution 4.0 International License. You may obtain a
copy of the license, titled CC-BY-4.0, at http://creativecommons.org/licenses/by/4.0/.

View file

@ -10,7 +10,7 @@ import (
"github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var (

View file

@ -21,7 +21,7 @@ import (
"text/template"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
)
var spaceRegex = regexp.MustCompile(`\n\s*`)

View file

@ -12,6 +12,7 @@ import (
_ "github.com/docker/distribution/registry/storage/driver/filesystem"
_ "github.com/docker/distribution/registry/storage/driver/gcs"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
_ "github.com/docker/distribution/registry/storage/driver/middleware/alicdn"
_ "github.com/docker/distribution/registry/storage/driver/middleware/cloudfront"
_ "github.com/docker/distribution/registry/storage/driver/middleware/redirect"
_ "github.com/docker/distribution/registry/storage/driver/oss"

View file

@ -108,6 +108,9 @@ type Configuration struct {
// A file may contain multiple CA certificates encoded as PEM
ClientCAs []string `yaml:"clientcas,omitempty"`
// Specifies the lowest TLS version allowed
MinimumTLS string `yaml:"minimumtls,omitempty"`
// LetsEncrypt is used to configuration setting up TLS through
// Let's Encrypt instead of manually specifying certificate and
// key. If a TLS certificate is specified, the Let's Encrypt
@ -388,7 +391,7 @@ func (loglevel *Loglevel) UnmarshalYAML(unmarshal func(interface{}) error) error
switch loglevelString {
case "error", "warn", "info", "debug":
default:
return fmt.Errorf("Invalid loglevel %s Must be one of [error, warn, info, debug]", loglevelString)
return fmt.Errorf("invalid loglevel %s Must be one of [error, warn, info, debug]", loglevelString)
}
*loglevel = Loglevel(loglevelString)
@ -463,7 +466,7 @@ func (storage *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if len(types) > 1 {
return fmt.Errorf("Must provide exactly one storage type. Provided: %v", types)
return fmt.Errorf("must provide exactly one storage type. Provided: %v", types)
}
}
*storage = storageMap
@ -665,11 +668,11 @@ func Parse(rd io.Reader) (*Configuration, error) {
v0_1.Loglevel = Loglevel("")
}
if v0_1.Storage.Type() == "" {
return nil, errors.New("No storage configuration provided")
return nil, errors.New("no storage configuration provided")
}
return (*Configuration)(v0_1), nil
}
return nil, fmt.Errorf("Expected *v0_1Configuration, received %#v", c)
return nil, fmt.Errorf("expected *v0_1Configuration, received %#v", c)
},
},
})

View file

@ -83,6 +83,7 @@ var configStruct = Configuration{
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"`
MinimumTLS string `yaml:"minimumtls,omitempty"`
LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"`
@ -105,6 +106,7 @@ var configStruct = Configuration{
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"`
MinimumTLS string `yaml:"minimumtls,omitempty"`
LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"`
@ -540,9 +542,7 @@ func copyConfig(config Configuration) *Configuration {
}
configCopy.Notifications = Notifications{Endpoints: []Endpoint{}}
for _, v := range config.Notifications.Endpoints {
configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v)
}
configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, config.Notifications.Endpoints...)
configCopy.HTTP.Headers = make(http.Header)
for k, v := range config.HTTP.Headers {

View file

@ -122,7 +122,7 @@ func (p *Parser) Parse(in []byte, v interface{}) error {
parseInfo, ok := p.mapping[versionedStruct.Version]
if !ok {
return fmt.Errorf("Unsupported version: %q", versionedStruct.Version)
return fmt.Errorf("unsupported version: %q", versionedStruct.Version)
}
parseAs := reflect.New(parseInfo.ParseAs)
@ -220,7 +220,7 @@ func (p *Parser) overwriteStruct(v reflect.Value, fullpath string, path []string
}
case reflect.Ptr:
if field.IsNil() {
field.Set(reflect.New(sf.Type))
field.Set(reflect.New(field.Type().Elem()))
}
}

View file

@ -0,0 +1,70 @@
package configuration
import (
"os"
"reflect"
. "gopkg.in/check.v1"
)
type localConfiguration struct {
Version Version `yaml:"version"`
Log *Log `yaml:"log"`
}
type Log struct {
Formatter string `yaml:"formatter,omitempty"`
}
var expectedConfig = localConfiguration{
Version: "0.1",
Log: &Log{
Formatter: "json",
},
}
type ParserSuite struct{}
var _ = Suite(new(ParserSuite))
func (suite *ParserSuite) TestParserOverwriteIninitializedPoiner(c *C) {
config := localConfiguration{}
os.Setenv("REGISTRY_LOG_FORMATTER", "json")
defer os.Unsetenv("REGISTRY_LOG_FORMATTER")
p := NewParser("registry", []VersionedParseInfo{
{
Version: "0.1",
ParseAs: reflect.TypeOf(config),
ConversionFunc: func(c interface{}) (interface{}, error) {
return c, nil
},
},
})
err := p.Parse([]byte(`{version: "0.1", log: {formatter: "text"}}`), &config)
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, expectedConfig)
}
func (suite *ParserSuite) TestParseOverwriteUnininitializedPoiner(c *C) {
config := localConfiguration{}
os.Setenv("REGISTRY_LOG_FORMATTER", "json")
defer os.Unsetenv("REGISTRY_LOG_FORMATTER")
p := NewParser("registry", []VersionedParseInfo{
{
Version: "0.1",
ParseAs: reflect.TypeOf(config),
ConversionFunc: func(c interface{}) (interface{}, error) {
return c, nil
},
},
})
err := p.Parse([]byte(`{version: "0.1"}`), &config)
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, expectedConfig)
}

View file

@ -70,7 +70,7 @@ to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
Removing intermediate container edb84c2b40cb
Successfully built 74acc70fa106
The commmand outputs its progress until it completes.
The command outputs its progress until it completes.
4. Start your configuration with compose.
@ -133,7 +133,7 @@ to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Content-Type: application/json
< Docker-Distribution-Api-Version: registry/2.0
< Date: Tue, 14 Apr 2015 22:34:13 GMT
< Content-Length: 39

View file

@ -35,7 +35,7 @@ the [release page](https://github.com/docker/golem/releases/tag/v0.1).
#### Running golem with docker
Additionally golem can be run as a docker image requiring no additonal
Additionally golem can be run as a docker image requiring no additional
installation.
`docker run --privileged -v "$GOPATH/src/github.com/docker/distribution/contrib/docker-integration:/test" -w /test distribution/golem golem -rundaemon .`

View file

@ -245,7 +245,7 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h
// Get response context.
ctx, w = dcontext.WithResponseWriter(ctx, w)
challenge.SetHeaders(w)
challenge.SetHeaders(r, w)
handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail(challenge.Error()), w)
dcontext.GetResponseLogger(ctx).Info("get token authentication challenge")

View file

@ -0,0 +1,80 @@
package main
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"errors"
"testing"
"time"
"strings"
"github.com/docker/distribution/registry/auth"
"github.com/docker/libtrust"
)
func TestCreateJWTSuccessWithEmptyACL(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
t.Fatal(err)
}
pk, err := libtrust.FromCryptoPrivateKey(key)
if err != nil {
t.Fatal(err)
}
tokenIssuer := TokenIssuer{
Expiration: time.Duration(100),
Issuer: "localhost",
SigningKey: pk,
}
grantedAccessList := make([]auth.Access, 0)
token, err := tokenIssuer.CreateJWT("test", "test", grantedAccessList)
if err != nil {
t.Fatal(err)
}
tokens := strings.Split(token, ".")
if len(token) == 0 {
t.Fatal("token not generated.")
}
json, err := decodeJWT(tokens[1])
if err != nil {
t.Fatal(err)
}
if !strings.Contains(json, "test") {
t.Fatal("Valid token was not generated.")
}
}
func decodeJWT(rawToken string) (string, error) {
data, err := joseBase64Decode(rawToken)
if err != nil {
return "", errors.New("Error in Decoding base64 String")
}
return data, nil
}
func joseBase64Decode(s string) (string, error) {
switch len(s) % 4 {
case 0:
case 2:
s += "=="
case 3:
s += "="
default:
{
return "", errors.New("Invalid base64 String")
}
}
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err //errors.New("Error in Decoding base64 String")
}
return string(data), nil
}

View file

@ -147,7 +147,8 @@ storage:
endpoint: optional endpoints
internal: optional internal endpoint
bucket: OSS bucket
encrypt: optional data encryption setting
encrypt: optional enable server-side encryption
encryptionkeyid: optional KMS key id for encryption
secure: optional ssl setting
chunksize: optional size valye
rootdirectory: optional root directory
@ -171,6 +172,7 @@ auth:
realm: silly-realm
service: silly-service
token:
autoredirect: true
realm: token-realm
service: token-service
issuer: registry-token-issuer
@ -196,7 +198,7 @@ middleware:
duration: 3000s
ipfilteredby: awsregion
awsregion: us-east-1, use-east-2
updatefrenquency: 12h
updatefrequency: 12h
iprangesurl: https://ip-ranges.amazonaws.com/ip-ranges.json
storage:
- name: redirect
@ -446,7 +448,8 @@ storage:
endpoint: optional endpoints
internal: optional internal endpoint
bucket: OSS bucket
encrypt: optional data encryption setting
encrypt: optional enable server-side encryption
encryptionkeyid: optional KMS key id for encryption
secure: optional ssl setting
chunksize: optional size valye
rootdirectory: optional root directory
@ -623,6 +626,7 @@ security.
| `service` | yes | The service being authenticated. |
| `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. |
| `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. |
| `autoredirect` | no | When set to `true`, `realm` will automatically be set using the Host header of the request as the domain and a path of `/auth/token/`|
For more information about Token based authentication configuration, see the
@ -681,7 +685,7 @@ middleware:
duration: 3000s
ipfilteredby: awsregion
awsregion: us-east-1, use-east-2
updatefrenquency: 12h
updatefrequency: 12h
iprangesurl: https://ip-ranges.amazonaws.com/ip-ranges.json
```
@ -701,15 +705,31 @@ interpretation of the options.
| `baseurl` | yes | The `SCHEME://HOST[/PATH]` at which Cloudfront is served. |
| `privatekey` | yes | The private key for Cloudfront, provided by AWS. |
| `keypairid` | yes | The key pair ID provided by AWS. |
| `duration` | no | An integer and unit for the duration of the Cloudfront session. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`. For example, `3000s` is valid, but `3000 s` is not. If you do not specify a `duration` or you specify an integer without a time unit, the duration defaults to `20m` (20 minutes).|
|`ipfilteredby`|no | A string with the following value `none|aws|awsregion`. |
|`awsregion`|no | A comma separated string of AWS regions, only available when `ipfilteredby` is `awsregion`. For example, `us-east-1, us-west-2`|
|`updatefrenquency`|no | The frequency to update AWS IP regions, default: `12h`|
|`iprangesurl`|no | The URL contains the AWS IP ranges information, default: `https://ip-ranges.amazonaws.com/ip-ranges.json`|
Then value of ipfilteredby:
`none`: default, do not filter by IP
`aws`: IP from AWS goes to S3 directly
`awsregion`: IP from certain AWS regions goes to S3 directly, use together with `awsregion`
| `duration` | no | An integer and unit for the duration of the Cloudfront session. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`. For example, `3000s` is valid, but `3000 s` is not. If you do not specify a `duration` or you specify an integer without a time unit, the duration defaults to `20m` (20 minutes). |
| `ipfilteredby` | no | A string with the following value `none`, `aws` or `awsregion`. |
| `awsregion` | no | A comma separated string of AWS regions, only available when `ipfilteredby` is `awsregion`. For example, `us-east-1, us-west-2` |
| `updatefrequency` | no | The frequency to update AWS IP regions, default: `12h` |
| `iprangesurl` | no | The URL contains the AWS IP ranges information, default: `https://ip-ranges.amazonaws.com/ip-ranges.json` |
Value of `ipfilteredby` can be:
| Value | Description |
|-------------|------------------------------------|
| `none` | default, do not filter by IP |
| `aws` | IP from AWS goes to S3 directly |
| `awsregion` | IP from certain AWS regions goes to S3 directly, use together with `awsregion`. |
### `alicdn`
`alicdn` storage middleware allows the registry to serve layers via a content delivery network provided by Alibaba Cloud. Alicdn requires the OSS storage driver.
| Parameter | Required | Description |
|--------------|----------|-------------------------------------------------------------------------|
| `baseurl` | yes | The `SCHEME://HOST` at which Alicdn is served. |
| `authtype` | yes | The URL authentication type for Alicdn, which should be `a`, `b` or `c`. See the [Authentication configuration](https://www.alibabacloud.com/help/doc-detail/85117.htm).|
| `privatekey` | yes | The URL authentication key for Alicdn. |
| `duration` | no | An integer and unit for the duration of the Alicdn session. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`.|
### `redirect`
@ -775,6 +795,7 @@ http:
clientcas:
- /path/to/ca.pem
- /path/to/another/ca.pem
minimumtls: tls1.0
letsencrypt:
cachefile: /path/to/cache-file
email: emailused@letsencrypt.com
@ -811,8 +832,9 @@ and proxy connections to the registry server.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `certificate` | yes | Absolute path to the x509 certificate file. |
| `key` | yes | Absolute path to the x509 private key file. |
| `clientcas` | no | An array of absolute paths to x509 CA files. |
| `key` | yes | Absolute path to the x509 private key file. |
| `clientcas` | no | An array of absolute paths to x509 CA files. |
| `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2). Defaults to tls1.0 |
### `letsencrypt`
@ -826,7 +848,9 @@ TLS certificates provided by
> to the `docker run` command or using a similar setting in a cloud
> configuration. You should also set the `hosts` option to the list of hostnames
> that are valid for this registry to avoid trying to get certificates for random
> hostnames due to malicious clients connecting with bogus SNI hostnames.
> hostnames due to malicious clients connecting with bogus SNI hostnames. Please
> ensure that you have the `ca-certificates` package installed in order to verify
> letsencrypt certificates.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|

View file

@ -2,6 +2,8 @@
title: "HTTP API V2"
description: "Specification for the Registry API."
keywords: registry, on-prem, images, tags, repository, distribution, api, advanced
redirect_from:
- /reference/api/registry_api/
---
# Docker Registry HTTP API V2
@ -1206,7 +1208,7 @@ The registry does not implement the V2 API.
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1244,7 +1246,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1316,7 +1318,7 @@ The following parameters should be specified on the request:
```
200 OK
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"name": <name>,
@ -1344,7 +1346,7 @@ The following headers will be returned with the response:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1382,7 +1384,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1419,7 +1421,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1456,7 +1458,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1514,7 +1516,7 @@ The following parameters should be specified on the request:
200 OK
Content-Length: <length>
Link: <<url>?n=<last n value>&last=<last entry from response>>; rel="next"
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"name": <name>,
@ -1543,7 +1545,7 @@ The following headers will be returned with the response:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1581,7 +1583,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1618,7 +1620,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1655,7 +1657,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1759,7 +1761,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1792,7 +1794,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1830,7 +1832,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1867,7 +1869,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -1904,7 +1906,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2005,7 +2007,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2041,7 +2043,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2079,7 +2081,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2116,7 +2118,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2153,7 +2155,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2189,7 +2191,7 @@ The error codes that may be included in the response body are enumerated below:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [{
@ -2277,7 +2279,7 @@ The following parameters should be specified on the request:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2310,7 +2312,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2348,7 +2350,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2385,7 +2387,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2422,7 +2424,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2458,7 +2460,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2583,7 +2585,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2614,7 +2616,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2647,7 +2649,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2685,7 +2687,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2722,7 +2724,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2759,7 +2761,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2843,7 +2845,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2874,7 +2876,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2917,7 +2919,7 @@ The range specification cannot be satisfied for the requested content. This can
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2955,7 +2957,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -2992,7 +2994,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3029,7 +3031,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3132,7 +3134,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3163,7 +3165,7 @@ The error codes that may be included in the response body are enumerated below:
```
405 Method Not Allowed
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3195,7 +3197,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3233,7 +3235,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3270,7 +3272,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3307,7 +3309,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3445,7 +3447,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3483,7 +3485,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3520,7 +3522,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3557,7 +3559,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3662,7 +3664,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3700,7 +3702,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3737,7 +3739,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3774,7 +3776,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3897,7 +3899,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3935,7 +3937,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -3972,7 +3974,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4009,7 +4011,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4102,7 +4104,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4134,7 +4136,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4166,7 +4168,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4204,7 +4206,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4241,7 +4243,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4278,7 +4280,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4370,7 +4372,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4402,7 +4404,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4434,7 +4436,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4472,7 +4474,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4509,7 +4511,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4546,7 +4548,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4636,7 +4638,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4668,7 +4670,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4710,7 +4712,7 @@ The `Content-Range` specification cannot be accepted, either because it does not
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4748,7 +4750,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4785,7 +4787,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4822,7 +4824,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4916,7 +4918,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4949,7 +4951,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -4981,7 +4983,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5019,7 +5021,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5056,7 +5058,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5093,7 +5095,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5177,7 +5179,7 @@ The following headers will be returned with the response:
```
400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5208,7 +5210,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5240,7 +5242,7 @@ The error codes that may be included in the response body are enumerated below:
401 Unauthorized
WWW-Authenticate: <scheme> realm="<realm>", ..."
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5278,7 +5280,7 @@ The error codes that may be included in the response body are enumerated below:
```
404 Not Found
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5315,7 +5317,7 @@ The error codes that may be included in the response body are enumerated below:
```
403 Forbidden
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5352,7 +5354,7 @@ The error codes that may be included in the response body are enumerated below:
```
429 Too Many Requests
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"errors:" [
@ -5414,7 +5416,7 @@ Request an unabridged list of repositories available. The implementation may im
```
200 OK
Content-Length: <length>
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"repositories": [
@ -5459,7 +5461,7 @@ The following parameters should be specified on the request:
200 OK
Content-Length: <length>
Link: <<url>?n=<last n value>&last=<last entry from response>>; rel="next"
Content-Type: application/json; charset=utf-8
Content-Type: application/json
{
"repositories": [

View file

@ -2,6 +2,8 @@
title: "HTTP API V2"
description: "Specification for the Registry API."
keywords: registry, on-prem, images, tags, repository, distribution, api, advanced
redirect_from:
- /reference/api/registry_api/
---
# Docker Registry HTTP API V2

View file

@ -60,7 +60,7 @@ return this response:
```
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Content-Type: application/json
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull,push"
Date: Thu, 10 Sep 2015 19:32:31 GMT

View file

@ -0,0 +1,41 @@
---
title: Update deprecated schema image manifest version 2, v1 images
description: Update deprecated schema v1 iamges
keywords: registry, on-prem, images, tags, repository, distribution, api, advanced, manifest
---
## Image manifest version 2, schema 1
With the release of image manifest version 2, schema 2, image manifest version
2, schema 1 has been deprecated. This could lead to compatibility and
vulnerability issues in images that haven't been updated to image manifest
version 2, schema 2.
This page contains information on how to update from image manifest version 2,
schema 1. However, these instructions will not ensure your new image will run
successfully. There may be several other issues to troubleshoot that are
associated with the deprecated image manifest that will block your image from
running succesfully. A list of possible methods to help update your image is
also included below.
### Update to image manifest version 2, schema 2
One way to upgrade an image from image manifest version 2, schema 1 to
schema 2 is to `docker pull` the image and then `docker push` the image with a
current version of Docker. Doing so will automatically convert the image to use
the latest image manifest specification.
Converting an image to image manifest version 2, schema 2 converts the
manifest format, but does not update the contents within the image. Images
using manifest version 2, schema 1 may contain unpatched vulnerabilities. We
recommend looking for an alternative image or rebuilding it.
### Update FROM statement
You can rebuild the image by updating the `FROM` statement in your
`Dockerfile`. If your image manifest is out-of-date, there is a chance the
image pulled from your `FROM` statement in your `Dockerfile` is also
out-of-date. See the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/#from)
and the [Dockerfile best practices guide](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
for more information on how to update the `FROM` statement in your
`Dockerfile`.

47
go.mod Normal file
View file

@ -0,0 +1,47 @@
module github.com/docker/distribution
go 1.12
require (
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible
github.com/Azure/go-autorest v10.8.1+incompatible // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d
github.com/aws/aws-sdk-go v1.34.9
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/bshuster-repo/logrus-logstash-hook v0.4.1
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c // indirect
github.com/dnaeon/go-vcr v1.0.1 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/docker/go-metrics v0.0.1
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33
github.com/gorilla/mux v1.7.2
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/marstr/guid v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f // indirect
github.com/ncw/swift v1.0.47
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.1
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a // indirect
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789
gopkg.in/yaml.v2 v2.2.2
)

176
go.sum Normal file
View file

@ -0,0 +1,176 @@
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-autorest v10.8.1+incompatible h1:u0jVQf+a6k6x8A+sT60l6EY9XZu+kHdnZVPAYqpVRo0=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aws/aws-sdk-go v1.34.9 h1:cUGBW9CVdi0mS7K1hDzxIqTpfeWhpoQiguq81M1tjK0=
github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c h1:KJAnOBuY9cTKVqB5cfbynpvFgeHRTREkRk8C977oFu4=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33 h1:893HsJqtxp9z1SF76gg6hY70hRY1wVlTSnC/h1yUDCo=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI=
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncw/swift v1.0.47 h1:4DQRPj35Y41WogBxyhOXlrI37nzGlyEcsforeudyYPQ=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff h1:mk5zS3XLqVUzdF/CQCZ5ERujSF/8JFo+Wpkp/5I93NA=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a h1:zo0EaRwJM6T5UQ+QEt2dDSgEmbFJ4pZr/Rzsjpu7zgI=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789 h1:NMiUjDZiD6qDVeBOzpImftxXzQHCp2Y2QLdmaqU9MRk=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -14,7 +14,7 @@ var (
// DownHandler registers a manual_http_status that always returns an Error
func DownHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
updater.Update(errors.New("Manual Check"))
updater.Update(errors.New("manual Check"))
} else {
w.WriteHeader(http.StatusNotFound)
}

View file

@ -291,7 +291,7 @@ func statusResponse(w http.ResponseWriter, r *http.Request, status int, checks m
}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprint(len(p)))
w.WriteHeader(status)
if _, err := w.Write(p); err != nil {

View file

@ -8,7 +8,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
@ -163,7 +163,7 @@ func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType st
},
}
m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors))
m.Manifests = make([]ManifestDescriptor, len(descriptors))
copy(m.Manifests, descriptors)
deserialized := DeserializedManifestList{
@ -177,7 +177,7 @@ func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType st
// UnmarshalJSON populates a new ManifestList struct from JSON data.
func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b), len(b))
m.canonical = make([]byte, len(b))
// store manifest list in canonical
copy(m.canonical, b)

View file

@ -7,7 +7,7 @@ import (
"testing"
"github.com/docker/distribution"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var expectedManifestListSerialization = []byte(`{

View file

@ -7,7 +7,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// Builder is a type for constructing manifests.
@ -48,7 +48,7 @@ func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotati
// valid media type for oci image manifests currently: "" or "application/vnd.oci.image.manifest.v1+json"
func (mb *Builder) SetMediaType(mediaType string) error {
if mediaType != "" && mediaType != v1.MediaTypeImageManifest {
return errors.New("Invalid media type for OCI image manifest")
return errors.New("invalid media type for OCI image manifest")
}
mb.mediaType = mediaType

View file

@ -7,7 +7,7 @@ import (
"github.com/docker/distribution"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type mockBlobService struct {

View file

@ -8,7 +8,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
@ -87,7 +87,7 @@ func FromStruct(m Manifest) (*DeserializedManifest, error) {
// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b), len(b))
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)

View file

@ -8,7 +8,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
var expectedManifestSerialization = []byte(`{

View file

@ -108,7 +108,7 @@ type SignedManifest struct {
// UnmarshalJSON populates a new SignedManifest struct from JSON data.
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
sm.all = make([]byte, len(b), len(b))
sm.all = make([]byte, len(b))
// store manifest and signatures in all
copy(sm.all, b)
@ -124,7 +124,7 @@ func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
}
// sm.Canonical stores the canonical manifest JSON
sm.Canonical = make([]byte, len(bytes), len(bytes))
sm.Canonical = make([]byte, len(bytes))
copy(sm.Canonical, bytes)
// Unmarshal canonical JSON into Manifest object

View file

@ -58,7 +58,7 @@ func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Man
func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
r, ok := d.(Reference)
if !ok {
return fmt.Errorf("Unable to add non-reference type to v1 builder")
return fmt.Errorf("unable to add non-reference type to v1 builder")
}
// Entries need to be prepended

View file

@ -106,7 +106,7 @@ func FromStruct(m Manifest) (*DeserializedManifest, error) {
// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b), len(b))
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)

View file

@ -87,7 +87,7 @@ func ManifestMediaTypes() (mediaTypes []string) {
// UnmarshalFunc implements manifest unmarshalling a given MediaType
type UnmarshalFunc func([]byte) (Manifest, Descriptor, error)
var mappings = make(map[string]UnmarshalFunc, 0)
var mappings = make(map[string]UnmarshalFunc)
// UnmarshalManifest looks up manifest unmarshal functions based on
// MediaType

View file

@ -10,4 +10,7 @@ const (
var (
// StorageNamespace is the prometheus namespace of blob/cache related operations
StorageNamespace = metrics.NewNamespace(NamespacePrefix, "storage", nil)
// NotificationsNamespace is the prometheus namespace of notification related metrics
NotificationsNamespace = metrics.NewNamespace(NamespacePrefix, "notifications", nil)
)

View file

@ -8,6 +8,7 @@ import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/uuid"
events "github.com/docker/go-events"
"github.com/opencontainers/go-digest"
)
@ -17,7 +18,7 @@ type bridge struct {
actor ActorRecord
source SourceRecord
request RequestRecord
sink Sink
sink events.Sink
}
var _ Listener = &bridge{}
@ -32,7 +33,7 @@ type URLBuilder interface {
// using the actor and source. Any urls populated in the events created by
// this bridge will be created using the URLBuilder.
// TODO(stevvooe): Update this to simply take a context.Context object.
func NewBridge(ub URLBuilder, source SourceRecord, actor ActorRecord, request RequestRecord, sink Sink, includeReferences bool) Listener {
func NewBridge(ub URLBuilder, source SourceRecord, actor ActorRecord, request RequestRecord, sink events.Sink, includeReferences bool) Listener {
return &bridge{
ub: ub,
includeReferences: includeReferences,
@ -125,15 +126,6 @@ func (b *bridge) RepoDeleted(repo reference.Named) error {
return b.sink.Write(*event)
}
func (b *bridge) createManifestEventAndWrite(action string, repo reference.Named, sm distribution.Manifest) error {
manifestEvent, err := b.createManifestEvent(action, repo, sm)
if err != nil {
return err
}
return b.sink.Write(*manifestEvent)
}
func (b *bridge) createManifestDeleteEventAndWrite(action string, repo reference.Named, dgst digest.Digest) error {
event := b.createEvent(action)
event.Target.Repository = repo.Name()

View file

@ -6,8 +6,9 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/uuid"
events "github.com/docker/go-events"
"github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
)
@ -46,8 +47,8 @@ var (
)
func TestEventBridgeManifestPulled(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkCommonManifest(t, EventActionPull, events...)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkCommonManifest(t, EventActionPull, event)
return nil
}))
@ -59,8 +60,8 @@ func TestEventBridgeManifestPulled(t *testing.T) {
}
func TestEventBridgeManifestPushed(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkCommonManifest(t, EventActionPush, events...)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkCommonManifest(t, EventActionPush, event)
return nil
}))
@ -72,10 +73,10 @@ func TestEventBridgeManifestPushed(t *testing.T) {
}
func TestEventBridgeManifestPushedWithTag(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkCommonManifest(t, EventActionPush, events...)
if events[0].Target.Tag != "latest" {
t.Fatalf("missing or unexpected tag: %#v", events[0].Target)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkCommonManifest(t, EventActionPush, event)
if event.(Event).Target.Tag != "latest" {
t.Fatalf("missing or unexpected tag: %#v", event.(Event).Target)
}
return nil
@ -88,10 +89,10 @@ func TestEventBridgeManifestPushedWithTag(t *testing.T) {
}
func TestEventBridgeManifestPulledWithTag(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkCommonManifest(t, EventActionPull, events...)
if events[0].Target.Tag != "latest" {
t.Fatalf("missing or unexpected tag: %#v", events[0].Target)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkCommonManifest(t, EventActionPull, event)
if event.(Event).Target.Tag != "latest" {
t.Fatalf("missing or unexpected tag: %#v", event.(Event).Target)
}
return nil
@ -104,10 +105,10 @@ func TestEventBridgeManifestPulledWithTag(t *testing.T) {
}
func TestEventBridgeManifestDeleted(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkDeleted(t, EventActionDelete, events...)
if events[0].Target.Digest != dgst {
t.Fatalf("unexpected digest on event target: %q != %q", events[0].Target.Digest, dgst)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkDeleted(t, EventActionDelete, event)
if event.(Event).Target.Digest != dgst {
t.Fatalf("unexpected digest on event target: %q != %q", event.(Event).Target.Digest, dgst)
}
return nil
}))
@ -119,10 +120,10 @@ func TestEventBridgeManifestDeleted(t *testing.T) {
}
func TestEventBridgeTagDeleted(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkDeleted(t, EventActionDelete, events...)
if events[0].Target.Tag != m.Tag {
t.Fatalf("unexpected tag on event target: %q != %q", events[0].Target.Tag, m.Tag)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkDeleted(t, EventActionDelete, event)
if event.(Event).Target.Tag != m.Tag {
t.Fatalf("unexpected tag on event target: %q != %q", event.(Event).Target.Tag, m.Tag)
}
return nil
}))
@ -134,8 +135,8 @@ func TestEventBridgeTagDeleted(t *testing.T) {
}
func TestEventBridgeRepoDeleted(t *testing.T) {
l := createTestEnv(t, testSinkFn(func(events ...Event) error {
checkDeleted(t, EventActionDelete, events...)
l := createTestEnv(t, testSinkFn(func(event events.Event) error {
checkDeleted(t, EventActionDelete, event)
return nil
}))
@ -162,36 +163,29 @@ func createTestEnv(t *testing.T, fn testSinkFn) Listener {
return NewBridge(ub, source, actor, request, fn, true)
}
func checkDeleted(t *testing.T, action string, events ...Event) {
if len(events) != 1 {
t.Fatalf("unexpected number of events: %v != 1", len(events))
func checkDeleted(t *testing.T, action string, event events.Event) {
if event.(Event).Source != source {
t.Fatalf("source not equal: %#v != %#v", event.(Event).Source, source)
}
event := events[0]
if event.Source != source {
t.Fatalf("source not equal: %#v != %#v", event.Source, source)
if event.(Event).Request != request {
t.Fatalf("request not equal: %#v != %#v", event.(Event).Request, request)
}
if event.Request != request {
t.Fatalf("request not equal: %#v != %#v", event.Request, request)
if event.(Event).Actor != actor {
t.Fatalf("request not equal: %#v != %#v", event.(Event).Actor, actor)
}
if event.Actor != actor {
t.Fatalf("request not equal: %#v != %#v", event.Actor, actor)
}
if event.Target.Repository != repo {
t.Fatalf("unexpected repository: %q != %q", event.Target.Repository, repo)
if event.(Event).Target.Repository != repo {
t.Fatalf("unexpected repository: %q != %q", event.(Event).Target.Repository, repo)
}
}
func checkCommonManifest(t *testing.T, action string, events ...Event) {
checkCommon(t, events...)
func checkCommonManifest(t *testing.T, action string, event events.Event) {
checkCommon(t, event)
event := events[0]
if event.Action != action {
t.Fatalf("unexpected event action: %q != %q", event.Action, action)
if event.(Event).Action != action {
t.Fatalf("unexpected event action: %q != %q", event.(Event).Action, action)
}
repoRef, _ := reference.WithName(repo)
@ -201,57 +195,51 @@ func checkCommonManifest(t *testing.T, action string, events ...Event) {
t.Fatalf("error building expected url: %v", err)
}
if event.Target.URL != u {
t.Fatalf("incorrect url passed: \n%q != \n%q", event.Target.URL, u)
if event.(Event).Target.URL != u {
t.Fatalf("incorrect url passed: \n%q != \n%q", event.(Event).Target.URL, u)
}
if len(event.Target.References) != len(layers) {
t.Fatalf("unexpected number of references %v != %v", len(event.Target.References), len(layers))
if len(event.(Event).Target.References) != len(layers) {
t.Fatalf("unexpected number of references %v != %v", len(event.(Event).Target.References), len(layers))
}
for i, targetReference := range event.Target.References {
for i, targetReference := range event.(Event).Target.References {
if targetReference.Digest != layers[i].BlobSum {
t.Fatalf("unexpected reference: %q != %q", targetReference.Digest, layers[i].BlobSum)
}
}
}
func checkCommon(t *testing.T, events ...Event) {
if len(events) != 1 {
t.Fatalf("unexpected number of events: %v != 1", len(events))
func checkCommon(t *testing.T, event events.Event) {
if event.(Event).Source != source {
t.Fatalf("source not equal: %#v != %#v", event.(Event).Source, source)
}
event := events[0]
if event.Source != source {
t.Fatalf("source not equal: %#v != %#v", event.Source, source)
if event.(Event).Request != request {
t.Fatalf("request not equal: %#v != %#v", event.(Event).Request, request)
}
if event.Request != request {
t.Fatalf("request not equal: %#v != %#v", event.Request, request)
if event.(Event).Actor != actor {
t.Fatalf("request not equal: %#v != %#v", event.(Event).Actor, actor)
}
if event.Actor != actor {
t.Fatalf("request not equal: %#v != %#v", event.Actor, actor)
if event.(Event).Target.Digest != dgst {
t.Fatalf("unexpected digest on event target: %q != %q", event.(Event).Target.Digest, dgst)
}
if event.Target.Digest != dgst {
t.Fatalf("unexpected digest on event target: %q != %q", event.Target.Digest, dgst)
if event.(Event).Target.Length != int64(len(payload)) {
t.Fatalf("unexpected target length: %v != %v", event.(Event).Target.Length, len(payload))
}
if event.Target.Length != int64(len(payload)) {
t.Fatalf("unexpected target length: %v != %v", event.Target.Length, len(payload))
}
if event.Target.Repository != repo {
t.Fatalf("unexpected repository: %q != %q", event.Target.Repository, repo)
if event.(Event).Target.Repository != repo {
t.Fatalf("unexpected repository: %q != %q", event.(Event).Target.Repository, repo)
}
}
type testSinkFn func(events ...Event) error
type testSinkFn func(event events.Event) error
func (tsf testSinkFn) Write(events ...Event) error {
return tsf(events...)
func (tsf testSinkFn) Write(event events.Event) error {
return tsf(event)
}
func (tsf testSinkFn) Close() error { return nil }

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/docker/distribution/configuration"
events "github.com/docker/go-events"
)
// EndpointConfig covers the optional configuration parameters for an active
@ -42,7 +43,7 @@ func (ec *EndpointConfig) defaults() {
// services when events are written. Writes are non-blocking and always
// succeed for callers but events may be queued internally.
type Endpoint struct {
Sink
events.Sink
url string
name string
@ -58,13 +59,13 @@ func NewEndpoint(name, url string, config EndpointConfig) *Endpoint {
endpoint.url = url
endpoint.EndpointConfig = config
endpoint.defaults()
endpoint.metrics = newSafeMetrics()
endpoint.metrics = newSafeMetrics(name)
// Configures the inmemory queue, retry, http pipeline.
endpoint.Sink = newHTTPSink(
endpoint.url, endpoint.Timeout, endpoint.Headers,
endpoint.Transport, endpoint.metrics.httpStatusListener())
endpoint.Sink = newRetryingSink(endpoint.Sink, endpoint.Threshold, endpoint.Backoff)
endpoint.Sink = events.NewRetryingSink(endpoint.Sink, events.NewBreaker(endpoint.Threshold, endpoint.Backoff))
endpoint.Sink = newEventQueue(endpoint.Sink, endpoint.metrics.eventQueueListener())
mediaTypes := append(config.Ignore.MediaTypes, config.IgnoredMediaTypes...)
endpoint.Sink = newIgnoredSink(endpoint.Sink, mediaTypes, config.Ignore.Actions)

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/docker/distribution"
events "github.com/docker/go-events"
)
// EventAction constants used in action field of Event.
@ -30,7 +31,7 @@ const (
type Envelope struct {
// Events make up the contents of the envelope. Events present in a single
// envelope are not necessarily related.
Events []Event `json:"events,omitempty"`
Events []events.Event `json:"events,omitempty"`
}
// TODO(stevvooe): The event type should be separate from the json format. It
@ -148,16 +149,3 @@ var (
// retries will not be successful.
ErrSinkClosed = fmt.Errorf("sink: closed")
)
// Sink accepts and sends events.
type Sink interface {
// Write writes one or more events to the sink. If no error is returned,
// the caller will assume that all events have been committed and will not
// try to send them again. If an error is received, the caller may retry
// sending the event. The caller should cede the slice of memory to the
// sink and not modify it after calling this method.
Write(events ...Event) error
// Close the sink, possibly waiting for pending events to flush.
Close() error
}

View file

@ -114,8 +114,7 @@ func TestEventEnvelopeJSONFormat(t *testing.T) {
prototype.Request.UserAgent = "test/0.1"
prototype.Source.Addr = "hostname.local:port"
var manifestPush Event
manifestPush = prototype
var manifestPush = prototype
manifestPush.ID = "asdf-asdf-asdf-asdf-0"
manifestPush.Target.Digest = "sha256:0123456789abcdef0"
manifestPush.Target.Length = 1
@ -124,8 +123,7 @@ func TestEventEnvelopeJSONFormat(t *testing.T) {
manifestPush.Target.Repository = "library/test"
manifestPush.Target.URL = "http://example.com/v2/library/test/manifests/latest"
var layerPush0 Event
layerPush0 = prototype
var layerPush0 = prototype
layerPush0.ID = "asdf-asdf-asdf-asdf-1"
layerPush0.Target.Digest = "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"
layerPush0.Target.Length = 2
@ -134,8 +132,7 @@ func TestEventEnvelopeJSONFormat(t *testing.T) {
layerPush0.Target.Repository = "library/test"
layerPush0.Target.URL = "http://example.com/v2/library/test/manifests/latest"
var layerPush1 Event
layerPush1 = prototype
var layerPush1 = prototype
layerPush1.ID = "asdf-asdf-asdf-asdf-2"
layerPush1.Target.Digest = "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d6"
layerPush1.Target.Length = 3

View file

@ -7,6 +7,8 @@ import (
"net/http"
"sync"
"time"
events "github.com/docker/go-events"
)
// httpSink implements a single-flight, http notification endpoint. This is
@ -45,15 +47,15 @@ func newHTTPSink(u string, timeout time.Duration, headers http.Header, transport
// httpStatusListener is called on various outcomes of sending notifications.
type httpStatusListener interface {
success(status int, events ...Event)
failure(status int, events ...Event)
err(err error, events ...Event)
success(status int, event events.Event)
failure(status int, events events.Event)
err(err error, events events.Event)
}
// Accept makes an attempt to notify the endpoint, returning an error if it
// fails. It is the caller's responsibility to retry on error. The events are
// accepted or rejected as a group.
func (hs *httpSink) Write(events ...Event) error {
func (hs *httpSink) Write(event events.Event) error {
hs.mu.Lock()
defer hs.mu.Unlock()
defer hs.client.Transport.(*headerRoundTripper).CloseIdleConnections()
@ -63,7 +65,7 @@ func (hs *httpSink) Write(events ...Event) error {
}
envelope := Envelope{
Events: events,
Events: []events.Event{event},
}
// TODO(stevvooe): It is not ideal to keep re-encoding the request body on
@ -73,7 +75,7 @@ func (hs *httpSink) Write(events ...Event) error {
p, err := json.MarshalIndent(envelope, "", " ")
if err != nil {
for _, listener := range hs.listeners {
listener.err(err, events...)
listener.err(err, event)
}
return fmt.Errorf("%v: error marshaling event envelope: %v", hs, err)
}
@ -82,7 +84,7 @@ func (hs *httpSink) Write(events ...Event) error {
resp, err := hs.client.Post(hs.url, EventsMediaType, body)
if err != nil {
for _, listener := range hs.listeners {
listener.err(err, events...)
listener.err(err, event)
}
return fmt.Errorf("%v: error posting: %v", hs, err)
@ -94,7 +96,7 @@ func (hs *httpSink) Write(events ...Event) error {
switch {
case resp.StatusCode >= 200 && resp.StatusCode < 400:
for _, listener := range hs.listeners {
listener.success(resp.StatusCode, events...)
listener.success(resp.StatusCode, event)
}
// TODO(stevvooe): This is a little accepting: we may want to support
@ -104,7 +106,7 @@ func (hs *httpSink) Write(events ...Event) error {
return nil
default:
for _, listener := range hs.listeners {
listener.failure(resp.StatusCode, events...)
listener.failure(resp.StatusCode, event)
}
return fmt.Errorf("%v: response status %v unaccepted", hs, resp.Status)
}
@ -133,8 +135,7 @@ type headerRoundTripper struct {
}
func (hrt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
var nreq http.Request
nreq = *req
var nreq = *req
nreq.Header = make(http.Header)
merge := func(headers http.Header) {

View file

@ -14,6 +14,7 @@ import (
"testing"
"github.com/docker/distribution/manifest/schema1"
events "github.com/docker/go-events"
)
// TestHTTPSink mocks out an http endpoint and notifies it under a couple of
@ -63,14 +64,14 @@ func TestHTTPSink(t *testing.T) {
})
server := httptest.NewTLSServer(serverHandler)
metrics := newSafeMetrics()
metrics := newSafeMetrics("")
sink := newHTTPSink(server.URL, 0, nil, nil,
&endpointMetricsHTTPStatusListener{safeMetrics: metrics})
// first make sure that the default transport gives x509 untrusted cert error
events := []Event{}
err := sink.Write(events...)
if !strings.Contains(err.Error(), "x509") {
event := Event{}
err := sink.Write(event)
if !strings.Contains(err.Error(), "x509") && !strings.Contains(err.Error(), "unknown ca") {
t.Fatal("TLS server with default transport should give unknown CA error")
}
if err := sink.Close(); err != nil {
@ -83,12 +84,13 @@ func TestHTTPSink(t *testing.T) {
}
sink = newHTTPSink(server.URL, 0, nil, tr,
&endpointMetricsHTTPStatusListener{safeMetrics: metrics})
err = sink.Write(events...)
err = sink.Write(event)
if err != nil {
t.Fatalf("unexpected error writing events: %v", err)
t.Fatalf("unexpected error writing event: %v", err)
}
// reset server to standard http server and sink to a basic sink
metrics = newSafeMetrics("")
server = httptest.NewServer(serverHandler)
sink = newHTTPSink(server.URL, 0, nil, nil,
&endpointMetricsHTTPStatusListener{safeMetrics: metrics})
@ -111,46 +113,52 @@ func TestHTTPSink(t *testing.T) {
}()
for _, tc := range []struct {
events []Event // events to send
event events.Event // events to send
url string
failure bool // true if there should be a failure.
isFailure bool // true if there should be a failure.
isError bool // true if the request returns an error
statusCode int // if not set, no status code should be incremented.
}{
{
statusCode: http.StatusOK,
events: []Event{
createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest)},
event: createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest),
},
{
statusCode: http.StatusOK,
events: []Event{
createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest),
createTestEvent("push", "library/test", layerMediaType),
createTestEvent("push", "library/test", layerMediaType),
},
event: createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest),
},
{
statusCode: http.StatusOK,
event: createTestEvent("push", "library/test", layerMediaType),
},
{
statusCode: http.StatusOK,
event: createTestEvent("push", "library/test", layerMediaType),
},
{
statusCode: http.StatusTemporaryRedirect,
},
{
statusCode: http.StatusBadRequest,
failure: true,
isFailure: true,
},
{
// Case where connection is immediately closed
url: closeL.Addr().String(),
failure: true,
url: "http://" + closeL.Addr().String(),
isError: true,
},
} {
if tc.failure {
expectedMetrics.Failures += len(tc.events)
if tc.isFailure {
expectedMetrics.Failures++
} else if tc.isError {
expectedMetrics.Errors++
} else {
expectedMetrics.Successes += len(tc.events)
expectedMetrics.Successes++
}
if tc.statusCode > 0 {
expectedMetrics.Statuses[fmt.Sprintf("%d %s", tc.statusCode, http.StatusText(tc.statusCode))] += len(tc.events)
expectedMetrics.Statuses[fmt.Sprintf("%d %s", tc.statusCode, http.StatusText(tc.statusCode))]++
}
url := tc.url
@ -161,11 +169,11 @@ func TestHTTPSink(t *testing.T) {
url += fmt.Sprintf("?status=%v", tc.statusCode)
sink.url = url
t.Logf("testcase: %v, fail=%v", url, tc.failure)
t.Logf("testcase: %v, fail=%v, error=%v", url, tc.isFailure, tc.isError)
// Try a simple event emission.
err := sink.Write(tc.events...)
err := sink.Write(tc.event)
if !tc.failure {
if !tc.isFailure && !tc.isError {
if err != nil {
t.Fatalf("unexpected error send event: %v", err)
}
@ -173,6 +181,7 @@ func TestHTTPSink(t *testing.T) {
if err == nil {
t.Fatalf("the endpoint should have rejected the request")
}
t.Logf("write error: %v", err)
}
if !reflect.DeepEqual(metrics.EndpointMetrics, expectedMetrics) {

View file

@ -136,11 +136,10 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository, r
var blobDigests []digest.Digest
blobs := repository.Blobs(ctx)
for i := 0; i < 2; i++ {
rs, ds, err := testutil.CreateRandomTarFile()
rs, dgst, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating test layer: %v", err)
}
dgst := digest.Digest(ds)
blobDigests = append(blobDigests, dgst)
wr, err := blobs.Create(ctx)

View file

@ -5,6 +5,19 @@ import (
"fmt"
"net/http"
"sync"
prometheus "github.com/docker/distribution/metrics"
events "github.com/docker/go-events"
"github.com/docker/go-metrics"
)
var (
// eventsCounter counts total events of incoming, success, failure, and errors
eventsCounter = prometheus.NotificationsNamespace.NewLabeledCounter("events", "The number of total events", "type", "endpoint")
// pendingGauge measures the pending queue size
pendingGauge = prometheus.NotificationsNamespace.NewLabeledGauge("pending", "The gauge of pending events in queue", metrics.Total, "endpoint")
// statusCounter counts the total notification call per each status code
statusCounter = prometheus.NotificationsNamespace.NewLabeledCounter("status", "The number of status code", "code", "endpoint")
)
// EndpointMetrics track various actions taken by the endpoint, typically by
@ -22,14 +35,16 @@ type EndpointMetrics struct {
// safeMetrics guards the metrics implementation with a lock and provides a
// safe update function.
type safeMetrics struct {
EndpointName string
EndpointMetrics
sync.Mutex // protects statuses map
}
// newSafeMetrics returns safeMetrics with map allocated.
func newSafeMetrics() *safeMetrics {
func newSafeMetrics(name string) *safeMetrics {
var sm safeMetrics
sm.Statuses = make(map[string]int)
sm.EndpointName = name
return &sm
}
@ -56,24 +71,32 @@ type endpointMetricsHTTPStatusListener struct {
var _ httpStatusListener = &endpointMetricsHTTPStatusListener{}
func (emsl *endpointMetricsHTTPStatusListener) success(status int, events ...Event) {
func (emsl *endpointMetricsHTTPStatusListener) success(status int, event events.Event) {
emsl.safeMetrics.Lock()
defer emsl.safeMetrics.Unlock()
emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))] += len(events)
emsl.Successes += len(events)
emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))]++
emsl.Successes++
statusCounter.WithValues(fmt.Sprintf("%d %s", status, http.StatusText(status)), emsl.EndpointName).Inc(1)
eventsCounter.WithValues("Successes", emsl.EndpointName).Inc(1)
}
func (emsl *endpointMetricsHTTPStatusListener) failure(status int, events ...Event) {
func (emsl *endpointMetricsHTTPStatusListener) failure(status int, event events.Event) {
emsl.safeMetrics.Lock()
defer emsl.safeMetrics.Unlock()
emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))] += len(events)
emsl.Failures += len(events)
emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))]++
emsl.Failures++
statusCounter.WithValues(fmt.Sprintf("%d %s", status, http.StatusText(status)), emsl.EndpointName).Inc(1)
eventsCounter.WithValues("Failures", emsl.EndpointName).Inc(1)
}
func (emsl *endpointMetricsHTTPStatusListener) err(err error, events ...Event) {
func (emsl *endpointMetricsHTTPStatusListener) err(err error, event events.Event) {
emsl.safeMetrics.Lock()
defer emsl.safeMetrics.Unlock()
emsl.Errors += len(events)
emsl.Errors++
eventsCounter.WithValues("Errors", emsl.EndpointName).Inc(1)
}
// endpointMetricsEventQueueListener maintains the incoming events counter and
@ -82,17 +105,22 @@ type endpointMetricsEventQueueListener struct {
*safeMetrics
}
func (eqc *endpointMetricsEventQueueListener) ingress(events ...Event) {
func (eqc *endpointMetricsEventQueueListener) ingress(event events.Event) {
eqc.Lock()
defer eqc.Unlock()
eqc.Events += len(events)
eqc.Pending += len(events)
eqc.Events++
eqc.Pending++
eventsCounter.WithValues("Events", eqc.EndpointName).Inc()
pendingGauge.WithValues(eqc.EndpointName).Inc(1)
}
func (eqc *endpointMetricsEventQueueListener) egress(events ...Event) {
func (eqc *endpointMetricsEventQueueListener) egress(event events.Event) {
eqc.Lock()
defer eqc.Unlock()
eqc.Pending -= len(events)
eqc.Pending--
pendingGauge.WithValues(eqc.EndpointName).Dec(1)
}
// endpoints is global registry of endpoints used to report metrics to expvar
@ -149,4 +177,7 @@ func init() {
}))
registry.(*expvar.Map).Set("notifications", &notifications)
// register prometheus metrics
metrics.Register(prometheus.NotificationsNamespace)
}

View file

@ -4,107 +4,16 @@ import (
"container/list"
"fmt"
"sync"
"time"
events "github.com/docker/go-events"
"github.com/sirupsen/logrus"
)
// NOTE(stevvooe): This file contains definitions for several utility sinks.
// Typically, the broadcaster is the only sink that should be required
// externally, but others are suitable for export if the need arises. Albeit,
// the tight integration with endpoint metrics should be removed.
// Broadcaster sends events to multiple, reliable Sinks. The goal of this
// component is to dispatch events to configured endpoints. Reliability can be
// provided by wrapping incoming sinks.
type Broadcaster struct {
sinks []Sink
events chan []Event
closed chan chan struct{}
}
// NewBroadcaster ...
// Add appends one or more sinks to the list of sinks. The broadcaster
// behavior will be affected by the properties of the sink. Generally, the
// sink should accept all messages and deal with reliability on its own. Use
// of EventQueue and RetryingSink should be used here.
func NewBroadcaster(sinks ...Sink) *Broadcaster {
b := Broadcaster{
sinks: sinks,
events: make(chan []Event),
closed: make(chan chan struct{}),
}
// Start the broadcaster
go b.run()
return &b
}
// Write accepts a block of events to be dispatched to all sinks. This method
// will never fail and should never block (hopefully!). The caller cedes the
// slice memory to the broadcaster and should not modify it after calling
// write.
func (b *Broadcaster) Write(events ...Event) error {
select {
case b.events <- events:
case <-b.closed:
return ErrSinkClosed
}
return nil
}
// Close the broadcaster, ensuring that all messages are flushed to the
// underlying sink before returning.
func (b *Broadcaster) Close() error {
logrus.Infof("broadcaster: closing")
select {
case <-b.closed:
// already closed
return fmt.Errorf("broadcaster: already closed")
default:
// do a little chan handoff dance to synchronize closing
closed := make(chan struct{})
b.closed <- closed
close(b.closed)
<-closed
return nil
}
}
// run is the main broadcast loop, started when the broadcaster is created.
// Under normal conditions, it waits for events on the event channel. After
// Close is called, this goroutine will exit.
func (b *Broadcaster) run() {
for {
select {
case block := <-b.events:
for _, sink := range b.sinks {
if err := sink.Write(block...); err != nil {
logrus.Errorf("broadcaster: error writing events to %v, these events will be lost: %v", sink, err)
}
}
case closing := <-b.closed:
// close all the underlying sinks
for _, sink := range b.sinks {
if err := sink.Close(); err != nil {
logrus.Errorf("broadcaster: error closing sink %v: %v", sink, err)
}
}
closing <- struct{}{}
logrus.Debugf("broadcaster: closed")
return
}
}
}
// eventQueue accepts all messages into a queue for asynchronous consumption
// by a sink. It is unbounded and thread safe but the sink must be reliable or
// events will be dropped.
type eventQueue struct {
sink Sink
sink events.Sink
events *list.List
listeners []eventQueueListener
cond *sync.Cond
@ -114,13 +23,13 @@ type eventQueue struct {
// eventQueueListener is called when various events happen on the queue.
type eventQueueListener interface {
ingress(events ...Event)
egress(events ...Event)
ingress(event events.Event)
egress(event events.Event)
}
// newEventQueue returns a queue to the provided sink. If the updater is non-
// nil, it will be called to update pending metrics on ingress and egress.
func newEventQueue(sink Sink, listeners ...eventQueueListener) *eventQueue {
func newEventQueue(sink events.Sink, listeners ...eventQueueListener) *eventQueue {
eq := eventQueue{
sink: sink,
events: list.New(),
@ -134,7 +43,7 @@ func newEventQueue(sink Sink, listeners ...eventQueueListener) *eventQueue {
// Write accepts the events into the queue, only failing if the queue has
// beend closed.
func (eq *eventQueue) Write(events ...Event) error {
func (eq *eventQueue) Write(event events.Event) error {
eq.mu.Lock()
defer eq.mu.Unlock()
@ -143,9 +52,9 @@ func (eq *eventQueue) Write(events ...Event) error {
}
for _, listener := range eq.listeners {
listener.ingress(events...)
listener.ingress(event)
}
eq.events.PushBack(events)
eq.events.PushBack(event)
eq.cond.Signal() // signal waiters
return nil
@ -171,18 +80,18 @@ func (eq *eventQueue) Close() error {
// run is the main goroutine to flush events to the target sink.
func (eq *eventQueue) run() {
for {
block := eq.next()
event := eq.next()
if block == nil {
if event == nil {
return // nil block means event queue is closed.
}
if err := eq.sink.Write(block...); err != nil {
if err := eq.sink.Write(event); err != nil {
logrus.Warnf("eventqueue: error writing events to %v, these events will be lost: %v", eq.sink, err)
}
for _, listener := range eq.listeners {
listener.egress(block...)
listener.egress(event)
}
}
}
@ -190,7 +99,7 @@ func (eq *eventQueue) run() {
// next encompasses the critical section of the run loop. When the queue is
// empty, it will block on the condition. If new data arrives, it will wake
// and return a block. When closed, a nil slice will be returned.
func (eq *eventQueue) next() []Event {
func (eq *eventQueue) next() events.Event {
eq.mu.Lock()
defer eq.mu.Unlock()
@ -204,7 +113,7 @@ func (eq *eventQueue) next() []Event {
}
front := eq.events.Front()
block := front.Value.([]Event)
block := front.Value.(events.Event)
eq.events.Remove(front)
return block
@ -213,12 +122,12 @@ func (eq *eventQueue) next() []Event {
// ignoredSink discards events with ignored target media types and actions.
// passes the rest along.
type ignoredSink struct {
Sink
events.Sink
ignoreMediaTypes map[string]bool
ignoreActions map[string]bool
}
func newIgnoredSink(sink Sink, ignored []string, ignoreActions []string) Sink {
func newIgnoredSink(sink events.Sink, ignored []string, ignoreActions []string) events.Sink {
if len(ignored) == 0 {
return sink
}
@ -242,151 +151,14 @@ func newIgnoredSink(sink Sink, ignored []string, ignoreActions []string) Sink {
// Write discards events with ignored target media types and passes the rest
// along.
func (imts *ignoredSink) Write(events ...Event) error {
var kept []Event
for _, e := range events {
if !imts.ignoreMediaTypes[e.Target.MediaType] {
kept = append(kept, e)
}
}
if len(kept) == 0 {
func (imts *ignoredSink) Write(event events.Event) error {
if imts.ignoreMediaTypes[event.(Event).Target.MediaType] || imts.ignoreActions[event.(Event).Action] {
return nil
}
var results []Event
for _, e := range kept {
if !imts.ignoreActions[e.Action] {
results = append(results, e)
}
}
if len(results) == 0 {
return nil
}
return imts.Sink.Write(results...)
return imts.Sink.Write(event)
}
// retryingSink retries the write until success or an ErrSinkClosed is
// returned. Underlying sink must have p > 0 of succeeding or the sink will
// block. Internally, it is a circuit breaker retries to manage reset.
// Concurrent calls to a retrying sink are serialized through the sink,
// meaning that if one is in-flight, another will not proceed.
type retryingSink struct {
mu sync.Mutex
sink Sink
closed bool
// circuit breaker heuristics
failures struct {
threshold int
recent int
last time.Time
backoff time.Duration // time after which we retry after failure.
}
}
type retryingSinkListener interface {
active(events ...Event)
retry(events ...Event)
}
// TODO(stevvooe): We are using circuit break here, which actually doesn't
// make a whole lot of sense for this use case, since we always retry. Move
// this to use bounded exponential backoff.
// newRetryingSink returns a sink that will retry writes to a sink, backing
// off on failure. Parameters threshold and backoff adjust the behavior of the
// circuit breaker.
func newRetryingSink(sink Sink, threshold int, backoff time.Duration) *retryingSink {
rs := &retryingSink{
sink: sink,
}
rs.failures.threshold = threshold
rs.failures.backoff = backoff
return rs
}
// Write attempts to flush the events to the downstream sink until it succeeds
// or the sink is closed.
func (rs *retryingSink) Write(events ...Event) error {
rs.mu.Lock()
defer rs.mu.Unlock()
retry:
if rs.closed {
return ErrSinkClosed
}
if !rs.proceed() {
logrus.Warnf("%v encountered too many errors, backing off", rs.sink)
rs.wait(rs.failures.backoff)
goto retry
}
if err := rs.write(events...); err != nil {
if err == ErrSinkClosed {
// terminal!
return err
}
logrus.Errorf("retryingsink: error writing events: %v, retrying", err)
goto retry
}
func (imts *ignoredSink) Close() error {
return nil
}
// Close closes the sink and the underlying sink.
func (rs *retryingSink) Close() error {
rs.mu.Lock()
defer rs.mu.Unlock()
if rs.closed {
return fmt.Errorf("retryingsink: already closed")
}
rs.closed = true
return rs.sink.Close()
}
// write provides a helper that dispatches failure and success properly. Used
// by write as the single-flight write call.
func (rs *retryingSink) write(events ...Event) error {
if err := rs.sink.Write(events...); err != nil {
rs.failure()
return err
}
rs.reset()
return nil
}
// wait backoff time against the sink, unlocking so others can proceed. Should
// only be called by methods that currently have the mutex.
func (rs *retryingSink) wait(backoff time.Duration) {
rs.mu.Unlock()
defer rs.mu.Lock()
// backoff here
time.Sleep(backoff)
}
// reset marks a successful call.
func (rs *retryingSink) reset() {
rs.failures.recent = 0
rs.failures.last = time.Time{}
}
// failure records a failure.
func (rs *retryingSink) failure() {
rs.failures.recent++
rs.failures.last = time.Now().UTC()
}
// proceed returns true if the call should proceed based on circuit breaker
// heuristics.
func (rs *retryingSink) proceed() bool {
return rs.failures.recent < rs.failures.threshold ||
time.Now().UTC().After(rs.failures.last.Add(rs.failures.backoff))
}

View file

@ -1,72 +1,21 @@
package notifications
import (
"fmt"
"math/rand"
"reflect"
"sync"
"time"
events "github.com/docker/go-events"
"github.com/sirupsen/logrus"
"testing"
)
func TestBroadcaster(t *testing.T) {
const nEvents = 1000
var sinks []Sink
for i := 0; i < 10; i++ {
sinks = append(sinks, &testSink{})
}
b := NewBroadcaster(sinks...)
var block []Event
var wg sync.WaitGroup
for i := 1; i <= nEvents; i++ {
block = append(block, createTestEvent("push", "library/test", "blob"))
if i%10 == 0 && i > 0 {
wg.Add(1)
go func(block ...Event) {
if err := b.Write(block...); err != nil {
t.Errorf("error writing block of length %d: %v", len(block), err)
}
wg.Done()
}(block...)
block = nil
}
}
wg.Wait() // Wait until writes complete
if t.Failed() {
t.FailNow()
}
checkClose(t, b)
// Iterate through the sinks and check that they all have the expected length.
for _, sink := range sinks {
ts := sink.(*testSink)
ts.mu.Lock()
defer ts.mu.Unlock()
if len(ts.events) != nEvents {
t.Fatalf("not all events ended up in testsink: len(testSink) == %d, not %d", len(ts.events), nEvents)
}
if !ts.closed {
t.Fatalf("sink should have been closed")
}
}
}
func TestEventQueue(t *testing.T) {
const nevents = 1000
var ts testSink
metrics := newSafeMetrics()
metrics := newSafeMetrics("")
eq := newEventQueue(
// delayed sync simulates destination slower than channel comms
&delayedSink{
@ -75,20 +24,16 @@ func TestEventQueue(t *testing.T) {
}, metrics.eventQueueListener())
var wg sync.WaitGroup
var block []Event
var event events.Event
for i := 1; i <= nevents; i++ {
block = append(block, createTestEvent("push", "library/test", "blob"))
if i%10 == 0 && i > 0 {
wg.Add(1)
go func(block ...Event) {
if err := eq.Write(block...); err != nil {
t.Errorf("error writing event block: %v", err)
}
wg.Done()
}(block...)
block = nil
}
event = createTestEvent("push", "library/test", "blob")
wg.Add(1)
go func(event events.Event) {
if err := eq.Write(event); err != nil {
t.Errorf("error writing event block: %v", err)
}
wg.Done()
}(event)
}
wg.Wait()
@ -102,8 +47,8 @@ func TestEventQueue(t *testing.T) {
metrics.Lock()
defer metrics.Unlock()
if len(ts.events) != nevents {
t.Fatalf("events did not make it to the sink: %d != %d", len(ts.events), 1000)
if ts.count != nevents {
t.Fatalf("events did not make it to the sink: %d != %d", ts.count, 1000)
}
if !ts.closed {
@ -126,16 +71,14 @@ func TestIgnoredSink(t *testing.T) {
type testcase struct {
ignoreMediaTypes []string
ignoreActions []string
expected []Event
expected events.Event
}
cases := []testcase{
{nil, nil, []Event{blob, manifest}},
{[]string{"other"}, []string{"other"}, []Event{blob, manifest}},
{[]string{"blob"}, []string{"other"}, []Event{manifest}},
{nil, nil, blob},
{[]string{"other"}, []string{"other"}, blob},
{[]string{"blob", "manifest"}, []string{"other"}, nil},
{[]string{"other"}, []string{"push"}, []Event{manifest}},
{[]string{"other"}, []string{"pull"}, []Event{blob}},
{[]string{"other"}, []string{"pull"}, blob},
{[]string{"other"}, []string{"pull", "push"}, nil},
}
@ -143,78 +86,54 @@ func TestIgnoredSink(t *testing.T) {
ts := &testSink{}
s := newIgnoredSink(ts, c.ignoreMediaTypes, c.ignoreActions)
if err := s.Write(blob, manifest); err != nil {
if err := s.Write(blob); err != nil {
t.Fatalf("error writing event: %v", err)
}
ts.mu.Lock()
if !reflect.DeepEqual(ts.events, c.expected) {
t.Fatalf("unexpected events: %#v != %#v", ts.events, c.expected)
if !reflect.DeepEqual(ts.event, c.expected) {
t.Fatalf("unexpected event: %#v != %#v", ts.event, c.expected)
}
ts.mu.Unlock()
}
cases = []testcase{
{nil, nil, manifest},
{[]string{"other"}, []string{"other"}, manifest},
{[]string{"blob"}, []string{"other"}, manifest},
{[]string{"blob", "manifest"}, []string{"other"}, nil},
{[]string{"other"}, []string{"push"}, manifest},
{[]string{"other"}, []string{"pull", "push"}, nil},
}
for _, c := range cases {
ts := &testSink{}
s := newIgnoredSink(ts, c.ignoreMediaTypes, c.ignoreActions)
if err := s.Write(manifest); err != nil {
t.Fatalf("error writing event: %v", err)
}
ts.mu.Lock()
if !reflect.DeepEqual(ts.event, c.expected) {
t.Fatalf("unexpected event: %#v != %#v", ts.event, c.expected)
}
ts.mu.Unlock()
}
}
func TestRetryingSink(t *testing.T) {
// Make a sync that fails most of the time, ensuring that all the events
// make it through.
var ts testSink
flaky := &flakySink{
rate: 1.0, // start out always failing.
Sink: &ts,
}
s := newRetryingSink(flaky, 3, 10*time.Millisecond)
var wg sync.WaitGroup
var block []Event
for i := 1; i <= 100; i++ {
block = append(block, createTestEvent("push", "library/test", "blob"))
// Above 50, set the failure rate lower
if i > 50 {
s.mu.Lock()
flaky.rate = 0.90
s.mu.Unlock()
}
if i%10 == 0 && i > 0 {
wg.Add(1)
go func(block ...Event) {
defer wg.Done()
if err := s.Write(block...); err != nil {
t.Errorf("error writing event block: %v", err)
}
}(block...)
block = nil
}
}
wg.Wait()
if t.Failed() {
t.FailNow()
}
checkClose(t, s)
ts.mu.Lock()
defer ts.mu.Unlock()
if len(ts.events) != 100 {
t.Fatalf("events not propagated: %d != %d", len(ts.events), 100)
}
}
type testSink struct {
events []Event
event events.Event
count int
mu sync.Mutex
closed bool
}
func (ts *testSink) Write(events ...Event) error {
func (ts *testSink) Write(event events.Event) error {
ts.mu.Lock()
defer ts.mu.Unlock()
ts.events = append(ts.events, events...)
ts.event = event
ts.count++
return nil
}
@ -228,29 +147,16 @@ func (ts *testSink) Close() error {
}
type delayedSink struct {
Sink
events.Sink
delay time.Duration
}
func (ds *delayedSink) Write(events ...Event) error {
func (ds *delayedSink) Write(event events.Event) error {
time.Sleep(ds.delay)
return ds.Sink.Write(events...)
return ds.Sink.Write(event)
}
type flakySink struct {
Sink
rate float64
}
func (fs *flakySink) Write(events ...Event) error {
if rand.Float64() < fs.rate {
return fmt.Errorf("error writing %d events", len(events))
}
return fs.Sink.Write(events...)
}
func checkClose(t *testing.T, sink Sink) {
func checkClose(t *testing.T, sink events.Sink) {
if err := sink.Close(); err != nil {
t.Fatalf("unexpected error closing: %v", err)
}
@ -261,7 +167,7 @@ func checkClose(t *testing.T, sink Sink) {
}
// Write after closed should be an error
if err := sink.Write([]Event{}...); err == nil {
if err := sink.Write(Event{}); err == nil {
t.Fatalf("write after closed did not have an error")
} else if err != ErrSinkClosed {
t.Fatalf("error should be ErrSinkClosed")

View file

@ -56,6 +56,35 @@ func ParseNormalizedNamed(s string) (Named, error) {
return named, nil
}
// ParseDockerRef normalizes the image reference following the docker convention. This is added
// mainly for backward compatibility.
// The reference returned can only be either tagged or digested. For reference contains both tag
// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
func ParseDockerRef(ref string) (Named, error) {
named, err := ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, ok := named.(NamedTagged); ok {
if canonical, ok := named.(Canonical); ok {
// The reference is both tagged and digested, only
// return digested.
newNamed, err := WithName(canonical.Name())
if err != nil {
return nil, err
}
newCanonical, err := WithDigest(newNamed, canonical.Digest())
if err != nil {
return nil, err
}
return newCanonical, nil
}
}
return TagNameOnly(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.

View file

@ -623,3 +623,83 @@ func TestMatch(t *testing.T) {
}
}
}
func TestParseDockerRef(t *testing.T) {
testcases := []struct {
name string
input string
expected string
}{
{
name: "nothing",
input: "busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "tag only",
input: "busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "digest only",
input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
expected: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
},
{
name: "path only",
input: "library/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "hostname only",
input: "docker.io/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "no tag",
input: "docker.io/library/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "no path",
input: "docker.io/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "no hostname",
input: "library/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "full reference with tag",
input: "docker.io/library/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "gcr reference without tag",
input: "gcr.io/library/busybox",
expected: "gcr.io/library/busybox:latest",
},
{
name: "both tag and digest",
input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
expected: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
normalized, err := ParseDockerRef(test.input)
if err != nil {
t.Fatal(err)
}
output := normalized.String()
if output != test.expected {
t.Fatalf("expected %q to be parsed as %v, got %v", test.input, test.expected, output)
}
_, err = Parse(output)
if err != nil {
t.Fatalf("%q should be a valid reference, but got an error: %v", output, err)
}
})
}
}

View file

@ -205,7 +205,7 @@ func Parse(s string) (Reference, error) {
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if nameMatch != nil && len(nameMatch) == 3 {
if len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {

View file

@ -639,7 +639,7 @@ func TestParseNamed(t *testing.T) {
failf("error parsing name: %s", err)
continue
} else if err == nil && testcase.err != nil {
failf("parsing succeded: expected error %v", testcase.err)
failf("parsing succeeded: expected error %v", testcase.err)
continue
} else if err != testcase.err {
failf("unexpected error %v, expected %v", err, testcase.err)

View file

@ -207,11 +207,11 @@ func (errs Errors) MarshalJSON() ([]byte, error) {
for _, daErr := range errs {
var err Error
switch daErr.(type) {
switch daErr := daErr.(type) {
case ErrorCode:
err = daErr.(ErrorCode).WithDetail(nil)
err = daErr.WithDetail(nil)
case Error:
err = daErr.(Error)
err = daErr
default:
err = ErrorCodeUnknown.WithDetail(daErr)

View file

@ -9,7 +9,7 @@ import (
// and sets the content-type header to 'application/json'. It will handle
// ErrorCoder and Errors, and if necessary will create an envelope.
func ServeJSON(w http.ResponseWriter, err error) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
var sc int
switch errs := err.(type) {

View file

@ -126,7 +126,7 @@ var (
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -147,7 +147,7 @@ var (
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -168,7 +168,7 @@ var (
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -189,7 +189,7 @@ var (
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -441,7 +441,7 @@ var routeDescriptors = []RouteDescriptor{
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: `{
"name": <name>,
"tags": [
@ -478,7 +478,7 @@ var routeDescriptors = []RouteDescriptor{
linkHeader,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: `{
"name": <name>,
"tags": [
@ -541,7 +541,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeTagInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -592,7 +592,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The received manifest was invalid in some way, as described by the error codes. The client should resolve the issue and retry the request.",
StatusCode: http.StatusBadRequest,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -615,7 +615,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: `{
"errors:" [{
"code": "BLOB_UNKNOWN",
@ -669,7 +669,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeTagInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -686,7 +686,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeManifestUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -766,7 +766,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeDigestInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -774,7 +774,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The blob, identified by `name` and `digest`, is unknown to the registry.",
StatusCode: http.StatusNotFound,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -838,7 +838,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeDigestInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -849,7 +849,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -905,7 +905,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The blob, identified by `name` and `digest`, is unknown to the registry.",
StatusCode: http.StatusNotFound,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -917,7 +917,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "Blob delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled",
StatusCode: http.StatusMethodNotAllowed,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
ErrorCodes: []errcode.ErrorCode{
@ -1179,7 +1179,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1190,7 +1190,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1254,7 +1254,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1265,7 +1265,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1336,7 +1336,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1347,7 +1347,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1431,7 +1431,7 @@ var routeDescriptors = []RouteDescriptor{
errcode.ErrorCodeUnsupported,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1442,7 +1442,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1488,7 +1488,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadInvalid,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1499,7 +1499,7 @@ var routeDescriptors = []RouteDescriptor{
ErrorCodeBlobUploadUnknown,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: errorsBody,
},
},
@ -1539,7 +1539,7 @@ var routeDescriptors = []RouteDescriptor{
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: `{
"repositories": [
<name>,
@ -1558,7 +1558,7 @@ var routeDescriptors = []RouteDescriptor{
{
StatusCode: http.StatusOK,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
ContentType: "application/json",
Format: `{
"repositories": [
<name>,

View file

@ -252,15 +252,3 @@ func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
u.RawQuery = merged.Encode()
return u
}
// appendValues appends the parameters to the url. Panics if the string is not
// a url.
func appendValues(u string, values ...url.Values) string {
up, err := url.Parse(u)
if err != nil {
panic(err) // should never happen
}
return appendValuesURL(up, values...).String()
}

View file

@ -182,11 +182,6 @@ func TestURLBuilderWithPrefix(t *testing.T) {
doTest(false)
}
type builderFromRequestTestCase struct {
request *http.Request
base string
}
func TestBuilderFromRequest(t *testing.T) {
u, err := url.Parse("http://example.com")
if err != nil {

View file

@ -21,7 +21,7 @@
// if ctx, err := accessController.Authorized(ctx, access); err != nil {
// if challenge, ok := err.(auth.Challenge) {
// // Let the challenge write the response.
// challenge.SetHeaders(w)
// challenge.SetHeaders(r, w)
// w.WriteHeader(http.StatusUnauthorized)
// return
// } else {
@ -87,7 +87,7 @@ type Challenge interface {
// adding the an HTTP challenge header on the response message. Callers
// are expected to set the appropriate HTTP status code (e.g. 401)
// themselves.
SetHeaders(w http.ResponseWriter)
SetHeaders(r *http.Request, w http.ResponseWriter)
}
// AccessController controls access to registry resources based on a request

View file

@ -111,7 +111,7 @@ type challenge struct {
var _ auth.Challenge = challenge{}
// SetHeaders sets the basic challenge header on the response.
func (ch challenge) SetHeaders(w http.ResponseWriter) {
func (ch challenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", ch.realm))
}

View file

@ -50,7 +50,7 @@ func TestBasicAccessController(t *testing.T) {
if err != nil {
switch err := err.(type) {
case auth.Challenge:
err.SetHeaders(w)
err.SetHeaders(r, w)
w.WriteHeader(http.StatusUnauthorized)
return
default:

View file

@ -82,7 +82,7 @@ type challenge struct {
var _ auth.Challenge = challenge{}
// SetHeaders sets a simple bearer challenge on the response.
func (ch challenge) SetHeaders(w http.ResponseWriter) {
func (ch challenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
header := fmt.Sprintf("Bearer realm=%q,service=%q", ch.realm, ch.service)
if ch.scope != "" {

View file

@ -21,7 +21,7 @@ func TestSillyAccessController(t *testing.T) {
if err != nil {
switch err := err.(type) {
case auth.Challenge:
err.SetHeaders(w)
err.SetHeaders(r, w)
w.WriteHeader(http.StatusUnauthorized)
return
default:

View file

@ -76,10 +76,11 @@ var (
// authChallenge implements the auth.Challenge interface.
type authChallenge struct {
err error
realm string
service string
accessSet accessSet
err error
realm string
autoRedirect bool
service string
accessSet accessSet
}
var _ auth.Challenge = authChallenge{}
@ -97,8 +98,14 @@ func (ac authChallenge) Status() int {
// challengeParams constructs the value to be used in
// the WWW-Authenticate response challenge header.
// See https://tools.ietf.org/html/rfc6750#section-3
func (ac authChallenge) challengeParams() string {
str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service)
func (ac authChallenge) challengeParams(r *http.Request) string {
var realm string
if ac.autoRedirect {
realm = fmt.Sprintf("https://%s/auth/token", r.Host)
} else {
realm = ac.realm
}
str := fmt.Sprintf("Bearer realm=%q,service=%q", realm, ac.service)
if scope := ac.accessSet.scopeParam(); scope != "" {
str = fmt.Sprintf("%s,scope=%q", str, scope)
@ -114,23 +121,25 @@ func (ac authChallenge) challengeParams() string {
}
// SetChallenge sets the WWW-Authenticate value for the response.
func (ac authChallenge) SetHeaders(w http.ResponseWriter) {
w.Header().Add("WWW-Authenticate", ac.challengeParams())
func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
w.Header().Add("WWW-Authenticate", ac.challengeParams(r))
}
// accessController implements the auth.AccessController interface.
type accessController struct {
realm string
issuer string
service string
rootCerts *x509.CertPool
trustedKeys map[string]libtrust.PublicKey
realm string
autoRedirect bool
issuer string
service string
rootCerts *x509.CertPool
trustedKeys map[string]libtrust.PublicKey
}
// tokenAccessOptions is a convenience type for handling
// options to the contstructor of an accessController.
type tokenAccessOptions struct {
realm string
autoRedirect bool
issuer string
service string
rootCertBundle string
@ -153,6 +162,15 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
autoRedirectVal, ok := options["autoredirect"]
if ok {
autoRedirect, ok := autoRedirectVal.(bool)
if !ok {
return opts, fmt.Errorf("token auth requires a valid option bool: autoredirect")
}
opts.autoRedirect = autoRedirect
}
return opts, nil
}
@ -205,11 +223,12 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
}
return &accessController{
realm: config.realm,
issuer: config.issuer,
service: config.service,
rootCerts: rootPool,
trustedKeys: trustedKeys,
realm: config.realm,
autoRedirect: config.autoRedirect,
issuer: config.issuer,
service: config.service,
rootCerts: rootPool,
trustedKeys: trustedKeys,
}, nil
}
@ -217,9 +236,10 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
// for actions on resources described by the given access items.
func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
challenge := &authChallenge{
realm: ac.realm,
service: ac.service,
accessSet: newAccessSet(accessItems...),
realm: ac.realm,
autoRedirect: ac.autoRedirect,
service: ac.service,
accessSet: newAccessSet(accessItems...),
}
req, err := dcontext.GetRequest(ctx)

View file

@ -333,6 +333,7 @@ func TestAccessController(t *testing.T) {
"issuer": issuer,
"service": service,
"rootcertbundle": rootCertBundleFilename,
"autoredirect": false,
}
accessController, err := newAccessController(options)
@ -518,6 +519,7 @@ func TestNewAccessControllerPemBlock(t *testing.T) {
"issuer": issuer,
"service": service,
"rootcertbundle": rootCertBundleFilename,
"autoredirect": false,
}
ac, err := newAccessController(options)

View file

@ -117,8 +117,8 @@ func init() {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c))
if strings.ContainsRune(" \t\r\n", rune(c)) {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {

View file

@ -366,6 +366,10 @@ func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, servic
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
}
if tr.AccessToken == "" {
return "", time.Time{}, ErrNoToken
}
if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
}

View file

@ -466,7 +466,7 @@ func TestEndpointAuthorizeTokenBasic(t *testing.T) {
},
})
authenicate1 := fmt.Sprintf("Basic realm=localhost")
authenicate1 := "Basic realm=localhost"
basicCheck := func(a string) bool {
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
}
@ -546,7 +546,7 @@ func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) {
},
})
authenicate1 := fmt.Sprintf("Basic realm=localhost")
authenicate1 := "Basic realm=localhost"
tokenExchanges := 0
basicCheck := func(a string) bool {
tokenExchanges = tokenExchanges + 1
@ -706,7 +706,7 @@ func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) {
},
})
authenicate1 := fmt.Sprintf("Basic realm=localhost")
authenicate1 := "Basic realm=localhost"
tokenExchanges := 0
basicCheck := func(a string) bool {
tokenExchanges = tokenExchanges + 1
@ -835,7 +835,7 @@ func TestEndpointAuthorizeBasic(t *testing.T) {
username := "user1"
password := "funSecretPa$$word"
authenicate := fmt.Sprintf("Basic realm=localhost")
authenicate := "Basic realm=localhost"
validCheck := func(a string) bool {
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
}

View file

@ -64,8 +64,8 @@ func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) {
return 0, fmt.Errorf("bad range format: %s", rng)
}
hbu.offset += end - start + 1
return (end - start + 1), nil
}
func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) {
@ -99,8 +99,8 @@ func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) {
return 0, fmt.Errorf("bad range format: %s", rng)
}
hbu.offset += int64(end - start + 1)
return (end - start + 1), nil
}
func (hbu *httpBlobUpload) Size() int64 {

View file

@ -8,7 +8,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/testutil"
)
@ -209,3 +209,286 @@ func TestUploadReadFrom(t *testing.T) {
t.Fatalf("Unexpected response status: %s, expected %s", uploadErr.Status, expected)
}
}
func TestUploadSize(t *testing.T) {
_, b := newRandomBlob(64)
readFromLocationPath := "/v2/test/upload/readfrom/uploads/testid"
writeLocationPath := "/v2/test/upload/readfrom/uploads/testid"
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
{
Request: testutil.Request{
Method: "GET",
Route: "/v2/",
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Docker-Distribution-API-Version": {"registry/2.0"},
}),
},
},
{
Request: testutil.Request{
Method: "PATCH",
Route: readFromLocationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
"Location": {readFromLocationPath},
"Range": {"0-63"},
}),
},
},
{
Request: testutil.Request{
Method: "PATCH",
Route: writeLocationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
"Location": {writeLocationPath},
"Range": {"0-63"},
}),
},
},
})
e, c := testServer(m)
defer c()
// Writing with ReadFrom
blobUpload := &httpBlobUpload{
client: &http.Client{},
location: e + readFromLocationPath,
}
if blobUpload.Size() != 0 {
t.Fatalf("Wrong size returned from Size: %d, expected 0", blobUpload.Size())
}
_, err := blobUpload.ReadFrom(bytes.NewReader(b))
if err != nil {
t.Fatalf("Error calling ReadFrom: %s", err)
}
if blobUpload.Size() != 64 {
t.Fatalf("Wrong size returned from Size: %d, expected 64", blobUpload.Size())
}
// Writing with Write
blobUpload = &httpBlobUpload{
client: &http.Client{},
location: e + writeLocationPath,
}
_, err = blobUpload.Write(b)
if err != nil {
t.Fatalf("Error calling Write: %s", err)
}
if blobUpload.Size() != 64 {
t.Fatalf("Wrong size returned from Size: %d, expected 64", blobUpload.Size())
}
}
func TestUploadWrite(t *testing.T) {
_, b := newRandomBlob(64)
repo := "test/upload/write"
locationPath := fmt.Sprintf("/v2/%s/uploads/testid", repo)
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
{
Request: testutil.Request{
Method: "GET",
Route: "/v2/",
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Docker-Distribution-API-Version": {"registry/2.0"},
}),
},
},
// Test Valid case
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
"Location": {locationPath},
"Range": {"0-63"},
}),
},
},
// Test invalid range
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
"Location": {locationPath},
"Range": {""},
}),
},
},
// Test 404
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusNotFound,
},
},
// Test 400 valid json
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusBadRequest,
Body: []byte(`
{ "errors":
[
{
"code": "BLOB_UPLOAD_INVALID",
"message": "blob upload invalid",
"detail": "more detail"
}
]
} `),
},
},
// Test 400 invalid json
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusBadRequest,
Body: []byte("something bad happened"),
},
},
// Test 500
{
Request: testutil.Request{
Method: "PATCH",
Route: locationPath,
Body: b,
},
Response: testutil.Response{
StatusCode: http.StatusInternalServerError,
},
},
})
e, c := testServer(m)
defer c()
blobUpload := &httpBlobUpload{
client: &http.Client{},
}
// Valid case
blobUpload.location = e + locationPath
n, err := blobUpload.Write(b)
if err != nil {
t.Fatalf("Error calling Write: %s", err)
}
if n != 64 {
t.Fatalf("Wrong length returned from Write: %d, expected 64", n)
}
// Bad range
blobUpload.location = e + locationPath
_, err = blobUpload.Write(b)
if err == nil {
t.Fatalf("Expected error when bad range received")
}
// 404
blobUpload.location = e + locationPath
_, err = blobUpload.Write(b)
if err == nil {
t.Fatalf("Expected error when not found")
}
if err != distribution.ErrBlobUploadUnknown {
t.Fatalf("Wrong error thrown: %s, expected %s", err, distribution.ErrBlobUploadUnknown)
}
// 400 valid json
blobUpload.location = e + locationPath
_, err = blobUpload.Write(b)
if err == nil {
t.Fatalf("Expected error when not found")
}
if uploadErr, ok := err.(errcode.Errors); !ok {
t.Fatalf("Wrong error type %T: %s", err, err)
} else if len(uploadErr) != 1 {
t.Fatalf("Unexpected number of errors: %d, expected 1", len(uploadErr))
} else {
v2Err, ok := uploadErr[0].(errcode.Error)
if !ok {
t.Fatalf("Not an 'Error' type: %#v", uploadErr[0])
}
if v2Err.Code != v2.ErrorCodeBlobUploadInvalid {
t.Fatalf("Unexpected error code: %s, expected %d", v2Err.Code.String(), v2.ErrorCodeBlobUploadInvalid)
}
if expected := "blob upload invalid"; v2Err.Message != expected {
t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Message, expected)
}
if expected := "more detail"; v2Err.Detail.(string) != expected {
t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Detail.(string), expected)
}
}
// 400 invalid json
blobUpload.location = e + locationPath
_, err = blobUpload.Write(b)
if err == nil {
t.Fatalf("Expected error when not found")
}
if uploadErr, ok := err.(*UnexpectedHTTPResponseError); !ok {
t.Fatalf("Wrong error type %T: %s", err, err)
} else {
respStr := string(uploadErr.Response)
if expected := "something bad happened"; respStr != expected {
t.Fatalf("Unexpected response string: %s, expected: %s", respStr, expected)
}
}
// 500
blobUpload.location = e + locationPath
_, err = blobUpload.Write(b)
if err == nil {
t.Fatalf("Expected error when not found")
}
if uploadErr, ok := err.(*UnexpectedHTTPStatusError); !ok {
t.Fatalf("Wrong error type %T: %s", err, err)
} else if expected := "500 " + http.StatusText(http.StatusInternalServerError); uploadErr.Status != expected {
t.Fatalf("Unexpected response status: %s, expected %s", uploadErr.Status, expected)
}
}

View file

@ -16,7 +16,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/cache/memory"
@ -667,7 +667,28 @@ func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.Rea
}
func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
panic("not implemented")
desc, err := bs.statter.Stat(ctx, dgst)
if err != nil {
return err
}
w.Header().Set("Content-Length", strconv.FormatInt(desc.Size, 10))
w.Header().Set("Content-Type", desc.MediaType)
w.Header().Set("Docker-Content-Digest", dgst.String())
w.Header().Set("Etag", dgst.String())
if r.Method == http.MethodHead {
return nil
}
blob, err := bs.Open(ctx, dgst)
if err != nil {
return err
}
defer blob.Close()
_, err = io.CopyN(w, blob, desc.Size)
return err
}
func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
@ -752,6 +773,14 @@ func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateO
case http.StatusAccepted:
// TODO(dmcgowan): Check for invalid UUID
uuid := resp.Header.Get("Docker-Upload-UUID")
if uuid == "" {
parts := strings.Split(resp.Header.Get("Location"), "/")
uuid = parts[len(parts)-1]
}
if uuid == "" {
return nil, errors.New("cannot retrieve docker upload UUID")
}
location, err := sanitizeLocation(resp.Header.Get("Location"), u)
if err != nil {
return nil, err
@ -770,7 +799,18 @@ func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateO
}
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
panic("not implemented")
location, err := bs.ub.BuildBlobUploadChunkURL(bs.name, id)
if err != nil {
return nil, err
}
return &httpBlobUpload{
statter: bs.statter,
client: bs.client,
uuid: id,
startedAt: time.Now(),
location: location,
}, nil
}
func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error {

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@ -22,7 +23,7 @@ import (
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/testutil"
"github.com/docker/distribution/uuid"
"github.com/docker/libtrust"
@ -57,6 +58,7 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R
Body: content,
Headers: http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(content))},
"Content-Type": {"application/octet-stream"},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
}),
},
@ -71,6 +73,7 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(content))},
"Content-Type": {"application/octet-stream"},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
}),
},
@ -80,7 +83,7 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R
func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
headers := map[string][]string{
"Content-Length": {strconv.Itoa(len(content))},
"Content-Type": {"application/json; charset=utf-8"},
"Content-Type": {"application/json"},
}
if link != "" {
headers["Link"] = append(headers["Link"], link)
@ -99,6 +102,193 @@ func addTestCatalog(route string, content []byte, link string, m *testutil.Reque
})
}
func TestBlobServeBlob(t *testing.T) {
dgst, blob := newRandomBlob(1024)
var m testutil.RequestResponseMap
addTestFetch("test.example.com/repo1", dgst, blob, &m)
e, c := testServer(m)
defer c()
ctx := context.Background()
repo, _ := reference.WithName("test.example.com/repo1")
r, err := NewRepository(repo, e, nil)
if err != nil {
t.Fatal(err)
}
l := r.Blobs(ctx)
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
err = l.ServeBlob(ctx, resp, req, dgst)
if err != nil {
t.Errorf("Error serving blob: %s", err.Error())
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Error reading response body: %s", err.Error())
}
if string(body) != string(blob) {
t.Errorf("Unexpected response body. Got %q, expected %q", string(body), string(blob))
}
expectedHeaders := []struct {
Name string
Value string
}{
{Name: "Content-Length", Value: "1024"},
{Name: "Content-Type", Value: "application/octet-stream"},
{Name: "Docker-Content-Digest", Value: dgst.String()},
{Name: "Etag", Value: dgst.String()},
}
for _, h := range expectedHeaders {
if resp.Header().Get(h.Name) != h.Value {
t.Errorf("Unexpected %s. Got %s, expected %s", h.Name, resp.Header().Get(h.Name), h.Value)
}
}
}
func TestBlobServeBlobHEAD(t *testing.T) {
dgst, blob := newRandomBlob(1024)
var m testutil.RequestResponseMap
addTestFetch("test.example.com/repo1", dgst, blob, &m)
e, c := testServer(m)
defer c()
ctx := context.Background()
repo, _ := reference.WithName("test.example.com/repo1")
r, err := NewRepository(repo, e, nil)
if err != nil {
t.Fatal(err)
}
l := r.Blobs(ctx)
resp := httptest.NewRecorder()
req := httptest.NewRequest("HEAD", "/", nil)
err = l.ServeBlob(ctx, resp, req, dgst)
if err != nil {
t.Errorf("Error serving blob: %s", err.Error())
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Error reading response body: %s", err.Error())
}
if string(body) != "" {
t.Errorf("Unexpected response body. Got %q, expected %q", string(body), "")
}
expectedHeaders := []struct {
Name string
Value string
}{
{Name: "Content-Length", Value: "1024"},
{Name: "Content-Type", Value: "application/octet-stream"},
{Name: "Docker-Content-Digest", Value: dgst.String()},
{Name: "Etag", Value: dgst.String()},
}
for _, h := range expectedHeaders {
if resp.Header().Get(h.Name) != h.Value {
t.Errorf("Unexpected %s. Got %s, expected %s", h.Name, resp.Header().Get(h.Name), h.Value)
}
}
}
func TestBlobResume(t *testing.T) {
dgst, b1 := newRandomBlob(1024)
id := uuid.Generate().String()
var m testutil.RequestResponseMap
repo, _ := reference.WithName("test.example.com/repo1")
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PATCH",
Route: "/v2/" + repo.Name() + "/blobs/uploads/" + id,
Body: b1,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Docker-Content-Digest": {dgst.String()},
"Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PUT",
Route: "/v2/" + repo.Name() + "/blobs/uploads/" + id,
QueryParams: map[string][]string{
"digest": {dgst.String()},
},
},
Response: testutil.Response{
StatusCode: http.StatusCreated,
Headers: http.Header(map[string][]string{
"Docker-Content-Digest": {dgst.String()},
"Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "HEAD",
Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(b1))},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
}),
},
})
e, c := testServer(m)
defer c()
ctx := context.Background()
r, err := NewRepository(repo, e, nil)
if err != nil {
t.Fatal(err)
}
l := r.Blobs(ctx)
upload, err := l.Resume(ctx, id)
if err != nil {
t.Errorf("Error resuming blob: %s", err.Error())
}
if upload.ID() != id {
t.Errorf("Unexpected UUID %s; expected %s", upload.ID(), id)
}
n, err := upload.ReadFrom(bytes.NewReader(b1))
if err != nil {
t.Fatal(err)
}
if n != int64(len(b1)) {
t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
}
blob, err := upload.Commit(ctx, distribution.Descriptor{
Digest: dgst,
Size: int64(len(b1)),
})
if err != nil {
t.Fatal(err)
}
if blob.Size != int64(len(b1)) {
t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
}
}
func TestBlobDelete(t *testing.T) {
dgst, _ := newRandomBlob(1024)
var m testutil.RequestResponseMap
@ -152,7 +342,7 @@ func TestBlobFetch(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if bytes.Compare(b, b1) != 0 {
if !bytes.Equal(b, b1) {
t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
}
@ -473,6 +663,198 @@ func TestBlobUploadMonolithic(t *testing.T) {
}
}
func TestBlobUploadMonolithicDockerUploadUUIDFromURL(t *testing.T) {
dgst, b1 := newRandomBlob(1024)
var m testutil.RequestResponseMap
repo, _ := reference.WithName("test.example.com/uploadrepo")
uploadID := uuid.Generate().String()
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "POST",
Route: "/v2/" + repo.Name() + "/blobs/uploads/",
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Content-Length": {"0"},
"Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
"Range": {"0-0"},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PATCH",
Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
Body: b1,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
"Content-Length": {"0"},
"Docker-Content-Digest": {dgst.String()},
"Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PUT",
Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
QueryParams: map[string][]string{
"digest": {dgst.String()},
},
},
Response: testutil.Response{
StatusCode: http.StatusCreated,
Headers: http.Header(map[string][]string{
"Content-Length": {"0"},
"Docker-Content-Digest": {dgst.String()},
"Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "HEAD",
Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(b1))},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
}),
},
})
e, c := testServer(m)
defer c()
ctx := context.Background()
r, err := NewRepository(repo, e, nil)
if err != nil {
t.Fatal(err)
}
l := r.Blobs(ctx)
upload, err := l.Create(ctx)
if err != nil {
t.Fatal(err)
}
if upload.ID() != uploadID {
log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
}
n, err := upload.ReadFrom(bytes.NewReader(b1))
if err != nil {
t.Fatal(err)
}
if n != int64(len(b1)) {
t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
}
blob, err := upload.Commit(ctx, distribution.Descriptor{
Digest: dgst,
Size: int64(len(b1)),
})
if err != nil {
t.Fatal(err)
}
if blob.Size != int64(len(b1)) {
t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
}
}
func TestBlobUploadMonolithicNoDockerUploadUUID(t *testing.T) {
dgst, b1 := newRandomBlob(1024)
var m testutil.RequestResponseMap
repo, _ := reference.WithName("test.example.com/uploadrepo")
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "POST",
Route: "/v2/" + repo.Name() + "/blobs/uploads/",
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Content-Length": {"0"},
"Location": {"/v2/" + repo.Name() + "/blobs/uploads/"},
"Range": {"0-0"},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PATCH",
Route: "/v2/" + repo.Name() + "/blobs/uploads/",
Body: b1,
},
Response: testutil.Response{
StatusCode: http.StatusAccepted,
Headers: http.Header(map[string][]string{
"Location": {"/v2/" + repo.Name() + "/blobs/uploads/"},
"Content-Length": {"0"},
"Docker-Content-Digest": {dgst.String()},
"Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "PUT",
Route: "/v2/" + repo.Name() + "/blobs/uploads/",
QueryParams: map[string][]string{
"digest": {dgst.String()},
},
},
Response: testutil.Response{
StatusCode: http.StatusCreated,
Headers: http.Header(map[string][]string{
"Content-Length": {"0"},
"Docker-Content-Digest": {dgst.String()},
"Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)},
}),
},
})
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: "HEAD",
Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Headers: http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(b1))},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
}),
},
})
e, c := testServer(m)
defer c()
ctx := context.Background()
r, err := NewRepository(repo, e, nil)
if err != nil {
t.Fatal(err)
}
l := r.Blobs(ctx)
upload, err := l.Create(ctx)
if err.Error() != "cannot retrieve docker upload UUID" {
log.Fatalf("expected rejection to retrieve docker upload UUID error. Got %q", err)
}
if upload != nil {
log.Fatal("Expected upload to be nil")
}
}
func TestBlobMount(t *testing.T) {
dgst, content := newRandomBlob(1024)
var m testutil.RequestResponseMap
@ -1062,7 +1444,7 @@ func TestObtainsErrorForMissingTag(t *testing.T) {
StatusCode: http.StatusNotFound,
Body: errBytes,
Headers: http.Header(map[string][]string{
"Content-Type": {"application/json; charset=utf-8"},
"Content-Type": {"application/json"},
}),
},
})
@ -1336,7 +1718,7 @@ func TestSanitizeLocation(t *testing.T) {
expected: "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
},
{
description: "ensure new hostname overidden",
description: "ensure new hostname overridden",
location: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
source: "http://blahalaja.com/v1",
expected: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",

View file

@ -6,6 +6,13 @@ import (
"sync"
)
func identityTransportWrapper(rt http.RoundTripper) http.RoundTripper {
return rt
}
// DefaultTransportWrapper allows a user to wrap every generated transport
var DefaultTransportWrapper = identityTransportWrapper
// RequestModifier represents an object which will do an inplace
// modification of an HTTP request.
type RequestModifier interface {
@ -31,10 +38,11 @@ func (h headerModifier) ModifyRequest(req *http.Request) error {
// NewTransport creates a new transport which will apply modifiers to
// the request on a RoundTrip call.
func NewTransport(base http.RoundTripper, modifiers ...RequestModifier) http.RoundTripper {
return &transport{
Modifiers: modifiers,
Base: base,
}
return DefaultTransportWrapper(
&transport{
Modifiers: modifiers,
Base: base,
})
}
// transport is an http.RoundTripper that makes HTTP requests after

View file

@ -28,7 +28,7 @@ import (
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
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"
@ -65,7 +65,7 @@ func TestCheckAPI(t *testing.T) {
checkResponse(t, "issuing api base check", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Type": []string{"application/json; charset=utf-8"},
"Content-Type": []string{"application/json"},
"Content-Length": []string{"2"},
})
@ -259,7 +259,7 @@ func TestURLPrefix(t *testing.T) {
checkResponse(t, "issuing api base check", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Type": []string{"application/json; charset=utf-8"},
"Content-Type": []string{"application/json"},
"Content-Length": []string{"2"},
})
}
@ -959,7 +959,6 @@ func testManifestWithStorageError(t *testing.T, env *testEnv, imageName referenc
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 {
@ -1066,12 +1065,11 @@ func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Name
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
for i := range unsignedManifest.FSLayers {
rs, dgstStr, err := testutil.CreateRandomTarFile()
rs, dgst, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating random layer %d: %v", i, err)
}
dgst := digest.Digest(dgstStr)
expectedLayers[dgst] = rs
unsignedManifest.FSLayers[i].BlobSum = dgst
@ -1180,7 +1178,7 @@ func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Name
// charset.
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, schema1.MediaTypeSignedManifest, sm2)
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json; charset=utf-8", sm2)
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2)
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2)
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
@ -1405,12 +1403,11 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Name
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
for i := range manifest.Layers {
rs, dgstStr, err := testutil.CreateRandomTarFile()
rs, dgst, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating random layer %d: %v", i, err)
}
dgst := digest.Digest(dgstStr)
expectedLayers[dgst] = rs
manifest.Layers[i].Digest = dgst
@ -2015,6 +2012,7 @@ type testEnv struct {
}
func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv {
upstreamEnv := newTestEnv(t, deleteEnabled)
config := configuration.Configuration{
Storage: configuration.Storage{
"testdriver": configuration.Parameters{},
@ -2024,7 +2022,7 @@ func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv {
}},
},
Proxy: configuration.Proxy{
RemoteURL: "http://example.com",
RemoteURL: upstreamEnv.server.URL,
},
}
config.Compatibility.Schema1.Enabled = true
@ -2329,7 +2327,7 @@ func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, error
// TODO(stevvooe): Shoot. The error setup is not working out. The content-
// type headers are being set after writing the status code.
// if resp.Header.Get("Content-Type") != "application/json; charset=utf-8" {
// if resp.Header.Get("Content-Type") != "application/json" {
// t.Fatalf("unexpected content type: %v != 'application/json'",
// resp.Header.Get("Content-Type"))
// }
@ -2357,7 +2355,7 @@ func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, error
// Ensure that counts of expected errors were all non-zero
for code := range expected {
if counts[code] == 0 {
t.Fatalf("expected error code %v not encounterd during %s: %s", code, msg, string(p))
t.Fatalf("expected error code %v not encountered during %s: %s", code, msg, string(p))
}
}
@ -2432,11 +2430,10 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string)
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
for i := range unsignedManifest.FSLayers {
rs, dgstStr, err := testutil.CreateRandomTarFile()
rs, dgst, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating random layer %d: %v", i, err)
}
dgst := digest.Digest(dgstStr)
expectedLayers[dgst] = rs
unsignedManifest.FSLayers[i].BlobSum = dgst

View file

@ -24,7 +24,7 @@ import (
"github.com/docker/distribution/notifications"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth"
registrymiddleware "github.com/docker/distribution/registry/middleware/registry"
repositorymiddleware "github.com/docker/distribution/registry/middleware/repository"
@ -36,6 +36,7 @@ import (
"github.com/docker/distribution/registry/storage/driver/factory"
storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware"
"github.com/docker/distribution/version"
events "github.com/docker/go-events"
"github.com/docker/go-metrics"
"github.com/docker/libtrust"
"github.com/garyburd/redigo/redis"
@ -70,7 +71,7 @@ type App struct {
// events contains notification related configuration.
events struct {
sink notifications.Sink
sink events.Sink
source notifications.SourceRecord
}
@ -328,7 +329,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
var ok bool
app.repoRemover, ok = app.registry.(distribution.RepositoryRemover)
if !ok {
dcontext.GetLogger(app).Warnf("Registry does not implement RempositoryRemover. Will not be able to delete repos and tags")
dcontext.GetLogger(app).Warnf("Registry does not implement RepositoryRemover. Will not be able to delete repos and tags")
}
return app
@ -446,7 +447,7 @@ func (app *App) register(routeName string, dispatch dispatchFunc) {
// configureEvents prepares the event sink for action.
func (app *App) configureEvents(configuration *configuration.Configuration) {
// Configure all of the endpoint sinks.
var sinks []notifications.Sink
var sinks []events.Sink
for _, endpoint := range configuration.Notifications.Endpoints {
if endpoint.Disabled {
dcontext.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name)
@ -470,7 +471,7 @@ func (app *App) configureEvents(configuration *configuration.Configuration) {
// replacing broadcaster with a rabbitmq implementation. It's recommended
// that the registry instances also act as the workers to keep deployment
// simple.
app.events.sink = notifications.NewBroadcaster(sinks...)
app.events.sink = events.NewBroadcaster(sinks...)
// Populate registry event source
hostname, err := os.Hostname()
@ -753,20 +754,18 @@ func (app *App) logError(ctx context.Context, errors errcode.Errors) {
for _, e1 := range errors {
var c context.Context
switch e1.(type) {
switch e := e1.(type) {
case errcode.Error:
e, _ := e1.(errcode.Error)
c = context.WithValue(ctx, errCodeKey{}, e.Code)
c = context.WithValue(c, errMessageKey{}, e.Message)
c = context.WithValue(c, errDetailKey{}, e.Detail)
case errcode.ErrorCode:
e, _ := e1.(errcode.ErrorCode)
c = context.WithValue(ctx, errCodeKey{}, e)
c = context.WithValue(c, errMessageKey{}, e.Message())
default:
// just normal go 'error'
c = context.WithValue(ctx, errCodeKey{}, errcode.ErrorCodeUnknown)
c = context.WithValue(c, errMessageKey{}, e1.Error())
c = context.WithValue(c, errMessageKey{}, e.Error())
}
c = dcontext.WithLogger(c, dcontext.GetLogger(c,
@ -847,7 +846,7 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont
switch err := err.(type) {
case auth.Challenge:
// Add the appropriate WWW-Auth header
err.SetHeaders(w)
err.SetHeaders(r, w)
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(accessRecords)); err != nil {
dcontext.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
@ -864,7 +863,7 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont
return err
}
dcontext.GetLogger(ctx).Info("authorized request")
dcontext.GetLogger(ctx, auth.UserNameKey).Info("authorized request")
// TODO(stevvooe): This pattern needs to be cleaned up a bit. One context
// should be replaced by another, rather than replacing the context on a
// mutable object.
@ -898,7 +897,7 @@ func (app *App) nameRequired(r *http.Request) bool {
func apiBase(w http.ResponseWriter, r *http.Request) {
const emptyJSON = "{}"
// Provide a simple /v2/ 200 OK response with empty json response.
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
fmt.Fprint(w, emptyJSON)

View file

@ -11,7 +11,7 @@ import (
"github.com/docker/distribution/configuration"
"github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth"
_ "github.com/docker/distribution/registry/auth/silly"
"github.com/docker/distribution/registry/storage"
@ -188,8 +188,8 @@ func TestNewApp(t *testing.T) {
t.Fatalf("unexpected status code during request: %v", err)
}
if req.Header.Get("Content-Type") != "application/json; charset=utf-8" {
t.Fatalf("unexpected content-type: %v != %v", req.Header.Get("Content-Type"), "application/json; charset=utf-8")
if req.Header.Get("Content-Type") != "application/json" {
t.Fatalf("unexpected content-type: %v != %v", req.Header.Get("Content-Type"), "application/json")
}
expectedAuthHeader := "Bearer realm=\"realm-test\",service=\"service-test\""

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
)

View file

@ -9,7 +9,7 @@ import (
dcontext "github.com/docker/distribution/context"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/storage"
"github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
@ -36,52 +36,8 @@ func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
}
if buh.UUID != "" {
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("error resolving upload: %v", err)
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
buh.State = state
if state.Name != ctx.Repository.Named().Name() {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Named().Name())
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
if state.UUID != buh.UUID {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID)
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
blobs := ctx.Repository.Blobs(buh)
upload, err := blobs.Resume(buh, buh.UUID)
if err != nil {
dcontext.GetLogger(ctx).Errorf("error resolving upload: %v", err)
if err == distribution.ErrBlobUploadUnknown {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err))
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
})
}
buh.Upload = upload
if size := upload.Size(); size != buh.State.Offset {
defer upload.Close()
dcontext.GetLogger(ctx).Errorf("upload resumed at wrong offest: %d != %d", size, buh.State.Offset)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
upload.Cancel(buh)
})
if h := buh.ResumeBlobUpload(ctx, r); h != nil {
return h
}
return closeResources(handler, buh.Upload)
}
@ -172,7 +128,7 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque
ct := r.Header.Get("Content-Type")
if ct != "" && ct != "application/octet-stream" {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type")))
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("bad Content-Type")))
// TODO(dmcgowan): encode error
return
}
@ -282,6 +238,57 @@ func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Re
w.WriteHeader(http.StatusNoContent)
}
func (buh *blobUploadHandler) ResumeBlobUpload(ctx *Context, r *http.Request) http.Handler {
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("error resolving upload: %v", err)
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
buh.State = state
if state.Name != ctx.Repository.Named().Name() {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Named().Name())
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
if state.UUID != buh.UUID {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dcontext.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID)
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
})
}
blobs := ctx.Repository.Blobs(buh)
upload, err := blobs.Resume(buh, buh.UUID)
if err != nil {
dcontext.GetLogger(ctx).Errorf("error resolving upload: %v", err)
if err == distribution.ErrBlobUploadUnknown {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err))
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
})
}
buh.Upload = upload
if size := upload.Size(); size != buh.State.Offset {
defer upload.Close()
dcontext.GetLogger(ctx).Errorf("upload resumed at wrong offset: %d != %d", size, buh.State.Offset)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
upload.Cancel(buh)
})
}
return nil
}
// blobUploadResponse provides a standard request for uploading blobs and
// chunk responses. This sets the correct headers but the response status is
// left to the caller. The fresh argument is used to ensure that new blob

View file

@ -55,7 +55,7 @@ func (ch *catalogHandler) GetCatalog(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
// Add a link header if there are more entries to retrieve
if moreEntries {

View file

@ -8,7 +8,7 @@ import (
"github.com/docker/distribution"
dcontext "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth"
"github.com/opencontainers/go-digest"
)

View file

@ -20,7 +20,7 @@ type logHook struct {
func (hook *logHook) Fire(entry *logrus.Entry) error {
addr := strings.Split(hook.Mail.Addr, ":")
if len(addr) != 2 {
return errors.New("Invalid Mail Address")
return errors.New("invalid Mail Address")
}
host := addr[0]
subject := fmt.Sprintf("[%s] %s: %s", entry.Level, host, entry.Message)
@ -37,7 +37,7 @@ func (hook *logHook) Fire(entry *logrus.Entry) error {
if err := t.Execute(b, entry); err != nil {
return err
}
body := fmt.Sprintf("%s", b)
body := b.String()
return hook.Mail.sendMail(subject, body)
}

View file

@ -17,7 +17,7 @@ type mailer struct {
func (mail *mailer) sendMail(subject, message string) error {
addr := strings.Split(mail.Addr, ":")
if len(addr) != 2 {
return errors.New("Invalid Mail Address")
return errors.New("invalid Mail Address")
}
host := addr[0]
msg := []byte("To:" + strings.Join(mail.To, ";") +

View file

@ -3,6 +3,7 @@ package handlers
import (
"bytes"
"fmt"
"mime"
"net/http"
"strings"
@ -14,11 +15,11 @@ import (
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth"
"github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// These constants determine which architecture and OS to choose from a
@ -97,14 +98,10 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request)
// we need to split each header value on "," to get the full list of "Accept" values (per RFC 2616)
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
for _, mediaType := range strings.Split(acceptHeader, ",") {
// remove "; q=..." if present
if i := strings.Index(mediaType, ";"); i >= 0 {
mediaType = mediaType[:i]
if mediaType, _, err = mime.ParseMediaType(mediaType); err != nil {
continue
}
// it's common (but not required) for Accept values to be space separated ("a/b, c/d, e/f")
mediaType = strings.TrimSpace(mediaType)
if mediaType == schema2.MediaTypeManifest {
supports[manifestSchema2] = true
}

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
)
@ -49,7 +49,7 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
if err := enc.Encode(tagsAPIResponse{

View file

@ -6,7 +6,6 @@ import (
"net/http"
"strconv"
"sync"
"time"
"github.com/docker/distribution"
dcontext "github.com/docker/distribution/context"
@ -15,9 +14,6 @@ import (
"github.com/opencontainers/go-digest"
)
// todo(richardscothern): from cache control header or config file
const blobTTL = 24 * 7 * time.Hour
type proxyBlobStore struct {
localStore distribution.BlobStore
remoteStore distribution.BlobService
@ -76,13 +72,8 @@ func (pbs *proxyBlobStore) serveLocal(ctx context.Context, w http.ResponseWriter
return false, nil
}
if err == nil {
proxyMetrics.BlobPush(uint64(localDesc.Size))
return true, pbs.localStore.ServeBlob(ctx, w, r, dgst)
}
return false, nil
proxyMetrics.BlobPush(uint64(localDesc.Size))
return true, pbs.localStore.ServeBlob(ctx, w, r, dgst)
}
func (pbs *proxyBlobStore) storeLocal(ctx context.Context, dgst digest.Digest) error {

View file

@ -193,7 +193,7 @@ func makeTestEnv(t *testing.T, name string) *testEnv {
}
func makeBlob(size int) []byte {
blob := make([]byte, size, size)
blob := make([]byte, size)
for i := 0; i < size; i++ {
blob[i] = byte('A' + rand.Int()%48)
}
@ -204,16 +204,6 @@ func init() {
rand.Seed(42)
}
func perm(m []distribution.Descriptor) []distribution.Descriptor {
for i := 0; i < len(m); i++ {
j := rand.Intn(i + 1)
tmp := m[i]
m[i] = m[j]
m[j] = tmp
}
return m
}
func populate(t *testing.T, te *testEnv, blobCount, size, numUnique int) {
var inRemote []distribution.Descriptor

View file

@ -165,11 +165,10 @@ func populateRepo(ctx context.Context, t *testing.T, repository distribution.Rep
t.Fatalf("unexpected error creating test upload: %v", err)
}
rs, ts, err := testutil.CreateRandomTarFile()
rs, dgst, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("unexpected error generating test layer file")
}
dgst := digest.Digest(ts)
if _, err := io.Copy(wr, rs); err != nil {
t.Fatalf("unexpected error copying to upload: %v", err)
}

View file

@ -118,7 +118,7 @@ func (ttles *TTLExpirationScheduler) Start() error {
}
if !ttles.stopped {
return fmt.Errorf("Scheduler already started")
return fmt.Errorf("scheduler already started")
}
dcontext.GetLogger(ttles.ctx).Infof("Starting cached object TTL expiration scheduler...")
@ -126,7 +126,7 @@ func (ttles *TTLExpirationScheduler) Start() error {
// Start timer for each deserialized entry
for _, entry := range ttles.entries {
entry.timer = ttles.startTimer(entry, entry.Expiry.Sub(time.Now()))
entry.timer = ttles.startTimer(entry, time.Until(entry.Expiry))
}
// Start a ticker to periodically save the entries index
@ -164,7 +164,7 @@ func (ttles *TTLExpirationScheduler) add(r reference.Reference, ttl time.Duratio
Expiry: time.Now().Add(ttl),
EntryType: eType,
}
dcontext.GetLogger(ttles.ctx).Infof("Adding new scheduler entry for %s with ttl=%s", entry.Key, entry.Expiry.Sub(time.Now()))
dcontext.GetLogger(ttles.ctx).Infof("Adding new scheduler entry for %s with ttl=%s", entry.Key, time.Until(entry.Expiry))
if oldEntry, present := ttles.entries[entry.Key]; present && oldEntry.timer != nil {
oldEntry.timer.Stop()
}

View file

@ -12,10 +12,18 @@ import (
"syscall"
"time"
"rsc.io/letsencrypt"
logrus_bugsnag "github.com/Shopify/logrus-bugsnag"
logstash "github.com/bshuster-repo/logrus-logstash-hook"
"github.com/bugsnag/bugsnag-go"
"github.com/docker/go-metrics"
gorhandlers "github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/yvasiyarov/gorelic"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"github.com/docker/distribution/configuration"
dcontext "github.com/docker/distribution/context"
"github.com/docker/distribution/health"
@ -23,11 +31,6 @@ import (
"github.com/docker/distribution/registry/listener"
"github.com/docker/distribution/uuid"
"github.com/docker/distribution/version"
"github.com/docker/go-metrics"
gorhandlers "github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/yvasiyarov/gorelic"
)
// this channel gets notified when process receives signal. It is global to ease unit testing
@ -95,6 +98,8 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg
return nil, fmt.Errorf("error configuring logger: %v", err)
}
configureBugsnag(config)
// inject a logger into the uuid library. warns us if there is a problem
// with uuid generation under low entropy.
uuid.Loggerf = dcontext.GetLogger(ctx).Warnf
@ -132,10 +137,26 @@ func (registry *Registry) ListenAndServe() error {
}
if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" {
var tlsMinVersion uint16
if config.HTTP.TLS.MinimumTLS == "" {
tlsMinVersion = tls.VersionTLS10
} else {
switch config.HTTP.TLS.MinimumTLS {
case "tls1.0":
tlsMinVersion = tls.VersionTLS10
case "tls1.1":
tlsMinVersion = tls.VersionTLS11
case "tls1.2":
tlsMinVersion = tls.VersionTLS12
default:
return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS)
}
dcontext.GetLogger(registry.app).Infof("restricting TLS to %s or higher", config.HTTP.TLS.MinimumTLS)
}
tlsConf := &tls.Config{
ClientAuth: tls.NoClientCert,
NextProtos: nextProtos(config),
MinVersion: tls.VersionTLS10,
MinVersion: tlsMinVersion,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
@ -151,19 +172,14 @@ func (registry *Registry) ListenAndServe() error {
if config.HTTP.TLS.Certificate != "" {
return fmt.Errorf("cannot specify both certificate and Let's Encrypt")
}
var m letsencrypt.Manager
if err := m.CacheFile(config.HTTP.TLS.LetsEncrypt.CacheFile); err != nil {
return err
}
if !m.Registered() {
if err := m.Register(config.HTTP.TLS.LetsEncrypt.Email, nil); err != nil {
return err
}
}
if len(config.HTTP.TLS.LetsEncrypt.Hosts) > 0 {
m.SetHosts(config.HTTP.TLS.LetsEncrypt.Hosts)
m := &autocert.Manager{
HostPolicy: autocert.HostWhitelist(config.HTTP.TLS.LetsEncrypt.Hosts...),
Cache: autocert.DirCache(config.HTTP.TLS.LetsEncrypt.CacheFile),
Email: config.HTTP.TLS.LetsEncrypt.Email,
Prompt: autocert.AcceptTOS,
}
tlsConf.GetCertificate = m.GetCertificate
tlsConf.NextProtos = append(tlsConf.NextProtos, acme.ALPNProto)
} else {
tlsConf.Certificates = make([]tls.Certificate, 1)
tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key)
@ -182,7 +198,7 @@ func (registry *Registry) ListenAndServe() error {
}
if ok := pool.AppendCertsFromPEM(caPem); !ok {
return fmt.Errorf("Could not add CA to pool")
return fmt.Errorf("could not add CA to pool")
}
}
@ -229,19 +245,6 @@ func configureReporting(app *handlers.App) http.Handler {
var handler http.Handler = app
if app.Config.Reporting.Bugsnag.APIKey != "" {
bugsnagConfig := bugsnag.Configuration{
APIKey: app.Config.Reporting.Bugsnag.APIKey,
// TODO(brianbland): provide the registry version here
// AppVersion: "2.0",
}
if app.Config.Reporting.Bugsnag.ReleaseStage != "" {
bugsnagConfig.ReleaseStage = app.Config.Reporting.Bugsnag.ReleaseStage
}
if app.Config.Reporting.Bugsnag.Endpoint != "" {
bugsnagConfig.Endpoint = app.Config.Reporting.Bugsnag.Endpoint
}
bugsnag.Configure(bugsnagConfig)
handler = bugsnag.Handler(handler)
}
@ -319,6 +322,32 @@ func logLevel(level configuration.Loglevel) log.Level {
return l
}
// configureBugsnag configures bugsnag reporting, if enabled
func configureBugsnag(config *configuration.Configuration) {
if config.Reporting.Bugsnag.APIKey == "" {
return
}
bugsnagConfig := bugsnag.Configuration{
APIKey: config.Reporting.Bugsnag.APIKey,
}
if config.Reporting.Bugsnag.ReleaseStage != "" {
bugsnagConfig.ReleaseStage = config.Reporting.Bugsnag.ReleaseStage
}
if config.Reporting.Bugsnag.Endpoint != "" {
bugsnagConfig.Endpoint = config.Reporting.Bugsnag.Endpoint
}
bugsnag.Configure(bugsnagConfig)
// configure logrus bugsnag hook
hook, err := logrus_bugsnag.NewBugsnagHook()
if err != nil {
log.Fatalln(err)
}
log.AddHook(hook)
}
// panicHandler add an HTTP handler to web app. The handler recover the happening
// panic. logrus.Panic transmits panic message to pre-config log hooks, which is
// defined in config.yml.

View file

@ -418,7 +418,7 @@ func TestBlobMount(t *testing.T) {
bs := repository.Blobs(ctx)
// Test destination for existence.
statDesc, err = bs.Stat(ctx, desc.Digest)
_, err = bs.Stat(ctx, desc.Digest)
if err == nil {
t.Fatalf("unexpected non-error stating unmounted blob: %v", desc)
}
@ -478,12 +478,12 @@ func TestBlobMount(t *testing.T) {
t.Fatalf("Unexpected error deleting blob")
}
d, err := bs.Stat(ctx, desc.Digest)
_, err = bs.Stat(ctx, desc.Digest)
if err != nil {
t.Fatalf("unexpected error stating blob deleted from source repository: %v", err)
}
d, err = sbs.Stat(ctx, desc.Digest)
d, err := sbs.Stat(ctx, desc.Digest)
if err == nil {
t.Fatalf("unexpected non-error stating deleted blob: %v", d)
}

View file

@ -1,66 +0,0 @@
package storage
import (
"context"
"expvar"
"sync/atomic"
dcontext "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/storage/cache"
)
type blobStatCollector struct {
metrics cache.Metrics
}
func (bsc *blobStatCollector) Hit() {
atomic.AddUint64(&bsc.metrics.Requests, 1)
atomic.AddUint64(&bsc.metrics.Hits, 1)
}
func (bsc *blobStatCollector) Miss() {
atomic.AddUint64(&bsc.metrics.Requests, 1)
atomic.AddUint64(&bsc.metrics.Misses, 1)
}
func (bsc *blobStatCollector) Metrics() cache.Metrics {
return bsc.metrics
}
func (bsc *blobStatCollector) Logger(ctx context.Context) cache.Logger {
return dcontext.GetLogger(ctx)
}
// blobStatterCacheMetrics keeps track of cache metrics for blob descriptor
// cache requests. Note this is kept globally and made available via expvar.
// For more detailed metrics, its recommend to instrument a particular cache
// implementation.
var blobStatterCacheMetrics cache.MetricsTracker = &blobStatCollector{}
func init() {
registry := expvar.Get("registry")
if registry == nil {
registry = expvar.NewMap("registry")
}
cache := registry.(*expvar.Map).Get("cache")
if cache == nil {
cache = &expvar.Map{}
cache.(*expvar.Map).Init()
registry.(*expvar.Map).Set("cache", cache)
}
storage := cache.(*expvar.Map).Get("storage")
if storage == nil {
storage = &expvar.Map{}
storage.(*expvar.Map).Init()
cache.(*expvar.Map).Set("storage", storage)
}
storage.(*expvar.Map).Set("blobdescriptor", expvar.Func(func() interface{} {
// no need for synchronous access: the increments are atomic and
// during reading, we don't care if the data is up to date. The
// numbers will always *eventually* be reported correctly.
return blobStatterCacheMetrics
}))
}

View file

@ -152,16 +152,6 @@ func (bs *blobStore) readlink(ctx context.Context, path string) (digest.Digest,
return linked, nil
}
// resolve reads the digest link at path and returns the blob store path.
func (bs *blobStore) resolve(ctx context.Context, path string) (string, error) {
dgst, err := bs.readlink(ctx, path)
if err != nil {
return "", err
}
return bs.path(dgst)
}
type blobStatter struct {
driver driver.StorageDriver
}

131
registry/storage/cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,131 @@
package cache
import (
"context"
"errors"
"testing"
"github.com/docker/distribution"
digest "github.com/opencontainers/go-digest"
)
func TestCacheSet(t *testing.T) {
cache := newTestStatter()
backend := newTestStatter()
st := NewCachedBlobStatter(cache, backend)
ctx := context.Background()
dgst := digest.Digest("dontvalidate")
_, err := st.Stat(ctx, dgst)
if err != distribution.ErrBlobUnknown {
t.Fatalf("Unexpected error %v, expected %v", err, distribution.ErrBlobUnknown)
}
desc := distribution.Descriptor{
Digest: dgst,
}
if err := backend.SetDescriptor(ctx, dgst, desc); err != nil {
t.Fatal(err)
}
actual, err := st.Stat(ctx, dgst)
if err != nil {
t.Fatal(err)
}
if actual.Digest != desc.Digest {
t.Fatalf("Unexpected descriptor %v, expected %v", actual, desc)
}
if len(cache.sets) != 1 || len(cache.sets[dgst]) == 0 {
t.Fatalf("Expected cache set")
}
if cache.sets[dgst][0].Digest != desc.Digest {
t.Fatalf("Unexpected descriptor %v, expected %v", cache.sets[dgst][0], desc)
}
desc2 := distribution.Descriptor{
Digest: digest.Digest("dontvalidate 2"),
}
cache.sets[dgst] = append(cache.sets[dgst], desc2)
actual, err = st.Stat(ctx, dgst)
if err != nil {
t.Fatal(err)
}
if actual.Digest != desc2.Digest {
t.Fatalf("Unexpected descriptor %v, expected %v", actual, desc)
}
}
func TestCacheError(t *testing.T) {
cache := newErrTestStatter(errors.New("cache error"))
backend := newTestStatter()
st := NewCachedBlobStatter(cache, backend)
ctx := context.Background()
dgst := digest.Digest("dontvalidate")
_, err := st.Stat(ctx, dgst)
if err != distribution.ErrBlobUnknown {
t.Fatalf("Unexpected error %v, expected %v", err, distribution.ErrBlobUnknown)
}
desc := distribution.Descriptor{
Digest: dgst,
}
if err := backend.SetDescriptor(ctx, dgst, desc); err != nil {
t.Fatal(err)
}
actual, err := st.Stat(ctx, dgst)
if err != nil {
t.Fatal(err)
}
if actual.Digest != desc.Digest {
t.Fatalf("Unexpected descriptor %v, expected %v", actual, desc)
}
if len(cache.sets) > 0 {
t.Fatalf("Set should not be called after stat error")
}
}
func newTestStatter() *testStatter {
return &testStatter{
stats: []digest.Digest{},
sets: map[digest.Digest][]distribution.Descriptor{},
}
}
func newErrTestStatter(err error) *testStatter {
return &testStatter{
sets: map[digest.Digest][]distribution.Descriptor{},
err: err,
}
}
type testStatter struct {
stats []digest.Digest
sets map[digest.Digest][]distribution.Descriptor
err error
}
func (s *testStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
if s.err != nil {
return distribution.Descriptor{}, s.err
}
if set := s.sets[dgst]; len(set) > 0 {
return set[len(set)-1], nil
}
return distribution.Descriptor{}, distribution.ErrBlobUnknown
}
func (s *testStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
s.sets[dgst] = append(s.sets[dgst], desc)
return s.err
}
func (s *testStatter) Clear(ctx context.Context, dgst digest.Digest) error {
return s.err
}

View file

@ -54,6 +54,10 @@ func checkBlobDescriptorCacheEmptyRepository(ctx context.Context, t *testing.T,
t.Fatalf("expected error checking for cache item with empty digest: %v", err)
}
if _, err := cache.Stat(ctx, "sha384:cba111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); err != distribution.ErrBlobUnknown {
t.Fatalf("expected unknown blob error with uncached repo: %v", err)
}
if _, err := cache.Stat(ctx, "sha384:abc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); err != distribution.ErrBlobUnknown {
t.Fatalf("expected unknown blob error with empty repo: %v", err)
}
@ -173,8 +177,7 @@ func checkBlobDescriptorCacheClear(ctx context.Context, t *testing.T, provider c
t.Error(err)
}
desc, err = cache.Stat(ctx, localDigest)
if err == nil {
if _, err = cache.Stat(ctx, localDigest); err == nil {
t.Fatalf("expected error statting deleted blob: %v", err)
}
}

View file

@ -4,39 +4,14 @@ import (
"context"
"github.com/docker/distribution"
dcontext "github.com/docker/distribution/context"
prometheus "github.com/docker/distribution/metrics"
"github.com/opencontainers/go-digest"
)
// Metrics is used to hold metric counters
// related to the number of times a cache was
// hit or missed.
type Metrics struct {
Requests uint64
Hits uint64
Misses uint64
}
// Logger can be provided on the MetricsTracker to log errors.
//
// Usually, this is just a proxy to dcontext.GetLogger.
type Logger interface {
Errorf(format string, args ...interface{})
}
// MetricsTracker represents a metric tracker
// which simply counts the number of hits and misses.
type MetricsTracker interface {
Hit()
Miss()
Metrics() Metrics
Logger(context.Context) Logger
}
type cachedBlobStatter struct {
cache distribution.BlobDescriptorService
backend distribution.BlobDescriptorService
tracker MetricsTracker
}
var (
@ -53,47 +28,36 @@ func NewCachedBlobStatter(cache distribution.BlobDescriptorService, backend dist
}
}
// NewCachedBlobStatterWithMetrics creates a new statter which prefers a cache and
// falls back to a backend. Hits and misses will send to the tracker.
func NewCachedBlobStatterWithMetrics(cache distribution.BlobDescriptorService, backend distribution.BlobDescriptorService, tracker MetricsTracker) distribution.BlobStatter {
return &cachedBlobStatter{
cache: cache,
backend: backend,
tracker: tracker,
}
}
func (cbds *cachedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
cacheCount.WithValues("Request").Inc(1)
desc, err := cbds.cache.Stat(ctx, dgst)
if err != nil {
if err != distribution.ErrBlobUnknown {
logErrorf(ctx, cbds.tracker, "error retrieving descriptor from cache: %v", err)
}
goto fallback
// try getting from cache
desc, cacheErr := cbds.cache.Stat(ctx, dgst)
if cacheErr == nil {
cacheCount.WithValues("Hit").Inc(1)
return desc, nil
}
cacheCount.WithValues("Hit").Inc(1)
if cbds.tracker != nil {
cbds.tracker.Hit()
}
return desc, nil
fallback:
cacheCount.WithValues("Miss").Inc(1)
if cbds.tracker != nil {
cbds.tracker.Miss()
}
desc, err = cbds.backend.Stat(ctx, dgst)
// couldn't get from cache; get from backend
desc, err := cbds.backend.Stat(ctx, dgst)
if err != nil {
return desc, err
}
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil {
logErrorf(ctx, cbds.tracker, "error adding descriptor %v to cache: %v", desc.Digest, err)
if cacheErr == distribution.ErrBlobUnknown {
// cache doesn't have info. update it with info got from backend
cacheCount.WithValues("Miss").Inc(1)
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil {
dcontext.GetLoggerWithField(ctx, "blob", dgst).WithError(err).Error("error from cache setting desc")
}
// we don't need to return cache error upstream if any. continue returning value from backend
} else {
// unknown error from cache. just log and error. do not store cache as it may be trigger many set calls
dcontext.GetLoggerWithField(ctx, "blob", dgst).WithError(cacheErr).Error("error from cache stat(ing) blob")
cacheCount.WithValues("Error").Inc(1)
}
return desc, err
return desc, nil
}
func (cbds *cachedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
@ -111,19 +75,7 @@ func (cbds *cachedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) er
func (cbds *cachedBlobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil {
logErrorf(ctx, cbds.tracker, "error adding descriptor %v to cache: %v", desc.Digest, err)
dcontext.GetLoggerWithField(ctx, "blob", dgst).WithError(err).Error("error from cache setting desc")
}
return nil
}
func logErrorf(ctx context.Context, tracker MetricsTracker, format string, args ...interface{}) {
if tracker == nil {
return
}
logger := tracker.Logger(ctx)
if logger == nil {
return
}
logger.Errorf(format, args...)
}

69
registry/storage/cache/metrics/prom.go vendored Normal file
View file

@ -0,0 +1,69 @@
package metrics
import (
"context"
"time"
"github.com/docker/distribution"
prometheus "github.com/docker/distribution/metrics"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/go-metrics"
"github.com/opencontainers/go-digest"
)
type prometheusCacheProvider struct {
cache.BlobDescriptorCacheProvider
latencyTimer metrics.LabeledTimer
}
func NewPrometheusCacheProvider(wrap cache.BlobDescriptorCacheProvider, name, help string) cache.BlobDescriptorCacheProvider {
return &prometheusCacheProvider{
wrap,
// TODO: May want to have fine grained buckets since redis calls are generally <1ms and the default minimum bucket is 5ms.
prometheus.StorageNamespace.NewLabeledTimer(name, help, "operation"),
}
}
func (p *prometheusCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
start := time.Now()
d, e := p.BlobDescriptorCacheProvider.Stat(ctx, dgst)
p.latencyTimer.WithValues("Stat").UpdateSince(start)
return d, e
}
func (p *prometheusCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
start := time.Now()
e := p.BlobDescriptorCacheProvider.SetDescriptor(ctx, dgst, desc)
p.latencyTimer.WithValues("SetDescriptor").UpdateSince(start)
return e
}
type prometheusRepoCacheProvider struct {
distribution.BlobDescriptorService
latencyTimer metrics.LabeledTimer
}
func (p *prometheusRepoCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
start := time.Now()
d, e := p.BlobDescriptorService.Stat(ctx, dgst)
p.latencyTimer.WithValues("RepoStat").UpdateSince(start)
return d, e
}
func (p *prometheusRepoCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
start := time.Now()
e := p.BlobDescriptorService.SetDescriptor(ctx, dgst, desc)
p.latencyTimer.WithValues("RepoSetDescriptor").UpdateSince(start)
return e
}
func (p *prometheusCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
s, err := p.BlobDescriptorCacheProvider.RepositoryScoped(repo)
if err != nil {
return nil, err
}
return &prometheusRepoCacheProvider{
s,
p.latencyTimer,
}, nil
}

View file

@ -7,6 +7,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/cache/metrics"
"github.com/garyburd/redigo/redis"
"github.com/opencontainers/go-digest"
)
@ -34,9 +35,13 @@ type redisBlobDescriptorService struct {
// NewRedisBlobDescriptorCacheProvider returns a new redis-based
// BlobDescriptorCacheProvider using the provided redis connection pool.
func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorCacheProvider {
return &redisBlobDescriptorService{
pool: pool,
}
return metrics.NewPrometheusCacheProvider(
&redisBlobDescriptorService{
pool: pool,
},
"cache_redis",
"Number of seconds taken by redis",
)
}
// RepositoryScoped returns the scoped cache.
@ -181,6 +186,10 @@ func (rsrbds *repositoryScopedRedisBlobDescriptorService) Stat(ctx context.Conte
// We allow a per repository mediatype, let's look it up here.
mediatype, err := redis.String(conn.Do("HGET", rsrbds.blobDescriptorHashKey(dgst), "mediatype"))
if err != nil {
if err == redis.ErrNil {
return distribution.Descriptor{}, distribution.ErrBlobUnknown
}
return distribution.Descriptor{}, err
}

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