Compare commits
179 commits
v2.7.0-rc.
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
f5cdc24dd3 | ||
|
34f1322664 | ||
|
2800ab0224 | ||
|
5dc1f65acc | ||
|
fe1ffa86bc | ||
|
8a8d91529d | ||
|
9466dd4e5a | ||
|
0316f34bf2 | ||
|
e1464fd317 | ||
|
35b26def43 | ||
|
a784441b62 | ||
|
1d0ea8ed7b | ||
|
208d68bd5c | ||
|
f361d443b7 | ||
|
53e18a9d9b | ||
|
7728c5e445 | ||
|
9690d843fa | ||
|
62d0fd45e7 | ||
|
81ba770eff | ||
|
a2ed1b5e80 | ||
|
55f88c35b9 | ||
|
742aab907b | ||
|
17a394f9af | ||
|
78c2ab6646 | ||
|
cdb4ba947a | ||
|
303f1899bb | ||
|
bf56f348be | ||
|
581be91482 | ||
|
be29c05a1e | ||
|
495a4af7cf | ||
|
74d442a058 | ||
|
795892662b | ||
|
ce101280fe | ||
|
4c7c63b557 | ||
|
492a10376c | ||
|
c79aa81772 | ||
|
800cb95821 | ||
|
2d1126ecc1 | ||
|
3b4d1e3ddf | ||
|
1e25ecefe4 | ||
|
db3c418ada | ||
|
98dcc5195e | ||
|
6b972e50fe | ||
|
e65b3f1316 | ||
|
efdba4f210 | ||
|
e9a023180f | ||
|
f1cb8a56c8 | ||
|
bfa6b923e7 | ||
|
f593b8d975 | ||
|
1f77c9a57f | ||
|
861aa2a621 | ||
|
9b6a019081 | ||
|
7e290869e7 | ||
|
016549532f | ||
|
974375f66c | ||
|
4ae059c714 | ||
|
d054b13dc3 | ||
|
244d5246c2 | ||
|
66809646d9 | ||
|
23f6bdd743 | ||
|
a837179414 | ||
|
6ca7b9e9fa | ||
|
f5e84a4939 | ||
|
0f5e2753a6 | ||
|
bdf3438b52 | ||
|
29c300c106 | ||
|
4798651387 | ||
|
f18781257e | ||
|
c9c3324300 | ||
|
5538da4923 | ||
|
fa7d949408 | ||
|
cf77113795 | ||
|
b4694b0d2d | ||
|
070cc010f7 | ||
|
dee21c0394 | ||
|
ae2e973db9 | ||
|
14b96e55d8 | ||
|
f656e60de5 | ||
|
740d4d1211 | ||
|
cc97b94f5d | ||
|
aeaeb84407 | ||
|
07a50201c9 | ||
|
1c481d34d9 | ||
|
a994bb839d | ||
|
c486db2d71 | ||
|
1fb7fffdb2 | ||
|
10f726344d | ||
|
8063102951 | ||
|
438b67feef | ||
|
be07be9904 | ||
|
92d213d2c1 | ||
|
dd3bdee21c | ||
|
90dfea7952 | ||
|
6c72ec2e85 | ||
|
ec84b86013 | ||
|
898b1f2a53 | ||
|
c5d5f938e3 | ||
|
3800c47fd2 | ||
|
a45e5cb13f | ||
|
8b31a894bd | ||
|
94097512db | ||
|
b23dd1ef37 | ||
|
79f6bcbe16 | ||
|
afe29bb697 | ||
|
45b2d0498d | ||
|
84f47e7bb3 | ||
|
6c329e56a2 | ||
|
71309e96e4 | ||
|
0c394fdd84 | ||
|
d80a17d8e0 | ||
|
55287010ce | ||
|
c2254191d4 | ||
|
5223c27422 | ||
|
dcfe05ce6c | ||
|
8f9c8094fb | ||
|
0e2d080a8a | ||
|
3226863cbc | ||
|
5afbf32400 | ||
|
74f429a5ad | ||
|
51bb5cee5b | ||
|
fd77cf43a6 | ||
|
c18c6c33b2 | ||
|
f877726503 | ||
|
6d62eb1d4a | ||
|
3390f32aec | ||
|
ae91d1f429 | ||
|
6e10631d9c | ||
|
e1e72e9563 | ||
|
f9a0506191 | ||
|
d3ddc3572c | ||
|
c192a281f8 | ||
|
a683c7c235 | ||
|
bbc9885aa2 | ||
|
92a6436714 | ||
|
d5a615b8c9 | ||
|
09a63caa37 | ||
|
228bafca0b | ||
|
76da6290b0 | ||
|
8b70616846 | ||
|
ec6566c02b | ||
|
3aa2a282f7 | ||
|
0d3efadf01 | ||
|
48818fdea7 | ||
|
411d6bcfd2 | ||
|
da8db4666b | ||
|
b75069ef13 | ||
|
b1fd12d0c1 | ||
|
15b0204758 | ||
|
cdb62b2b77 | ||
|
7df881dcbe | ||
|
91b0f0559e | ||
|
980d1a696b | ||
|
d1abdeb623 | ||
|
0ac367fd6b | ||
|
eb1a2cd911 | ||
|
b7839211af | ||
|
90bed67126 | ||
|
40b7b5830a | ||
|
08c6bbed05 | ||
|
d9e1218235 | ||
|
63f6c1205d | ||
|
aa985ba889 | ||
|
dd36fd3622 | ||
|
1251e51ad0 | ||
|
9ebf151ac2 | ||
|
7c4d584e58 | ||
|
93e082742a | ||
|
f7046a6d68 | ||
|
cd1648d62c | ||
|
8a800e1292 | ||
|
1cb4180b1a | ||
|
451cd548a0 | ||
|
6335cc258f | ||
|
de8636b78c | ||
|
2e1e6307dd | ||
|
b2bd465760 | ||
|
f730f3ab77 | ||
|
e69837454a | ||
|
c9eba1a5bb |
1193 changed files with 366507 additions and 62757 deletions
3
.github/CODE_OF_CONDUCT.md
vendored
Normal file
3
.github/CODE_OF_CONDUCT.md
vendored
Normal 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
20
.golangci.yml
Normal 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
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"Vendor": true,
|
||||
"Deadline": "2m",
|
||||
"Sort": ["linter", "severity", "path", "line"],
|
||||
"EnableGC": true,
|
||||
"Enable": [
|
||||
"structcheck",
|
||||
"staticcheck",
|
||||
"unconvert",
|
||||
|
||||
"gofmt",
|
||||
"goimports",
|
||||
"golint",
|
||||
"vet"
|
||||
]
|
||||
}
|
13
.travis.yml
13
.travis.yml
|
@ -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
|
||||
|
|
135
CONTRIBUTING.md
135
CONTRIBUTING.md
|
@ -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`.
|
||||
|
|
22
Dockerfile
22
Dockerfile
|
@ -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
144
GOVERNANCE.md
Normal 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.
|
253
MAINTAINERS
253
MAINTAINERS
|
@ -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"
|
||||
|
|
6
Makefile
6
Makefile
|
@ -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) $@"
|
||||
|
|
91
README.md
91
README.md
|
@ -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/>
|
||||
|
||||
[](https://circleci.com/gh/docker/distribution/tree/master)
|
||||
[](https://travis-ci.org/docker/distribution)
|
||||
[](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/.
|
||||
|
|
2
blobs.go
2
blobs.go
|
@ -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 (
|
||||
|
|
|
@ -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*`)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
70
configuration/parser_test.go
Normal file
70
configuration/parser_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 .`
|
||||
|
|
|
@ -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")
|
||||
|
|
80
contrib/token-server/token_test.go
Normal file
80
contrib/token-server/token_test.go
Normal 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
|
||||
}
|
|
@ -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 |
|
||||
|-----------|----------|-------------------------------------------------------|
|
||||
|
|
184
docs/spec/api.md
184
docs/spec/api.md
|
@ -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": [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
41
docs/spec/deprecated-schema-v1.md
Normal file
41
docs/spec/deprecated-schema-v1.md
Normal 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
47
go.mod
Normal 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
176
go.sum
Normal 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=
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(`{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(`{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", ¬ifications)
|
||||
|
||||
// register prometheus metrics
|
||||
metrics.Register(prometheus.NotificationsNamespace)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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\""
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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, ";") +
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
}
|
|
@ -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
131
registry/storage/cache/cache_test.go
vendored
Normal 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
|
||||
}
|
7
registry/storage/cache/cachecheck/suite.go
vendored
7
registry/storage/cache/cachecheck/suite.go
vendored
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
69
registry/storage/cache/metrics/prom.go
vendored
Normal 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
|
||||
}
|
15
registry/storage/cache/redis/redis.go
vendored
15
registry/storage/cache/redis/redis.go
vendored
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue