Compare commits

...

34 Commits

Author SHA1 Message Date
Antonio Murdaca
8094910c9a adapt code for projectatomic github
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-08 09:32:20 +01:00
Antonio Murdaca
82b121caf1 update docker code and adapt code
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 16:50:37 +01:00
Antonio Murdaca
89631ab4f1 fix building readme
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 13:08:51 +01:00
Antonio Murdaca
4d7ac1999e add another todo about v2 only
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 13:04:49 +01:00
Antonio Murdaca
370f1bc685 reg v1 setup wip
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 13:02:52 +01:00
Antonio Murdaca
51c104bde0 registry binary v1 working
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 12:57:28 +01:00
Antonio Murdaca
286ab4e144 build registry v1 for testing
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 11:48:16 +01:00
Antonio Murdaca
34fed9cabc update todo
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 10:54:15 +01:00
Antonio Murdaca
01b3c23ffa add todo
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 09:06:19 +01:00
Antonio Murdaca
7fc29e2323 fix readme
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 08:55:45 +01:00
Antonio Murdaca
b03817412b add dnf for fedora installation
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 08:55:03 +01:00
Antonio Murdaca
03b19aa069 fix readme
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-04 08:53:10 +01:00
Antonio Murdaca
69b84ce650 fix building section in readme
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-03 08:10:26 +01:00
Antonio Murdaca
044e9b5390 bump v0.1.10-dev
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-29 12:20:40 +01:00
Antonio Murdaca
30db2ad7fc bump v0.1.9
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-29 12:20:12 +01:00
Antonio Murdaca
b8d3588b54 bump v0.1.9-dev
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-29 12:18:52 +01:00
Antonio Murdaca
3eefe215e0 support insecure registries
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-29 11:44:51 +01:00
Antonio Murdaca
e5d9bee80c remove img-type
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-29 11:27:43 +01:00
Antonio Murdaca
7d7fb3c4b0 remote sudo_user, upstream has it
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-27 11:49:51 +01:00
Antonio Murdaca
8449c6ae07 bump docker to 1.10.2
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-23 00:25:23 +01:00
Antonio Murdaca
b77535feee add todo authconfigs fallthrough
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-22 15:04:55 +01:00
Antonio Murdaca
a09019661e support $SUDO_USER
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-22 14:39:12 +01:00
Antonio Murdaca
2986c6d0d6 return nicer error
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-07 18:14:24 +01:00
Antonio Murdaca
01b47e3681 default to inspect docker images
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-07 18:13:30 +01:00
Antonio Murdaca
1d452edce6 make space for appc img inspect :)
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-07 18:07:01 +01:00
Antonio Murdaca
3de433ea4a re-enable and fix failing tests
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-02 13:51:10 +01:00
Antonio Murdaca
143ff7165d fix readme
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-02 13:38:21 +01:00
Antonio Murdaca
6cfd14f0c0 lots of todo and fixes
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-02 11:44:49 +01:00
Antonio Murdaca
8d22756979 move infof to debugf to not clutter stderr
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-02-01 17:45:19 +01:00
Antonio Murdaca
55804fad16 add todo
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-01-29 10:30:22 +01:00
Antonio Murdaca
71e0fec389 bump v0.1.5-dev
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-01-29 10:15:09 +01:00
Antonio Murdaca
98aca9c188 bump v0.1.4
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-01-29 10:12:37 +01:00
Antonio Murdaca
a13bacf8ad fix by Jan
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-01-28 23:58:58 +01:00
Antonio Murdaca
45007a3006 spec for v0.1.3 & bump v0.1.4-dev
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-01-28 19:02:28 +01:00
104 changed files with 2590 additions and 2216 deletions

View File

@@ -1,14 +1,21 @@
FROM fedora
RUN dnf -y update && dnf install -y make git golang golang-github-cpuguy83-go-md2man golang-github-Sirupsen-logrus-devel golang-github-codegangsta-cli-devel golang-golangorg-net-devel
RUN dnf -y update && dnf install -y make git golang golang-github-cpuguy83-go-md2man \
# registry v1 deps
xz-devel \
python-devel \
python-pip \
swig \
redhat-rpm-config \
openssl-devel \
patch
# Install two versions of the registry. The first is an older version that
# Install three versions of the registry. The first is an older version that
# only supports schema1 manifests. The second is a newer version that supports
# both. This allows integration-cli tests to cover push/pull with both schema1
# and schema2 manifests.
# and schema2 manifests. Install registry v1 also.
ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd
ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827
#ENV REGISTRY_COMMIT_v1 TODO(runcom)
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \
@@ -18,14 +25,21 @@ RUN set -x \
&& (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \
&& GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \
go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \
&& rm -rf "$GOPATH"
&& rm -rf "$GOPATH" \
&& export DRV1="$(mktemp -d)" \
&& git clone https://github.com/docker/docker-registry.git "$DRV1" \
# no need for setuptools since we have a version conflict with fedora
&& sed -i.bak s/setuptools==5.8//g "$DRV1/requirements/main.txt" \
&& sed -i.bak s/setuptools==5.8//g "$DRV1/depends/docker-registry-core/requirements/main.txt" \
&& pip install "$DRV1/depends/docker-registry-core" \
&& pip install file://"$DRV1#egg=docker-registry[bugsnag,newrelic,cors]" \
&& patch $(python -c 'import boto; import os; print os.path.dirname(boto.__file__)')/connection.py \
< "$DRV1/contrib/boto_header_patch.diff" \
&& dnf -y update && dnf install -y m2crypto
ENV GOPATH /usr/share/gocode:/go
WORKDIR /go/src/github.com/runcom/skopeo
WORKDIR /go/src/github.com/projectatomic/skopeo
COPY . /go/src/github.com/projectatomic/skopeo
#ENTRYPOINT ["hack/dind"]
COPY . /go/src/github.com/runcom/skopeo
# remove distro-supplied dependencies, so we build against them and the rest from vendor/
RUN rm -rf /go/src/github.com/runcom/skopeo/vendor/golang.org && rm -rf /go/src/github.com/runcom/skopeo/vendor/github.com/Sirupsen && rm -rf /go/src/github.com/runcom/skopeo/vendor/github.com/codegangsta

205
LICENSE
View File

@@ -1,20 +1,191 @@
Copyright (c) 2016 Antonio Murdaca <runcom@redhat.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2016 Antonio Murdaca <runcom@redhat.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -2,8 +2,9 @@
export GO15VENDOREXPERIMENT=1
BINDIR=${DESTDIR}/usr/bin/
MANDIR=${DESTDIR}/usr/share/man
PREFIX ?= ${DESTDIR}/usr
INSTALLDIR=${PREFIX}/bin
MANINSTALLDIR=${PREFIX}/share/man
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
DOCKER_IMAGE := skopeo-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
@@ -31,11 +32,11 @@ clean:
rm -f skopeo.1
install: install-binary
install -m 644 skopeo.1 ${MANDIR}/man1/
install -m 644 skopeo.1 ${MANINSTALLDIR}/man1/
install-binary:
install -d -m 0755 ${BINDIR}
install -m 755 skopeo ${BINDIR}
install -d -m 0755 ${INSTALLDIR}
install -m 755 skopeo ${INSTALLDIR}
man:
go-md2man -in man/skopeo.1.md -out skopeo.1

View File

@@ -1,11 +1,11 @@
skopeo [![Build Status](https://travis-ci.org/runcom/skopeo.svg?branch=master)](https://travis-ci.org/runcom/skopeo)
skopeo [![Build Status](https://travis-ci.org/projectatomic/skopeo.svg?branch=master)](https://travis-ci.org/projectatomic/skopeo)
=
_Please be aware `skopeo` is still work in progress_
`skopeo` is a command line utility which is able to _inspect_ a repository on a Docker registry.
By _inspect_ I mean it fetches the repository's manifest and it is able to show you a `docker inspect`-like
json output about a whole repository or a tag. This tool, in constrast to `docker inspect`, helps you gather useful information about
json output about a whole repository or a tag. This tool, in contrast to `docker inspect`, helps you gather useful information about
a repository or a tag before pulling it (using disk space) - e.g. - which tags are available for the given repository? which labels the image has?
Examples:
@@ -66,19 +66,10 @@ $ skopeo myregistrydomain.com:5000/busybox
# let's try now to fake a non existent Docker's config file
$ skopeo --docker-cfg="" myregistrydomain.com:5000/busybox
INFO[0000] Docker cli config file not found: stat : no such file or directory, falling back to --username and --password
FATA[0000] unable to ping registry endpoint http://myregistrydomain.com:5000/v0/
v2 ping attempt failed with error: Get http://myregistrydomain.com:5000/v2/: malformed HTTP response "\x15\x03\x01\x00\x02\x02"
v1 ping attempt failed with error: Get http://myregistrydomain.com:5000/v1/_ping: malformed HTTP response "\x15\x03\x01\x00\x02\x02"
FATA[0000] Get https://myregistrydomain.com:5000/v2/busybox/manifests/latest: no basic auth credentials
# we can see the cli config isn't found so it looks for --username and --password
# but because I didn't provide them I can't auth to the registry and I receive the above error
INFO[0000] Docker cli config file not found: stat : no such file or directory, falling back to --username and --password
# passing --username and --password - we can see that everything goes fine despite an info showing
# cli config can't be found
# passing --username and --password - we can see that everything goes fine
$ skopeo --docker-cfg="" --username=testuser --password=testpassword myregistrydomain.com:5000/busybox
INFO[0000] Docker cli config file not found: stat : no such file or directory, falling back to --username and --password
{"Tag":"latest","Digest":"sha256:473bb2189d7b913ed7187a33d11e743fdc2f88931122a44d91a301b64419f092","RepoTags":["latest"],"Comment":"","Created":"2016-01-15T18:06:41.282540103Z","ContainerConfig":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"DockerVersion":"1.8.3","Author":"","Config":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"Architecture":"amd64","Os":"linux"}
```
If your cli config is found but it doesn't contain the necessary credentials for the queried registry
@@ -88,9 +79,11 @@ Building
-
To build `skopeo` you need at least Go 1.5 because it uses the latest `GO15VENDOREXPERIMENT` flag. Also, make sure to clone the repository in your `GOPATH` - otherwise compilation fails.
```sh
$ cd $GOPATH/src/github.com # make sure you have github.com folder otherwise just create it
$ git clone https://github.com/runcom/skopeo
$ cd runcom && make binary
$ cd $GOPATH/src
$ mkdir -p github.com/projectatomic
$ cd projectatomic
$ git clone https://github.com/projectatomic/skopeo
$ cd skopeo && make binary
```
Man:
-
@@ -100,9 +93,14 @@ $ make man
```
Installing
-
If you built from source:
```sh
$ sudo make install
```
`skopeo` is also available from Fedora 23:
```sh
sudo dnf install skopeo
```
Tests
-
_You need Docker installed on your system in order to run the test suite_
@@ -111,8 +109,13 @@ $ make test-integration
```
TODO
-
- make skopeo docker registry v2 only
- output raw manifest
- download layers and support docker load tar(s)
- get rid of docker/docker code (?)
- show repo tags via flag or when reference isn't tagged or digested
- add tests (integration with deployed registries in container - Docker-like)
- support rkt/appc image spec
NOT TODO
-
@@ -120,4 +123,4 @@ NOT TODO
License
-
MIT
ASL 2.0

345
docker/inspect.go Normal file
View File

@@ -0,0 +1,345 @@
package docker
import (
"encoding/json"
"fmt"
"strings"
"syscall"
"time"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/distribution/digest"
distreference "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client"
"github.com/docker/docker/api"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/distribution"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/image"
"github.com/docker/docker/opts"
versionPkg "github.com/docker/docker/pkg/version"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
engineTypes "github.com/docker/engine-api/types"
registryTypes "github.com/docker/engine-api/types/registry"
"github.com/projectatomic/skopeo/types"
"golang.org/x/net/context"
)
// fallbackError wraps an error that can possibly allow fallback to a different
// endpoint.
type fallbackError struct {
// err is the error being wrapped.
err error
// confirmedV2 is set to true if it was confirmed that the registry
// supports the v2 protocol. This is used to limit fallbacks to the v1
// protocol.
confirmedV2 bool
transportOK bool
}
// Error renders the FallbackError as a string.
func (f fallbackError) Error() string {
return f.err.Error()
}
type manifestFetcher interface {
Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error)
}
func validateName(name string) error {
distref, err := distreference.ParseNamed(name)
if err != nil {
return err
}
hostname, _ := distreference.SplitHostname(distref)
if hostname == "" {
return fmt.Errorf("Please use a fully qualified repository name")
}
return nil
}
func GetData(c *cli.Context, name string) (*types.ImageInspect, error) {
if err := validateName(name); err != nil {
return nil, err
}
ref, err := reference.ParseNamed(name)
if err != nil {
return nil, err
}
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, err
}
authConfig, err := getAuthConfig(c, repoInfo.Index)
if err != nil {
return nil, err
}
if err := validateRepoName(repoInfo.Name()); err != nil {
return nil, err
}
options := &registry.Options{}
options.Mirrors = opts.NewListOpts(nil)
options.InsecureRegistries = opts.NewListOpts(nil)
options.InsecureRegistries.Set("0.0.0.0/0")
registryService := registry.NewService(options)
// TODO(runcom): hacky, provide a way of passing tls cert (flag?) to be used to lookup
for _, ic := range registryService.Config.IndexConfigs {
ic.Secure = false
}
endpoints, err := registryService.LookupPullEndpoints(repoInfo.Hostname())
if err != nil {
return nil, err
}
logrus.Debugf("endpoints: %v", endpoints)
var (
ctx = context.Background()
lastErr error
discardNoSupportErrors bool
imgInspect *types.ImageInspect
confirmedV2 bool
confirmedTLSRegistries = make(map[string]struct{})
)
for _, endpoint := range endpoints {
// make sure I can reach the registry, same as docker pull does
v1endpoint, err := endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), nil)
if err != nil {
return nil, err
}
if _, err := v1endpoint.Ping(); err != nil {
if strings.Contains(err.Error(), "timeout") {
return nil, err
}
continue
}
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
continue
}
if endpoint.URL.Scheme != "https" {
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
continue
}
}
logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
//fetcher, err := newManifestFetcher(endpoint, repoInfo, config)
fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService)
if err != nil {
lastErr = err
continue
}
if imgInspect, err = fetcher.Fetch(ctx, ref); err != nil {
// Was this fetch cancelled? If so, don't try to fall back.
fallback := false
select {
case <-ctx.Done():
default:
if fallbackErr, ok := err.(fallbackError); ok {
fallback = true
confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
if fallbackErr.transportOK && endpoint.URL.Scheme == "https" {
confirmedTLSRegistries[endpoint.URL.Host] = struct{}{}
}
err = fallbackErr.err
}
}
if fallback {
if _, ok := err.(distribution.ErrNoSupport); !ok {
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
discardNoSupportErrors = true
// save the current error
lastErr = err
} else if !discardNoSupportErrors {
// Save the ErrNoSupport error, because it's either the first error or all encountered errors
// were also ErrNoSupport errors.
lastErr = err
}
continue
}
logrus.Errorf("Not continuing with pull after error: %v", err)
return nil, err
}
return imgInspect, nil
}
if lastErr == nil {
lastErr = fmt.Errorf("no endpoints found for %s", ref.String())
}
return nil, lastErr
}
func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig engineTypes.AuthConfig, registryService *registry.Service) (manifestFetcher, error) {
switch endpoint.Version {
case registry.APIVersion2:
return &v2ManifestFetcher{
endpoint: endpoint,
authConfig: authConfig,
service: registryService,
repoInfo: repoInfo,
}, nil
case registry.APIVersion1:
return &v1ManifestFetcher{
endpoint: endpoint,
authConfig: authConfig,
service: registryService,
repoInfo: repoInfo,
}, nil
}
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
}
func getAuthConfig(c *cli.Context, index *registryTypes.IndexInfo) (engineTypes.AuthConfig, error) {
var (
username = c.GlobalString("username")
password = c.GlobalString("password")
cfg = c.GlobalString("docker-cfg")
defAuthConfig = engineTypes.AuthConfig{
Username: c.GlobalString("username"),
Password: c.GlobalString("password"),
Email: "stub@example.com",
}
)
//
// FINAL TODO(runcom): avoid returning empty config! just fallthrough and return
// the first useful authconfig
//
// TODO(runcom): ??? atomic needs this
// TODO(runcom): implement this to opt-in for docker-cfg, no need to make this
// work by default with docker's conf
//useDockerConf := c.GlobalString("use-docker-cfg")
if username != "" && password != "" {
return defAuthConfig, nil
}
confFile, err := cliconfig.Load(cfg)
if err != nil {
return engineTypes.AuthConfig{}, err
}
authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index)
logrus.Debugf("authConfig for %s: %v", index.Name, authConfig)
return authConfig, nil
}
func validateRepoName(name string) error {
if name == "" {
return fmt.Errorf("Repository name can't be empty")
}
if name == api.NoBaseImageSpecifier {
return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier)
}
return nil
}
func makeImageInspect(img *image.Image, tag string, dgst digest.Digest, tagList []string) *types.ImageInspect {
var digest string
if err := dgst.Validate(); err == nil {
digest = dgst.String()
}
return &types.ImageInspect{
Tag: tag,
Digest: digest,
RepoTags: tagList,
Comment: img.Comment,
Created: img.Created.Format(time.RFC3339Nano),
ContainerConfig: &img.ContainerConfig,
DockerVersion: img.DockerVersion,
Author: img.Author,
Config: img.Config,
Architecture: img.Architecture,
Os: img.OS,
}
}
func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) {
var dver struct {
DockerVersion string `json:"docker_version"`
}
if err := json.Unmarshal(imageJSON, &dver); err != nil {
return nil, err
}
useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3")
if useFallback {
var v1Image image.V1Image
err := json.Unmarshal(imageJSON, &v1Image)
if err != nil {
return nil, err
}
imageJSON, err = json.Marshal(v1Image)
if err != nil {
return nil, err
}
}
var c map[string]*json.RawMessage
if err := json.Unmarshal(imageJSON, &c); err != nil {
return nil, err
}
c["rootfs"] = rawJSON(rootfs)
c["history"] = rawJSON(history)
return c, nil
}
func rawJSON(value interface{}) *json.RawMessage {
jsonval, err := json.Marshal(value)
if err != nil {
return nil
}
return (*json.RawMessage)(&jsonval)
}
func continueOnError(err error) bool {
switch v := err.(type) {
case errcode.Errors:
if len(v) == 0 {
return true
}
return continueOnError(v[0])
case distribution.ErrNoSupport:
return continueOnError(v.Err)
case errcode.Error:
return shouldV2Fallback(v)
case *client.UnexpectedHTTPResponseError:
return true
case ImageConfigPullError:
return false
case error:
return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error()))
}
// let's be nice and fallback if the error is a completely
// unexpected one.
// If new errors have to be handled in some way, please
// add them to the switch above.
return true
}
// shouldV2Fallback returns true if this error is a reason to fall back to v1.
func shouldV2Fallback(err errcode.Error) bool {
switch err.Code {
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
return true
}
return false
}

View File

@@ -1,4 +1,4 @@
package main
package docker
import (
"encoding/json"
@@ -9,11 +9,14 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/registry/client/transport"
dockerdistribution "github.com/docker/docker/distribution"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/image"
"github.com/docker/docker/image/v1"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
engineTypes "github.com/docker/engine-api/types"
"github.com/projectatomic/skopeo/types"
"golang.org/x/net/context"
)
@@ -23,18 +26,18 @@ type v1ManifestFetcher struct {
repo distribution.Repository
confirmedV2 bool
// wrap in a config?
authConfig types.AuthConfig
authConfig engineTypes.AuthConfig
service *registry.Service
session *registry.Session
}
func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) {
func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) {
var (
imgInspect *imageInspect
imgInspect *types.ImageInspect
)
if _, isCanonical := ref.(reference.Canonical); isCanonical {
// Allowing fallback, because HTTPS v1 is before HTTP v2
return nil, fallbackError{err: registry.ErrNoSupport{errors.New("Cannot pull by digest with v1 registry")}}
return nil, fallbackError{err: dockerdistribution.ErrNoSupport{errors.New("Cannot pull by digest with v1 registry")}}
}
tlsConfig, err := mf.service.TLSConfig(mf.repoInfo.Index.Name)
if err != nil {
@@ -44,11 +47,11 @@ func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i
tr := transport.NewTransport(
registry.NewTransport(tlsConfig),
//registry.DockerHeaders(mf.config.MetaHeaders)...,
registry.DockerHeaders(nil)...,
registry.DockerHeaders(dockerversion.DockerUserAgent(), nil)...,
)
client := registry.HTTPClient(tr)
//v1Endpoint, err := mf.endpoint.ToV1Endpoint(mf.config.MetaHeaders)
v1Endpoint, err := mf.endpoint.ToV1Endpoint(nil)
v1Endpoint, err := mf.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), nil)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return nil, fallbackError{err: err}
@@ -65,7 +68,7 @@ func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i
return imgInspect, nil
}
func (mf *v1ManifestFetcher) fetchWithSession(ctx context.Context, ref reference.Named) (*imageInspect, error) {
func (mf *v1ManifestFetcher) fetchWithSession(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) {
repoData, err := mf.session.GetRepositoryData(mf.repoInfo)
if err != nil {
if strings.Contains(err.Error(), "HTTP code: 404") {

View File

@@ -1,4 +1,4 @@
package main
package docker
import (
"encoding/json"
@@ -19,7 +19,8 @@ import (
"github.com/docker/docker/image/v1"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
engineTypes "github.com/docker/engine-api/types"
"github.com/projectatomic/skopeo/types"
"golang.org/x/net/context"
)
@@ -29,13 +30,13 @@ type v2ManifestFetcher struct {
repo distribution.Repository
confirmedV2 bool
// wrap in a config?
authConfig types.AuthConfig
authConfig engineTypes.AuthConfig
service *registry.Service
}
func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) {
func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) {
var (
imgInspect *imageInspect
imgInspect *types.ImageInspect
err error
)
@@ -43,7 +44,7 @@ func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i
mf.repo, mf.confirmedV2, err = dockerdistribution.NewV2Repository(ctx, mf.repoInfo, mf.endpoint, nil, &mf.authConfig, "pull")
if err != nil {
logrus.Debugf("Error getting v2 registry: %v", err)
return nil, fallbackError{err: err, confirmedV2: mf.confirmedV2}
return nil, err
}
imgInspect, err = mf.fetchWithRepository(ctx, ref)
@@ -51,14 +52,15 @@ func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i
if _, ok := err.(fallbackError); ok {
return nil, err
}
if registry.ContinueOnError(err) {
err = fallbackError{err: err, confirmedV2: mf.confirmedV2}
if continueOnError(err) {
logrus.Errorf("Error trying v2 registry: %v", err)
return nil, fallbackError{err: err, confirmedV2: mf.confirmedV2, transportOK: true}
}
}
return imgInspect, err
}
func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) (*imageInspect, error) {
func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) {
var (
manifest distribution.Manifest
tagOrDigest string // Used for logging/progress only
@@ -293,7 +295,7 @@ func (mf *v2ManifestFetcher) pullSchema2(ctx context.Context, ref reference.Name
go func() {
configJSON, err := mf.pullSchema2ImageConfig(ctx, target.Digest)
if err != nil {
errChan <- err
errChan <- ImageConfigPullError{Err: err}
cancel()
return
}
@@ -368,6 +370,17 @@ func receiveConfig(configChan <-chan []byte, errChan <-chan error) ([]byte, imag
}
}
// ImageConfigPullError is an error pulling the image config blob
// (only applies to schema2).
type ImageConfigPullError struct {
Err error
}
// Error returns the error string for ImageConfigPullError.
func (e ImageConfigPullError) Error() string {
return "error pulling image configuration: " + e.Err.Error()
}
// allowV1Fallback checks if the error is a possible reason to fallback to v1
// (even if confirmedV2 has been set already), and if so, wraps the error in
// a fallbackError with confirmedV2 set to false. Otherwise, it returns the
@@ -376,13 +389,13 @@ func allowV1Fallback(err error) error {
switch v := err.(type) {
case errcode.Errors:
if len(v) != 0 {
if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
return fallbackError{err: err, confirmedV2: false}
if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) {
return fallbackError{err: err, confirmedV2: false, transportOK: true}
}
}
case errcode.Error:
if registry.ShouldV2Fallback(v) {
return fallbackError{err: err, confirmedV2: false}
if shouldV2Fallback(v) {
return fallbackError{err: err, confirmedV2: false, transportOK: true}
}
}
return err

View File

@@ -19,7 +19,7 @@ set -e
set -o pipefail
export SKOPEO_PKG='github.com/runcom/skopeo'
export SKOPEO_PKG='github.com/projectatomic/skopeo'
export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export MAKEDIR="$SCRIPTDIR/make"

View File

@@ -6,19 +6,21 @@ rm -rf vendor/
source 'hack/.vendor-helpers.sh'
clone git github.com/codegangsta/cli v1.2.0
clone git github.com/Sirupsen/logrus v0.8.7 # logrus is a common dependency among multiple deps
clone git github.com/docker/docker master
clone git github.com/Sirupsen/logrus v0.8.7
clone git github.com/vbatts/tar-split v0.9.11
clone git github.com/gorilla/mux master
clone git github.com/gorilla/context master
clone git golang.org/x/net master https://github.com/golang/net.git
clone git github.com/docker/engine-api master
clone git github.com/docker/distribution v2.3.0-rc.1
clone git github.com/go-check/check v1
clone git github.com/docker/docker 29bade2cd0a09191279f04ebc6aeedaa70c772a0
clone git github.com/docker/engine-api 7f6071353fc48f69d2328c4ebe8f3bd0f7c75da4
clone git github.com/docker/distribution 7b66c50bb7e0e4b3b83f8fd134a9f6ea4be08b57
clone git github.com/docker/go-connections master
clone git github.com/docker/go-units master
clone git github.com/docker/libtrust master
clone git github.com/opencontainers/runc master
clone git github.com/vbatts/tar-split master
clone git github.com/gorilla/mux master
clone git github.com/gorilla/context master
clean

View File

@@ -1,303 +1,55 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/distribution/digest"
distreference "github.com/docker/distribution/reference"
"github.com/docker/docker/api"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/image"
versionPkg "github.com/docker/docker/pkg/version"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
types "github.com/docker/engine-api/types"
containerTypes "github.com/docker/engine-api/types/container"
registryTypes "github.com/docker/engine-api/types/registry"
"golang.org/x/net/context"
"github.com/projectatomic/skopeo/docker"
"github.com/projectatomic/skopeo/types"
)
// fallbackError wraps an error that can possibly allow fallback to a different
// endpoint.
type fallbackError struct {
// err is the error being wrapped.
err error
// confirmedV2 is set to true if it was confirmed that the registry
// supports the v2 protocol. This is used to limit fallbacks to the v1
// protocol.
confirmedV2 bool
}
type imgKind int
// Error renders the FallbackError as a string.
func (f fallbackError) Error() string {
return f.err.Error()
}
const (
imgTypeDocker = "docker://"
imgTypeAppc = "appc://"
type manifestFetcher interface {
Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error)
}
kindUnknown = iota
kindDocker
kindAppc
)
type imageInspect struct {
Tag string
Digest string
RepoTags []string
Comment string
Created string
ContainerConfig *containerTypes.Config
DockerVersion string
Author string
Config *containerTypes.Config
Architecture string
Os string
}
func validateName(name string) error {
distref, err := distreference.ParseNamed(name)
if err != nil {
return err
func getImgType(img string) imgKind {
if strings.HasPrefix(img, imgTypeDocker) {
return kindDocker
}
hostname, _ := distreference.SplitHostname(distref)
if hostname == "" {
return fmt.Errorf("Please use a fully qualified repository name")
if strings.HasPrefix(img, imgTypeAppc) {
return kindAppc
}
return nil
// TODO(runcom): v2 will support this
//return kindUnknown
return kindDocker
}
func inspect(c *cli.Context) (*imageInspect, error) {
name := c.Args().First()
if err := validateName(name); err != nil {
return nil, err
}
ref, err := reference.ParseNamed(name)
if err != nil {
return nil, err
}
imgInspect, err := getData(c, ref)
if err != nil {
return nil, err
func inspect(c *cli.Context) (*types.ImageInspect, error) {
var (
imgInspect *types.ImageInspect
err error
name = c.Args().First()
kind = getImgType(name)
)
switch kind {
case kindDocker:
imgInspect, err = docker.GetData(c, strings.Replace(name, imgTypeDocker, "", -1))
if err != nil {
return nil, err
}
case kindAppc:
return nil, fmt.Errorf("not implemented yet")
default:
return nil, fmt.Errorf("%s image is invalid, please use either 'docker://' or 'appc://'", name)
}
return imgInspect, nil
}
func getData(c *cli.Context, ref reference.Named) (*imageInspect, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, err
}
authConfig, err := getAuthConfig(c, repoInfo.Index)
if err != nil {
return nil, err
}
if err := validateRepoName(repoInfo.Name()); err != nil {
return nil, err
}
//options := &registry.Options{}
//options.Mirrors = opts.NewListOpts(nil)
//options.InsecureRegistries = opts.NewListOpts(nil)
//options.InsecureRegistries.Set("0.0.0.0/0")
//registryService := registry.NewService(options)
registryService := registry.NewService(nil)
//// TODO(runcom): hacky, provide a way of passing tls cert (flag?) to be used to lookup
//for _, ic := range registryService.Config.IndexConfigs {
//ic.Secure = false
//}
endpoints, err := registryService.LookupPullEndpoints(repoInfo)
if err != nil {
return nil, err
}
var (
ctx = context.Background()
lastErr error
discardNoSupportErrors bool
imgInspect *imageInspect
confirmedV2 bool
)
for _, endpoint := range endpoints {
// make sure I can reach the registry, same as docker pull does
v1endpoint, err := endpoint.ToV1Endpoint(nil)
if err != nil {
return nil, err
}
if _, err := v1endpoint.Ping(); err != nil {
if strings.Contains(err.Error(), "timeout") {
return nil, err
}
continue
}
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
continue
}
logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
//fetcher, err := newManifestFetcher(endpoint, repoInfo, config)
fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService)
if err != nil {
lastErr = err
continue
}
if imgInspect, err = fetcher.Fetch(ctx, ref); err != nil {
// Was this fetch cancelled? If so, don't try to fall back.
fallback := false
select {
case <-ctx.Done():
default:
if fallbackErr, ok := err.(fallbackError); ok {
fallback = true
confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
err = fallbackErr.err
}
}
if fallback {
if _, ok := err.(registry.ErrNoSupport); !ok {
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
discardNoSupportErrors = true
// save the current error
lastErr = err
} else if !discardNoSupportErrors {
// Save the ErrNoSupport error, because it's either the first error or all encountered errors
// were also ErrNoSupport errors.
lastErr = err
}
continue
}
logrus.Debugf("Not continuing with error: %v", err)
return nil, err
}
return imgInspect, nil
}
if lastErr == nil {
lastErr = fmt.Errorf("no endpoints found for %s", ref.String())
}
return nil, lastErr
}
func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, registryService *registry.Service) (manifestFetcher, error) {
switch endpoint.Version {
case registry.APIVersion2:
return &v2ManifestFetcher{
endpoint: endpoint,
authConfig: authConfig,
service: registryService,
repoInfo: repoInfo,
}, nil
case registry.APIVersion1:
return &v1ManifestFetcher{
endpoint: endpoint,
authConfig: authConfig,
service: registryService,
repoInfo: repoInfo,
}, nil
}
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
}
func getAuthConfig(c *cli.Context, index *registryTypes.IndexInfo) (types.AuthConfig, error) {
var (
username = c.GlobalString("username")
password = c.GlobalString("password")
cfg = c.GlobalString("docker-cfg")
)
if _, err := os.Stat(cfg); err != nil {
logrus.Infof("Docker cli config file %q not found: %v, falling back to --username and --password if needed", cfg, err)
return types.AuthConfig{
Username: username,
Password: password,
Email: "stub@example.com",
}, nil
}
confFile, err := cliconfig.Load(cfg)
if err != nil {
return types.AuthConfig{}, err
}
authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index)
logrus.Debugf("authConfig for %s: %v", index.Name, authConfig)
return authConfig, nil
}
func validateRepoName(name string) error {
if name == "" {
return fmt.Errorf("Repository name can't be empty")
}
if name == api.NoBaseImageSpecifier {
return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier)
}
return nil
}
func makeImageInspect(img *image.Image, tag string, dgst digest.Digest, tagList []string) *imageInspect {
var digest string
if err := dgst.Validate(); err == nil {
digest = dgst.String()
}
return &imageInspect{
Tag: tag,
Digest: digest,
RepoTags: tagList,
Comment: img.Comment,
Created: img.Created.Format(time.RFC3339Nano),
ContainerConfig: &img.ContainerConfig,
DockerVersion: img.DockerVersion,
Author: img.Author,
Config: img.Config,
Architecture: img.Architecture,
Os: img.OS,
}
}
func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) {
var dver struct {
DockerVersion string `json:"docker_version"`
}
if err := json.Unmarshal(imageJSON, &dver); err != nil {
return nil, err
}
useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3")
if useFallback {
var v1Image image.V1Image
err := json.Unmarshal(imageJSON, &v1Image)
if err != nil {
return nil, err
}
imageJSON, err = json.Marshal(v1Image)
if err != nil {
return nil, err
}
}
var c map[string]*json.RawMessage
if err := json.Unmarshal(imageJSON, &c); err != nil {
return nil, err
}
c["rootfs"] = rawJSON(rootfs)
c["history"] = rawJSON(history)
return c, nil
}
func rawJSON(value interface{}) *json.RawMessage {
jsonval, err := json.Marshal(value)
if err != nil {
return nil
}
return (*json.RawMessage)(&jsonval)
}

View File

@@ -31,7 +31,7 @@ type SkopeoSuite struct {
regV1 *testRegistryV1
regV2 *testRegistryV2
regV2Shema1 *testRegistryV2
regV1WithAuth *testRegistryV1
regV1WithAuth *testRegistryV1 // does v1 support auth?
regV2WithAuth *testRegistryV2
}
@@ -47,7 +47,7 @@ func (s *SkopeoSuite) SetUpTest(c *check.C) {
_, err := exec.LookPath(skopeoBinary)
c.Assert(err, check.IsNil)
s.regV1 = setupRegistryV1At(c, privateRegistryURL0, false) // not used
s.regV1 = setupRegistryV1At(c, privateRegistryURL0, false) // TODO:(runcom)
s.regV2 = setupRegistryV2At(c, privateRegistryURL1, false, false)
s.regV2Shema1 = setupRegistryV2At(c, privateRegistryURL2, false, true)
s.regV1WithAuth = setupRegistryV1At(c, privateRegistryURL3, true) // not used
@@ -84,11 +84,7 @@ func (s *SkopeoSuite) TestVersion(c *check.C) {
func (s *SkopeoSuite) TestCanAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) {
out, err := exec.Command(skopeoBinary, "--docker-cfg=''", "--username="+s.regV2WithAuth.username, "--password="+s.regV2WithAuth.password, fmt.Sprintf("%s/busybox:latest", s.regV2WithAuth.url)).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf(string(out)))
wanted := "falling back to --username and --password if needed"
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted = "Error: image busybox not found"
wanted := "Error: image busybox not found"
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
@@ -97,11 +93,7 @@ func (s *SkopeoSuite) TestCanAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C)
func (s *SkopeoSuite) TestNeedAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) {
out, err := exec.Command(skopeoBinary, "--docker-cfg=''", fmt.Sprintf("%s/busybox:latest", s.regV2WithAuth.url)).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf(string(out)))
wanted := "falling back to --username and --password if needed"
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted = "no basic auth credentials"
wanted := "no basic auth credentials"
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}

View File

@@ -13,15 +13,21 @@ import (
)
const (
binaryV1 = "docker-registry"
binaryV2 = "registry-v2"
binaryV2Schema1 = "registry-v2-schema1"
)
type testRegistryV1 struct {
cmd *exec.Cmd
url string
dir string
}
func setupRegistryV1At(c *check.C, url string, auth bool) *testRegistryV1 {
return &testRegistryV1{}
return &testRegistryV1{
url: url,
}
}
type testRegistryV2 struct {

View File

@@ -11,7 +11,7 @@ import (
)
const (
version = "0.1.3"
version = "0.1.10-dev"
usage = "inspect images on a registry"
)

View File

@@ -1,241 +0,0 @@
%if 0%{?fedora} || 0%{?rhel} == 6
%global with_devel 0
%global with_bundled 1
%global with_debug 0
%global with_check 1
%global with_unit_test 0
%else
%global with_devel 0
%global with_bundled 1
%global with_debug 1
%global with_check 0
%global with_unit_test 0
%endif
%if 0%{?with_debug}
%global _dwz_low_mem_die_limit 0
%else
%global debug_package %{nil}
%endif
%global provider github
%global provider_tld com
%global project runcom
%global repo skopeo
# https://github.com/runcom/skopeo
%global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo}
%global import_path %{provider_prefix}
%global commit 572a6b6f537d71f7cabfdcfe185c6d7cb4367272
%global shortcommit %(c=%{commit}; echo ${c:0:7})
Name: skopeo
Version: 0.1.3
Release: 0.1.git%{shortcommit}%{?dist}
Summary: Inspect Docker images and repositories on registries
License: MIT
URL: https://%{provider_prefix}
Source0: https://%{provider_prefix}/archive/%{commit}/%{repo}-%{shortcommit}.tar.gz
# e.g. el6 has ppc64 arch without gcc-go, so EA tag is required
ExclusiveArch: %{?go_arches:%{go_arches}}%{!?go_arches:%{ix86} x86_64 %{arm}}
# If go_compiler is not set to 1, there is no virtual provide. Use golang instead.
BuildRequires: %{?go_compiler:compiler(go-compiler)}%{!?go_compiler:golang}
%description
%{summary}
%if 0%{?with_devel}
%package devel
Summary: %{summary}
BuildArch: noarch
%if 0%{?with_check} && ! 0%{?with_bundled}
BuildRequires: golang >= 1.5
BuildRequires: golang-github-cpuguy83-go-md2man
BuildRequires: golang(github.com/Azure/go-ansiterm/winterm)
BuildRequires: golang(github.com/Sirupsen/logrus)
BuildRequires: golang(github.com/docker/distribution)
BuildRequires: golang(github.com/docker/distribution/context)
BuildRequires: golang(github.com/docker/distribution/digest)
BuildRequires: golang(github.com/docker/distribution/manifest)
BuildRequires: golang(github.com/docker/distribution/manifest/manifestlist)
BuildRequires: golang(github.com/docker/distribution/manifest/schema1)
BuildRequires: golang(github.com/docker/distribution/manifest/schema2)
BuildRequires: golang(github.com/docker/distribution/reference)
BuildRequires: golang(github.com/docker/distribution/registry/api/errcode)
BuildRequires: golang(github.com/docker/distribution/registry/api/v2)
BuildRequires: golang(github.com/docker/distribution/registry/client)
BuildRequires: golang(github.com/docker/distribution/registry/client/auth)
BuildRequires: golang(github.com/docker/distribution/registry/client/transport)
BuildRequires: golang(github.com/docker/distribution/registry/storage/cache)
BuildRequires: golang(github.com/docker/distribution/registry/storage/cache/memory)
BuildRequires: golang(github.com/docker/distribution/uuid)
BuildRequires: golang(github.com/docker/docker/api)
BuildRequires: golang(github.com/docker/docker/daemon/graphdriver)
BuildRequires: golang(github.com/docker/docker/distribution/metadata)
BuildRequires: golang(github.com/docker/docker/distribution/xfer)
BuildRequires: golang(github.com/docker/docker/dockerversion)
BuildRequires: golang(github.com/docker/docker/image)
BuildRequires: golang(github.com/docker/docker/image/v1)
BuildRequires: golang(github.com/docker/docker/layer)
BuildRequires: golang(github.com/docker/docker/opts)
BuildRequires: golang(github.com/docker/docker/pkg/archive)
BuildRequires: golang(github.com/docker/docker/pkg/chrootarchive)
BuildRequires: golang(github.com/docker/docker/pkg/fileutils)
BuildRequires: golang(github.com/docker/docker/pkg/homedir)
BuildRequires: golang(github.com/docker/docker/pkg/httputils)
BuildRequires: golang(github.com/docker/docker/pkg/idtools)
BuildRequires: golang(github.com/docker/docker/pkg/ioutils)
BuildRequires: golang(github.com/docker/docker/pkg/jsonlog)
BuildRequires: golang(github.com/docker/docker/pkg/jsonmessage)
BuildRequires: golang(github.com/docker/docker/pkg/longpath)
BuildRequires: golang(github.com/docker/docker/pkg/mflag)
BuildRequires: golang(github.com/docker/docker/pkg/parsers/kernel)
BuildRequires: golang(github.com/docker/docker/pkg/plugins)
BuildRequires: golang(github.com/docker/docker/pkg/pools)
BuildRequires: golang(github.com/docker/docker/pkg/progress)
BuildRequires: golang(github.com/docker/docker/pkg/promise)
BuildRequires: golang(github.com/docker/docker/pkg/random)
BuildRequires: golang(github.com/docker/docker/pkg/reexec)
BuildRequires: golang(github.com/docker/docker/pkg/stringid)
BuildRequires: golang(github.com/docker/docker/pkg/system)
BuildRequires: golang(github.com/docker/docker/pkg/tarsum)
BuildRequires: golang(github.com/docker/docker/pkg/term)
BuildRequires: golang(github.com/docker/docker/pkg/term/windows)
BuildRequires: golang(github.com/docker/docker/pkg/useragent)
BuildRequires: golang(github.com/docker/docker/pkg/version)
BuildRequires: golang(github.com/docker/docker/reference)
BuildRequires: golang(github.com/docker/docker/registry)
BuildRequires: golang(github.com/docker/engine-api/types)
BuildRequires: golang(github.com/docker/engine-api/types/blkiodev)
BuildRequires: golang(github.com/docker/engine-api/types/container)
BuildRequires: golang(github.com/docker/engine-api/types/filters)
BuildRequires: golang(github.com/docker/engine-api/types/image)
BuildRequires: golang(github.com/docker/engine-api/types/network)
BuildRequires: golang(github.com/docker/engine-api/types/registry)
BuildRequires: golang(github.com/docker/engine-api/types/strslice)
BuildRequires: golang(github.com/docker/go-connections/nat)
BuildRequires: golang(github.com/docker/go-connections/tlsconfig)
BuildRequires: golang(github.com/docker/go-units)
BuildRequires: golang(github.com/docker/libtrust)
BuildRequires: golang(github.com/gorilla/context)
BuildRequires: golang(github.com/gorilla/mux)
BuildRequires: golang(github.com/opencontainers/runc/libcontainer/user)
BuildRequires: golang(github.com/vbatts/tar-split/archive/tar)
BuildRequires: golang(github.com/vbatts/tar-split/tar/asm)
BuildRequires: golang(github.com/vbatts/tar-split/tar/storage)
BuildRequires: golang(golang.org/x/net/context)
%endif
%if 0%{?with_bundled}
BuildRequires: golang >= 1.5
BuildRequires: golang-github-cpuguy83-go-md2man
%endif
%description devel
%{summary}
This package contains library source intended for
building other packages which use import path with
%{import_path} prefix.
%endif
%if 0%{?with_unit_test} && 0%{?with_devel}
%package unit-test-devel
Summary: Unit tests for %{name} package
%if 0%{?with_check}
#Here comes all BuildRequires: PACKAGE the unit tests
#in %%check section need for running
%endif
# test subpackage tests code from devel subpackage
Requires: %{name}-devel = %{version}-%{release}
%description unit-test-devel
%{summary}
This package contains unit tests for project
providing packages with %{import_path} prefix.
%endif
%prep
%setup -q -n %{repo}-%{commit}
%build
mkdir -p ./_build/src/github.com/runcom
ln -s $(pwd) ./_build/src/github.com/runcom/skopeo
export GOPATH=$(pwd)/_build:%{gopath}
export GO15VENDOREXPERIMENT=1
cd $(pwd)/_build/src/github.com/runcom/skopeo && %gobuild -o skopeo .
%install
mkdir -p $RPM_BUILD_ROOT/%{_mandir}/man1
make DESTDIR=%{buildroot} install
# source codes for building projects
%if 0%{?with_devel}
install -d -p %{buildroot}/%{gopath}/src/%{import_path}/
echo "%%dir %%{gopath}/src/%%{import_path}/." >> devel.file-list
# find all *.go but no *_test.go files and generate devel.file-list
for file in $(find . -iname "*.go" \! -iname "*_test.go") ; do
echo "%%dir %%{gopath}/src/%%{import_path}/$(dirname $file)" >> devel.file-list
install -d -p %{buildroot}/%{gopath}/src/%{import_path}/$(dirname $file)
cp -pav $file %{buildroot}/%{gopath}/src/%{import_path}/$file
echo "%%{gopath}/src/%%{import_path}/$file" >> devel.file-list
done
%endif
# testing files for this project
%if 0%{?with_unit_test} && 0%{?with_devel}
install -d -p %{buildroot}/%{gopath}/src/%{import_path}/
# find all *_test.go files and generate unit-test.file-list
for file in $(find . -iname "*_test.go"); do
echo "%%dir %%{gopath}/src/%%{import_path}/$(dirname $file)" >> devel.file-list
install -d -p %{buildroot}/%{gopath}/src/%{import_path}/$(dirname $file)
cp -pav $file %{buildroot}/%{gopath}/src/%{import_path}/$file
echo "%%{gopath}/src/%%{import_path}/$file" >> unit-test-devel.file-list
done
%endif
%if 0%{?with_devel}
sort -u -o devel.file-list devel.file-list
%endif
%check
%if 0%{?with_check} && 0%{?with_unit_test} && 0%{?with_devel}
%if ! 0%{?with_bundled}
export GOPATH=%{buildroot}/%{gopath}:%{gopath}
%else
export GOPATH=%{buildroot}/%{gopath}:$(pwd)/Godeps/_workspace:%{gopath}
%endif
%gotest %{import_path}/integration
%endif
#define license tag if not already defined
%{!?_licensedir:%global license %doc}
%if 0%{?with_devel}
%files devel -f devel.file-list
%license LICENSE
%doc README.md
%dir %{gopath}/src/%{provider}.%{provider_tld}/%{project}
%endif
%if 0%{?with_unit_test} && 0%{?with_devel}
%files unit-test-devel -f unit-test-devel.file-list
%license LICENSE
%doc README.md
%endif
%files
%{_bindir}/skopeo
%{_mandir}/man1/skopeo.1*
%doc README.md LICENSE
%changelog
#
# TODO(runcom): change the commit has below!!!!
#
* Thu Jan 28 2016 Antonio Murdaca <amurdaca@redhat.com> - 0.1.3-0.1.git572a6b6
- First package for Fedora

19
types/types.go Normal file
View File

@@ -0,0 +1,19 @@
package types
import (
containerTypes "github.com/docker/engine-api/types/container"
)
type ImageInspect struct {
Tag string
Digest string
RepoTags []string
Comment string
Created string
ContainerConfig *containerTypes.Config
DockerVersion string
Author string
Config *containerTypes.Config
Architecture string
Os string
}

View File

@@ -2,6 +2,7 @@ Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@users.noreply.gith
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com>
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com>
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com>
Brian Bland <brian.bland@docker.com> Brian Bland <brian.t.bland@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu>
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com>
@@ -11,4 +12,4 @@ Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com>
Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com>
Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com>
Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr>
davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com>
davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com>

View File

@@ -21,6 +21,7 @@ Ben Firshman <ben@firshman.co.uk>
bin liu <liubin0329@gmail.com>
Brian Bland <brian.bland@docker.com>
burnettk <burnettk@gmail.com>
Carson A <ca@carsonoid.net>
Chris Dillon <squarism@gmail.com>
Daisuke Fujita <dtanshi45@gmail.com>
Darren Shepherd <darren@rancher.com>
@@ -33,11 +34,13 @@ davidli <wenquan.li@hp.com>
Dejan Golja <dejan@golja.org>
Derek McGowan <derek@mcgstyle.net>
Diogo Mónica <diogo.monica@gmail.com>
DJ Enriquez <dj.enriquez@infospace.com>
Donald Huang <don.hcd@gmail.com>
Doug Davis <dug@us.ibm.com>
farmerworking <farmerworking@gmail.com>
Florentin Raud <florentin.raud@gmail.com>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
gabriell nascimento <gabriell@bluesoft.com.br>
harche <p.harshal@gmail.com>
Henri Gomez <henri.gomez@gmail.com>
Hu Keping <hukeping@huawei.com>
@@ -55,7 +58,9 @@ Josh Hawn <josh.hawn@docker.com>
Julien Fernandez <julien.fernandez@gmail.com>
Kelsey Hightower <kelsey.hightower@gmail.com>
Kenneth Lim <kennethlimcp@gmail.com>
Kenny Leung <kleung@google.com>
Li Yi <denverdino@gmail.com>
Liu Hua <sdu.liu@huawei.com>
Louis Kottmann <louis.kottmann@gmail.com>
Luke Carpenter <x@rubynerd.net>
Mary Anthony <mary@docker.com>
@@ -76,7 +81,9 @@ Olivier Jacques <olivier.jacques@hp.com>
Patrick Devine <patrick.devine@docker.com>
Philip Misiowiec <philip@atlashealth.com>
Richard Scothern <richard.scothern@docker.com>
Rodolfo Carvalho <rhcarvalho@gmail.com>
Rusty Conover <rusty@luckydinosaur.com>
Sean Boran <Boran@users.noreply.github.com>
Sebastiaan van Stijn <github@gone.nl>
Sharif Nassar <sharif@mrwacky.com>
Shawn Falkner-Horine <dreadpirateshawn@gmail.com>
@@ -93,11 +100,13 @@ Thomas Sjögren <konstruktoid@users.noreply.github.com>
Tianon Gravi <admwiggin@gmail.com>
Tibor Vass <teabee89@gmail.com>
Tonis Tiigi <tonistiigi@gmail.com>
Trevor Pounds <trevor.pounds@gmail.com>
Troels Thomsen <troels@thomsen.io>
Vincent Batts <vbatts@redhat.com>
Vincent Demeester <vincent@sbr.pm>
Vincent Giersch <vincent.giersch@ovh.net>
W. Trevor King <wking@tremily.us>
weiyuan.yl <weiyuan.yl@alibaba-inc.com>
xg.song <xg.song@venusource.com>
xiekeyang <xiekeyang@huawei.com>
Yann ROBERT <yann.robert@anantaplex.fr>

View File

@@ -32,6 +32,11 @@
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.brianbland]
Name = "Brian Bland"
Email = "brian.bland@docker.com"
GitHub = "BrianBland"
[people.dmcgowan]
Name = "Derek McGowan"
Email = "derek@mcgstyle.net"

View File

@@ -1,6 +1,6 @@
// Package context provides several utilities for working with
// golang.org/x/net/context in http requests. Primarily, the focus is on
// logging relevent request information but this package is not limited to
// logging relevant request information but this package is not limited to
// that purpose.
//
// The easiest way to get started is to get the background context:

View File

@@ -10,7 +10,7 @@ import (
// WithTrace allocates a traced timing span in a new context. This allows a
// caller to track the time between calling WithTrace and the returned done
// function. When the done function is called, a log message is emitted with a
// "trace.duration" field, corresponding to the elapased time and a
// "trace.duration" field, corresponding to the elapsed time and a
// "trace.func" field, corresponding to the function that called WithTrace.
//
// The logging keys "trace.id" and "trace.parent.id" are provided to implement

View File

@@ -22,7 +22,7 @@ var (
// may be easily referenced by easily referenced by a string
// representation of the digest as well as short representation.
// The uniqueness of the short representation is based on other
// digests in the set. If digests are ommited from this set,
// digests in the set. If digests are omitted from this set,
// collisions in a larger set may not be detected, therefore it
// is important to always do short representation lookups on
// the complete set of digests. To mitigate collisions, an

View File

@@ -9,6 +9,7 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/reference"
"github.com/docker/libtrust"
"github.com/docker/distribution/digest"
@@ -39,10 +40,8 @@ type configManifestBuilder struct {
// configJSON is configuration supplied when the ManifestBuilder was
// created.
configJSON []byte
// name is the name provided to NewConfigManifestBuilder
name string
// tag is the tag provided to NewConfigManifestBuilder
tag string
// ref contains the name and optional tag provided to NewConfigManifestBuilder.
ref reference.Named
// descriptors is the set of descriptors referencing the layers.
descriptors []distribution.Descriptor
// emptyTarDigest is set to a valid digest if an empty tar has been
@@ -54,13 +53,12 @@ type configManifestBuilder struct {
// schema version from an image configuration and a set of descriptors.
// It takes a BlobService so that it can add an empty tar to the blob store
// if the resulting manifest needs empty layers.
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, name, tag string, configJSON []byte) distribution.ManifestBuilder {
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder {
return &configManifestBuilder{
bs: bs,
pk: pk,
configJSON: configJSON,
name: name,
tag: tag,
ref: ref,
}
}
@@ -190,12 +188,17 @@ func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Mani
history[0].V1Compatibility = string(transformedConfig)
tag := ""
if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
tag = tagged.Tag()
}
mfst := Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 1,
},
Name: mb.name,
Tag: mb.tag,
Name: mb.ref.Name(),
Tag: tag,
Architecture: img.Architecture,
FSLayers: fsLayerList,
History: history,

View File

@@ -102,7 +102,7 @@ type SignedManifest struct {
Canonical []byte `json:"-"`
// all contains the byte representation of the Manifest including signatures
// and is retuend by Payload()
// and is returned by Payload()
all []byte
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/reference"
"github.com/docker/libtrust"
)
@@ -20,13 +21,18 @@ type referenceManifestBuilder struct {
// NewReferenceManifestBuilder is used to build new manifests for the current
// schema version using schema1 dependencies.
func NewReferenceManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) distribution.ManifestBuilder {
func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder {
tag := ""
if tagged, isTagged := ref.(reference.Tagged); isTagged {
tag = tagged.Tag()
}
return &referenceManifestBuilder{
Manifest: Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 1,
},
Name: name,
Name: ref.Name(),
Tag: tag,
Architecture: architecture,
},

View File

@@ -2,7 +2,7 @@ package distribution
import (
"fmt"
"strings"
"mime"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
@@ -81,22 +81,26 @@ type UnmarshalFunc func([]byte) (Manifest, Descriptor, error)
var mappings = make(map[string]UnmarshalFunc, 0)
// UnmarshalManifest looks up manifest unmarshall functions based on
// UnmarshalManifest looks up manifest unmarshal functions based on
// MediaType
func UnmarshalManifest(ctHeader string, p []byte) (Manifest, Descriptor, error) {
// Need to look up by the actual content type, not the raw contents of
// Need to look up by the actual media type, not the raw contents of
// the header. Strip semicolons and anything following them.
var mediatype string
semicolonIndex := strings.Index(ctHeader, ";")
if semicolonIndex != -1 {
mediatype = ctHeader[:semicolonIndex]
} else {
mediatype = ctHeader
if ctHeader != "" {
var err error
mediatype, _, err = mime.ParseMediaType(ctHeader)
if err != nil {
return nil, Descriptor{}, err
}
}
unmarshalFunc, ok := mappings[mediatype]
if !ok {
return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype: %s", mediatype)
unmarshalFunc, ok = mappings[""]
if !ok {
return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype and no default available: %s", mediatype)
}
}
return unmarshalFunc(p)

View File

@@ -6,7 +6,7 @@
// reference := repository [ ":" tag ] [ "@" digest ]
// name := [hostname '/'] component ['/' component]*
// hostname := hostcomponent ['.' hostcomponent]* [':' port-number]
// hostcomponent := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/
// hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/

View File

@@ -22,7 +22,7 @@ var (
// hostnameComponentRegexp restricts the registry hostname component of a
// repository name to start with a component as defined by hostnameRegexp
// and followed by an optional port.
hostnameComponentRegexp = match(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`)
hostnameComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// hostnameRegexp defines the structure of potential hostname components
// that may be part of image names. This is purposely a subset of what is
@@ -49,7 +49,7 @@ var (
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the hostname and name part omitting
// the seperating forward slash from either.
// the separating forward slash from either.
NameRegexp = expression(
optional(hostnameRegexp, literal(`/`)),
nameComponentRegexp,

View File

@@ -2,6 +2,7 @@ package distribution
import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/reference"
)
// Scope defines the set of items that match a namespace.
@@ -32,7 +33,7 @@ type Namespace interface {
// Repository should return a reference to the named repository. The
// registry may or may not have the repository but should always return a
// reference.
Repository(ctx context.Context, name string) (Repository, error)
Repository(ctx context.Context, name reference.Named) (Repository, error)
// Repositories fills 'repos' with a lexigraphically sorted catalog of repositories
// up to the size of 'repos' and returns the value 'n' for the number of entries
@@ -48,8 +49,8 @@ type ManifestServiceOption interface {
// Repository is a named collection of manifests and layers.
type Repository interface {
// Name returns the name of the repository.
Name() string
// Named returns the name of the repository.
Named() reference.Named
// Manifests returns a reference to this repository's manifest service.
// with the supplied options applied.

View File

@@ -271,7 +271,7 @@ type MethodDescriptor struct {
// RequestDescriptor per API use case.
type RequestDescriptor struct {
// Name provides a short identifier for the request, usable as a title or
// to provide quick context for the particalar request.
// to provide quick context for the particular request.
Name string
// Description should cover the requests purpose, covering any details for
@@ -303,14 +303,14 @@ type RequestDescriptor struct {
// ResponseDescriptor describes the components of an API response.
type ResponseDescriptor struct {
// Name provides a short identifier for the response, usable as a title or
// to provide quick context for the particalar response.
// to provide quick context for the particular response.
Name string
// Description should provide a brief overview of the role of the
// response.
Description string
// StatusCode specifies the status recieved by this particular response.
// StatusCode specifies the status received by this particular response.
StatusCode int
// Headers covers any headers that may be returned from the response.

View File

@@ -84,7 +84,7 @@ var (
})
// ErrorCodeManifestUnverified is returned when the manifest fails
// signature verfication.
// signature verification.
ErrorCodeManifestUnverified = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "MANIFEST_UNVERIFIED",
Message: "manifest failed signature verification",

View File

@@ -5,7 +5,7 @@ import (
"net/url"
"strings"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/gorilla/mux"
)
@@ -113,10 +113,10 @@ func (ub *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) {
}
// BuildTagsURL constructs a url to list the tags in the named repository.
func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
func (ub *URLBuilder) BuildTagsURL(name reference.Named) (string, error) {
route := ub.cloneRoute(RouteNameTags)
tagsURL, err := route.URL("name", name)
tagsURL, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
@@ -126,10 +126,18 @@ func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
// BuildManifestURL constructs a url for the manifest identified by name and
// reference. The argument reference may be either a tag or digest.
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) {
route := ub.cloneRoute(RouteNameManifest)
manifestURL, err := route.URL("name", name, "reference", reference)
tagOrDigest := ""
switch v := ref.(type) {
case reference.Tagged:
tagOrDigest = v.Tag()
case reference.Digested:
tagOrDigest = v.Digest().String()
}
manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest)
if err != nil {
return "", err
}
@@ -138,10 +146,10 @@ func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
}
// BuildBlobURL constructs the url for the blob identified by name and dgst.
func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
func (ub *URLBuilder) BuildBlobURL(ref reference.Canonical) (string, error) {
route := ub.cloneRoute(RouteNameBlob)
layerURL, err := route.URL("name", name, "digest", dgst.String())
layerURL, err := route.URL("name", ref.Name(), "digest", ref.Digest().String())
if err != nil {
return "", err
}
@@ -151,10 +159,10 @@ func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, err
// BuildBlobUploadURL constructs a url to begin a blob upload in the
// repository identified by name.
func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
func (ub *URLBuilder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneRoute(RouteNameBlobUpload)
uploadURL, err := route.URL("name", name)
uploadURL, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
@@ -166,10 +174,10 @@ func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (str
// including any url values. This should generally not be used by clients, as
// this url is provided by server implementations during the blob upload
// process.
func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) {
route := ub.cloneRoute(RouteNameBlobUploadChunk)
uploadURL, err := route.URL("name", name, "uuid", uuid)
uploadURL, err := route.URL("name", name.Name(), "uuid", uuid)
if err != nil {
return "", err
}

View File

@@ -15,6 +15,10 @@ import (
"github.com/docker/distribution/registry/client/transport"
)
// ErrNoBasicAuthCredentials is returned if a request can't be authorized with
// basic auth due to lack of credentials.
var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
// AuthenticationHandler is an interface for authorizing a request from
// params from a "WWW-Authenicate" header for a single scheme.
type AuthenticationHandler interface {
@@ -285,9 +289,9 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
}
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
// The default/minimum lifetime.
tr.ExpiresIn = minimumTokenLifetimeSeconds
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
}
if tr.IssuedAt.IsZero() {
@@ -322,5 +326,5 @@ func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]st
return nil
}
}
return errors.New("no basic auth credentials")
return ErrNoBasicAuthCredentials
}

View File

@@ -27,6 +27,39 @@ type Registry interface {
Repositories(ctx context.Context, repos []string, last string) (n int, err error)
}
// checkHTTPRedirect is a callback that can manipulate redirected HTTP
// requests. It is used to preserve Accept and Range headers.
func checkHTTPRedirect(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
if len(via) > 0 {
for headerName, headerVals := range via[0].Header {
if headerName != "Accept" && headerName != "Range" {
continue
}
for _, val := range headerVals {
// Don't add to redirected request if redirected
// request already has a header with the same
// name and value.
hasValue := false
for _, existingVal := range req.Header[headerName] {
if existingVal == val {
hasValue = true
break
}
}
if !hasValue {
req.Header.Add(headerName, val)
}
}
}
}
return nil
}
// NewRegistry creates a registry namespace which can be used to get a listing of repositories
func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) {
ub, err := v2.NewURLBuilderFromString(baseURL)
@@ -35,8 +68,9 @@ func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTrippe
}
client := &http.Client{
Transport: transport,
Timeout: 1 * time.Minute,
Transport: transport,
Timeout: 1 * time.Minute,
CheckRedirect: checkHTTPRedirect,
}
return &registry{
@@ -98,18 +132,15 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri
}
// NewRepository creates a new Repository for the given repository name and base URL.
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
if _, err := reference.ParseNamed(name); err != nil {
return nil, err
}
func NewRepository(ctx context.Context, name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
ub, err := v2.NewURLBuilderFromString(baseURL)
if err != nil {
return nil, err
}
client := &http.Client{
Transport: transport,
Transport: transport,
CheckRedirect: checkHTTPRedirect,
// TODO(dmcgowan): create cookie jar
}
@@ -125,21 +156,21 @@ type repository struct {
client *http.Client
ub *v2.URLBuilder
context context.Context
name string
name reference.Named
}
func (r *repository) Name() string {
func (r *repository) Named() reference.Named {
return r.name
}
func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
statter := &blobStatter{
name: r.Name(),
name: r.name,
ub: r.ub,
client: r.client,
}
return &blobs{
name: r.Name(),
name: r.name,
ub: r.ub,
client: r.client,
statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter),
@@ -149,7 +180,7 @@ func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
// todo(richardscothern): options should be sent over the wire
return &manifests{
name: r.Name(),
name: r.name,
ub: r.ub,
client: r.client,
etags: make(map[string]string),
@@ -161,7 +192,7 @@ func (r *repository) Tags(ctx context.Context) distribution.TagService {
client: r.client,
ub: r.ub,
context: r.context,
name: r.Name(),
name: r.Named(),
}
}
@@ -170,7 +201,7 @@ type tags struct {
client *http.Client
ub *v2.URLBuilder
context context.Context
name string
name reference.Named
}
// All returns all tags
@@ -253,7 +284,11 @@ func descriptorFromResponse(response *http.Response) (distribution.Descriptor, e
// to construct a descriptor for the tag. If the registry doesn't support HEADing
// a manifest, fallback to GET.
func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
u, err := t.ub.BuildManifestURL(t.name, tag)
ref, err := reference.WithTag(t.name, tag)
if err != nil {
return distribution.Descriptor{}, err
}
u, err := t.ub.BuildManifestURL(ref)
if err != nil {
return distribution.Descriptor{}, err
}
@@ -293,14 +328,18 @@ func (t *tags) Untag(ctx context.Context, tag string) error {
}
type manifests struct {
name string
name reference.Named
ub *v2.URLBuilder
client *http.Client
etags map[string]string
}
func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
u, err := ms.ub.BuildManifestURL(ms.name, dgst.String())
ref, err := reference.WithDigest(ms.name, dgst)
if err != nil {
return false, err
}
u, err := ms.ub.BuildManifestURL(ref)
if err != nil {
return false, err
}
@@ -337,11 +376,19 @@ func (o etagOption) Apply(ms distribution.ManifestService) error {
}
func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
var (
digestOrTag string
ref reference.Named
err error
)
var tag string
for _, option := range options {
if opt, ok := option.(withTagOption); ok {
tag = opt.tag
digestOrTag = opt.tag
ref, err = reference.WithTag(ms.name, opt.tag)
if err != nil {
return nil, err
}
} else {
err := option.Apply(ms)
if err != nil {
@@ -350,14 +397,15 @@ func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...dis
}
}
var ref string
if tag != "" {
ref = tag
} else {
ref = dgst.String()
if digestOrTag == "" {
digestOrTag = dgst.String()
ref, err = reference.WithDigest(ms.name, dgst)
if err != nil {
return nil, err
}
}
u, err := ms.ub.BuildManifestURL(ms.name, ref)
u, err := ms.ub.BuildManifestURL(ref)
if err != nil {
return nil, err
}
@@ -371,8 +419,8 @@ func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...dis
req.Header.Add("Accept", t)
}
if _, ok := ms.etags[ref]; ok {
req.Header.Set("If-None-Match", ms.etags[ref])
if _, ok := ms.etags[digestOrTag]; ok {
req.Header.Set("If-None-Match", ms.etags[digestOrTag])
}
resp, err := ms.client.Do(req)
@@ -414,13 +462,19 @@ func (o withTagOption) Apply(m distribution.ManifestService) error {
}
// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the
// tag name in order to build the correct upload URL. This state is written and read under a lock.
// tag name in order to build the correct upload URL.
func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
var tag string
ref := ms.name
var tagged bool
for _, option := range options {
if opt, ok := option.(withTagOption); ok {
tag = opt.tag
var err error
ref, err = reference.WithTag(ref, opt.tag)
if err != nil {
return "", err
}
tagged = true
} else {
err := option.Apply(ms)
if err != nil {
@@ -428,13 +482,24 @@ func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options .
}
}
}
manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag)
mediaType, p, err := m.Payload()
if err != nil {
return "", err
}
mediaType, p, err := m.Payload()
if !tagged {
// generate a canonical digest and Put by digest
_, d, err := distribution.UnmarshalManifest(mediaType, p)
if err != nil {
return "", err
}
ref, err = reference.WithDigest(ref, d.Digest)
if err != nil {
return "", err
}
}
manifestURL, err := ms.ub.BuildManifestURL(ref)
if err != nil {
return "", err
}
@@ -466,7 +531,11 @@ func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options .
}
func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
u, err := ms.ub.BuildManifestURL(ms.name, dgst.String())
ref, err := reference.WithDigest(ms.name, dgst)
if err != nil {
return err
}
u, err := ms.ub.BuildManifestURL(ref)
if err != nil {
return err
}
@@ -493,7 +562,7 @@ func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
}*/
type blobs struct {
name string
name reference.Named
ub *v2.URLBuilder
client *http.Client
@@ -531,7 +600,11 @@ func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
}
func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst)
ref, err := reference.WithDigest(bs.name, dgst)
if err != nil {
return nil, err
}
blobURL, err := bs.ub.BuildBlobURL(ref)
if err != nil {
return nil, err
}
@@ -666,13 +739,17 @@ func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error {
}
type blobStatter struct {
name string
name reference.Named
ub *v2.URLBuilder
client *http.Client
}
func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
u, err := bs.ub.BuildBlobURL(bs.name, dgst)
ref, err := reference.WithDigest(bs.name, dgst)
if err != nil {
return distribution.Descriptor{}, err
}
u, err := bs.ub.BuildBlobURL(ref)
if err != nil {
return distribution.Descriptor{}, err
}
@@ -720,7 +797,11 @@ func buildCatalogValues(maxEntries int, last string) url.Values {
}
func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst)
ref, err := reference.WithDigest(bs.name, dgst)
if err != nil {
return err
}
blobURL, err := bs.ub.BuildBlobURL(ref)
if err != nil {
return err
}

View File

@@ -1,12 +1,22 @@
package transport
import (
"bufio"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
)
var (
contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
// ErrWrongCodeForByteRange is returned if the client sends a request
// with a Range header but the server returns a 2xx or 3xx code other
// than 206 Partial Content.
ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request")
)
// ReadSeekCloser combines io.ReadSeeker with io.Closer.
@@ -40,8 +50,6 @@ type httpReadSeeker struct {
// rc is the remote read closer.
rc io.ReadCloser
// brd is a buffer for internal buffered io.
brd *bufio.Reader
// readerOffset tracks the offset as of the last read.
readerOffset int64
// seekOffset allows Seek to override the offset. Seek changes
@@ -79,11 +87,6 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
hrs.seekOffset += int64(n)
hrs.readerOffset += int64(n)
// Simulate io.EOF error if we reach filesize.
if err == nil && hrs.size >= 0 && hrs.readerOffset >= hrs.size {
err = io.EOF
}
return n, err
}
@@ -92,8 +95,18 @@ func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
return 0, hrs.err
}
lastReaderOffset := hrs.readerOffset
if whence == os.SEEK_SET && hrs.rc == nil {
// If no request has been made yet, and we are seeking to an
// absolute position, set the read offset as well to avoid an
// unnecessary request.
hrs.readerOffset = offset
}
_, err := hrs.reader()
if err != nil {
hrs.readerOffset = lastReaderOffset
return 0, err
}
@@ -101,14 +114,14 @@ func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
switch whence {
case os.SEEK_CUR:
newOffset += int64(offset)
newOffset += offset
case os.SEEK_END:
if hrs.size < 0 {
return 0, errors.New("content length not known")
}
newOffset = hrs.size + int64(offset)
newOffset = hrs.size + offset
case os.SEEK_SET:
newOffset = int64(offset)
newOffset = offset
}
if newOffset < 0 {
@@ -131,7 +144,6 @@ func (hrs *httpReadSeeker) Close() error {
}
hrs.rc = nil
hrs.brd = nil
hrs.err = errors.New("httpLayer: closed")
@@ -154,7 +166,7 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
}
if hrs.rc != nil {
return hrs.brd, nil
return hrs.rc, nil
}
req, err := http.NewRequest("GET", hrs.url, nil)
@@ -163,10 +175,8 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
}
if hrs.readerOffset > 0 {
// TODO(stevvooe): Get this working correctly.
// If we are at different offset, issue a range request from there.
req.Header.Add("Range", "1-")
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", hrs.readerOffset))
// TODO: get context in here
// context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range"))
}
@@ -179,12 +189,55 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
// Normally would use client.SuccessStatus, but that would be a cyclic
// import
if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
hrs.rc = resp.Body
if resp.StatusCode == http.StatusOK {
if hrs.readerOffset > 0 {
if resp.StatusCode != http.StatusPartialContent {
return nil, ErrWrongCodeForByteRange
}
contentRange := resp.Header.Get("Content-Range")
if contentRange == "" {
return nil, errors.New("no Content-Range header found in HTTP 206 response")
}
submatches := contentRangeRegexp.FindStringSubmatch(contentRange)
if len(submatches) < 4 {
return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange)
}
startByte, err := strconv.ParseUint(submatches[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange)
}
if startByte != uint64(hrs.readerOffset) {
return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset)
}
endByte, err := strconv.ParseUint(submatches[2], 10, 64)
if err != nil {
return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange)
}
if submatches[3] == "*" {
hrs.size = -1
} else {
size, err := strconv.ParseUint(submatches[3], 10, 64)
if err != nil {
return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange)
}
if endByte+1 != size {
return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange)
}
hrs.size = int64(size)
}
} else if resp.StatusCode == http.StatusOK {
hrs.size = resp.ContentLength
} else {
hrs.size = -1
}
hrs.rc = resp.Body
} else {
defer resp.Body.Close()
if hrs.errorHandler != nil {
@@ -193,11 +246,5 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status)
}
if hrs.brd == nil {
hrs.brd = bufio.NewReader(hrs.rc)
} else {
hrs.brd.Reset(hrs.rc)
}
return hrs.brd, nil
return hrs.rc, nil
}

View File

@@ -23,9 +23,6 @@ const (
// MinVersion represents Minimum REST API version supported
MinVersion version.Version = "1.12"
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
DefaultDockerfileName string = "Dockerfile"
// NoBaseImageSpecifier is the symbol used by the FROM
// command to specify that no base image is to be used.
NoBaseImageSpecifier string = "scratch"

View File

@@ -17,6 +17,7 @@ import (
const (
// ConfigFileName is the name of config file
ConfigFileName = "config.json"
configFileDir = ".docker"
oldConfigfile = ".dockercfg"
// This constant is only used for really old config files when the
@@ -31,7 +32,7 @@ var (
func init() {
if configDir == "" {
configDir = filepath.Join(homedir.Get(), ".docker")
configDir = filepath.Join(homedir.Get(), configFileDir)
}
}
@@ -47,12 +48,13 @@ func SetConfigDir(dir string) {
// ConfigFile ~/.docker/config.json file info
type ConfigFile struct {
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
filename string // Note: not serialized - for internal use only
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
filename string // Note: not serialized - for internal use only
}
// NewConfigFile initializes an empty configuration file for the given filename 'fn'
@@ -86,11 +88,6 @@ func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error {
if err != nil {
return err
}
origEmail := strings.Split(arr[1], " = ")
if len(origEmail) != 2 {
return fmt.Errorf("Invalid Auth config file")
}
authConfig.Email = origEmail[1]
authConfig.ServerAddress = defaultIndexserver
configFile.AuthConfigs[defaultIndexserver] = authConfig
} else {
@@ -126,6 +123,13 @@ func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
return nil
}
// ContainsAuth returns whether there is authentication configured
// in this file or not.
func (configFile *ConfigFile) ContainsAuth() bool {
return configFile.CredentialsStore != "" ||
(configFile.AuthConfigs != nil && len(configFile.AuthConfigs) > 0)
}
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from
// a non-nested reader
func LegacyLoadFromReader(configData io.Reader) (*ConfigFile, error) {
@@ -249,6 +253,10 @@ func (configFile *ConfigFile) Filename() string {
// encodeAuth creates a base64 encoded string to containing authorization information
func encodeAuth(authConfig *types.AuthConfig) string {
if authConfig.Username == "" && authConfig.Password == "" {
return ""
}
authStr := authConfig.Username + ":" + authConfig.Password
msg := []byte(authStr)
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
@@ -258,6 +266,10 @@ func encodeAuth(authConfig *types.AuthConfig) string {
// decodeAuth decodes a base64 encoded string and returns username and password
func decodeAuth(authStr string) (string, string, error) {
if authStr == "" {
return "", "", nil
}
decLen := base64.StdEncoding.DecodedLen(len(authStr))
decoded := make([]byte, decLen)
authByte := []byte(authStr)

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/Sirupsen/logrus"
"github.com/vbatts/tar-split/tar/storage"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
@@ -92,6 +93,23 @@ type Driver interface {
DiffSize(id, parent string) (size int64, err error)
}
// DiffGetterDriver is the interface for layered file system drivers that
// provide a specialized function for getting file contents for tar-split.
type DiffGetterDriver interface {
Driver
// DiffGetter returns an interface to efficiently retrieve the contents
// of files in a layer.
DiffGetter(id string) (FileGetCloser, error)
}
// FileGetCloser extends the storage.FileGetter interface with a Close method
// for cleaning up.
type FileGetCloser interface {
storage.FileGetter
// Close cleans up any resources associated with the FileGetCloser.
Close() error
}
func init() {
drivers = make(map[string]InitFunc)
}

106
vendor/github.com/docker/docker/distribution/errors.go generated vendored Normal file
View File

@@ -0,0 +1,106 @@
package distribution
import (
"net/url"
"strings"
"syscall"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client"
"github.com/docker/docker/distribution/xfer"
)
// ErrNoSupport is an error type used for errors indicating that an operation
// is not supported. It encapsulates a more specific error.
type ErrNoSupport struct{ Err error }
func (e ErrNoSupport) Error() string {
if e.Err == nil {
return "not supported"
}
return e.Err.Error()
}
// fallbackError wraps an error that can possibly allow fallback to a different
// endpoint.
type fallbackError struct {
// err is the error being wrapped.
err error
// confirmedV2 is set to true if it was confirmed that the registry
// supports the v2 protocol. This is used to limit fallbacks to the v1
// protocol.
confirmedV2 bool
// transportOK is set to true if we managed to speak HTTP with the
// registry. This confirms that we're using appropriate TLS settings
// (or lack of TLS).
transportOK bool
}
// Error renders the FallbackError as a string.
func (f fallbackError) Error() string {
return f.err.Error()
}
// shouldV2Fallback returns true if this error is a reason to fall back to v1.
func shouldV2Fallback(err errcode.Error) bool {
switch err.Code {
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
return true
}
return false
}
// continueOnError returns true if we should fallback to the next endpoint
// as a result of this error.
func continueOnError(err error) bool {
switch v := err.(type) {
case errcode.Errors:
if len(v) == 0 {
return true
}
return continueOnError(v[0])
case ErrNoSupport:
return continueOnError(v.Err)
case errcode.Error:
return shouldV2Fallback(v)
case *client.UnexpectedHTTPResponseError:
return true
case ImageConfigPullError:
return false
case error:
return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error()))
}
// let's be nice and fallback if the error is a completely
// unexpected one.
// If new errors have to be handled in some way, please
// add them to the switch above.
return true
}
// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the
// operation after this error.
func retryOnError(err error) error {
switch v := err.(type) {
case errcode.Errors:
return retryOnError(v[0])
case errcode.Error:
switch v.Code {
case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied:
return xfer.DoNotRetry{Err: err}
}
case *url.Error:
return retryOnError(v.Err)
case *client.UnexpectedHTTPResponseError:
return xfer.DoNotRetry{Err: err}
case error:
if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) {
return xfer.DoNotRetry{Err: err}
}
}
// let's be nice and fallback if the error is a completely
// unexpected one.
// If new errors have to be handled in some way, please
// add them to the switch above.
return err
}

View File

@@ -2,7 +2,6 @@ package distribution
import (
"fmt"
"os"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api"
@@ -19,7 +18,6 @@ import (
// ImagePullConfig stores pull configuration.
type ImagePullConfig struct {
// MetaHeaders stores HTTP headers with metadata about the image
// (DockerHeaders with prefix X-Meta- in the request).
MetaHeaders map[string][]string
// AuthConfig holds authentication credentials for authenticating with
// the registry.
@@ -90,7 +88,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
return err
}
endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo)
endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
if err != nil {
return err
}
@@ -111,12 +109,25 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
// confirm that it was talking to a v2 registry. This will
// prevent fallback to the v1 protocol.
confirmedV2 bool
// confirmedTLSRegistries is a map indicating which registries
// are known to be using TLS. There should never be a plaintext
// retry for any of these.
confirmedTLSRegistries = make(map[string]struct{})
)
for _, endpoint := range endpoints {
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
continue
}
if endpoint.URL.Scheme != "https" {
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
continue
}
}
logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
puller, err := newPuller(endpoint, repoInfo, imagePullConfig)
@@ -134,11 +145,14 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
if fallbackErr, ok := err.(fallbackError); ok {
fallback = true
confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
if fallbackErr.transportOK && endpoint.URL.Scheme == "https" {
confirmedTLSRegistries[endpoint.URL.Host] = struct{}{}
}
err = fallbackErr.err
}
}
if fallback {
if _, ok := err.(registry.ErrNoSupport); !ok {
if _, ok := err.(ErrNoSupport); !ok {
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
discardNoSupportErrors = true
// append subsequent errors
@@ -149,9 +163,10 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
// append subsequent errors
lastErr = err
}
logrus.Errorf("Attempting next endpoint for pull after error: %v", err)
continue
}
logrus.Debugf("Not continuing with error: %v", err)
logrus.Errorf("Not continuing with pull after error: %v", err)
return err
}
@@ -188,16 +203,3 @@ func validateRepoName(name string) error {
}
return nil
}
// tmpFileClose creates a closer function for a temporary file that closes the file
// and also deletes it.
func tmpFileCloser(tmpFile *os.File) func() error {
return func() error {
tmpFile.Close()
if err := os.RemoveAll(tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name())
}
return nil
}
}

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil"
"net"
"net/url"
"os"
"strings"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/distribution/metadata"
"github.com/docker/docker/distribution/xfer"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/image"
"github.com/docker/docker/image/v1"
"github.com/docker/docker/layer"
@@ -36,7 +38,7 @@ type v1Puller struct {
func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
if _, isCanonical := ref.(reference.Canonical); isCanonical {
// Allowing fallback, because HTTPS v1 is before HTTP v2
return fallbackError{err: registry.ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}}
return fallbackError{err: ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}}
}
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name)
@@ -47,10 +49,10 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
tr := transport.NewTransport(
// TODO(tiborvass): was ReceiveTimeout
registry.NewTransport(tlsConfig),
registry.DockerHeaders(p.config.MetaHeaders)...,
registry.DockerHeaders(dockerversion.DockerUserAgent(), p.config.MetaHeaders)...,
)
client := registry.HTTPClient(tr)
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), p.config.MetaHeaders)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return fallbackError{err: err}
@@ -278,6 +280,7 @@ type v1LayerDescriptor struct {
layersDownloaded *bool
layerSize int64
session *registry.Session
tmpFile *os.File
}
func (ld *v1LayerDescriptor) Key() string {
@@ -307,7 +310,7 @@ func (ld *v1LayerDescriptor) Download(ctx context.Context, progressOutput progre
}
*ld.layersDownloaded = true
tmpFile, err := ioutil.TempFile("", "GetImageBlob")
ld.tmpFile, err = ioutil.TempFile("", "GetImageBlob")
if err != nil {
layerReader.Close()
return nil, 0, err
@@ -316,17 +319,28 @@ func (ld *v1LayerDescriptor) Download(ctx context.Context, progressOutput progre
reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, layerReader), progressOutput, ld.layerSize, ld.ID(), "Downloading")
defer reader.Close()
_, err = io.Copy(tmpFile, reader)
_, err = io.Copy(ld.tmpFile, reader)
if err != nil {
ld.Close()
return nil, 0, err
}
progress.Update(progressOutput, ld.ID(), "Download complete")
logrus.Debugf("Downloaded %s to tempfile %s", ld.ID(), tmpFile.Name())
logrus.Debugf("Downloaded %s to tempfile %s", ld.ID(), ld.tmpFile.Name())
tmpFile.Seek(0, 0)
return ioutils.NewReadCloserWrapper(tmpFile, tmpFileCloser(tmpFile)), ld.layerSize, nil
ld.tmpFile.Seek(0, 0)
return ld.tmpFile, ld.layerSize, nil
}
func (ld *v1LayerDescriptor) Close() {
if ld.tmpFile != nil {
ld.tmpFile.Close()
if err := os.RemoveAll(ld.tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name())
}
ld.tmpFile = nil
}
}
func (ld *v1LayerDescriptor) Registered(diffID layer.DiffID) {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"runtime"
@@ -17,6 +18,8 @@ import (
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/distribution/metadata"
"github.com/docker/docker/distribution/xfer"
"github.com/docker/docker/image"
@@ -32,6 +35,17 @@ import (
var errRootFSMismatch = errors.New("layers from manifest don't match image configuration")
// ImageConfigPullError is an error pulling the image config blob
// (only applies to schema2).
type ImageConfigPullError struct {
Err error
}
// Error returns the error string for ImageConfigPullError.
func (e ImageConfigPullError) Error() string {
return "error pulling image configuration: " + e.Err.Error()
}
type v2Puller struct {
V2MetadataService *metadata.V2MetadataService
endpoint registry.APIEndpoint
@@ -48,16 +62,20 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
if err != nil {
logrus.Warnf("Error getting v2 registry: %v", err)
return fallbackError{err: err, confirmedV2: p.confirmedV2}
return err
}
if err = p.pullV2Repository(ctx, ref); err != nil {
if _, ok := err.(fallbackError); ok {
return err
}
if registry.ContinueOnError(err) {
logrus.Debugf("Error trying v2 registry: %v", err)
return fallbackError{err: err, confirmedV2: p.confirmedV2}
if continueOnError(err) {
logrus.Errorf("Error trying v2 registry: %v", err)
return fallbackError{
err: err,
confirmedV2: p.confirmedV2,
transportOK: true,
}
}
}
return err
@@ -114,6 +132,8 @@ type v2LayerDescriptor struct {
repoInfo *registry.RepositoryInfo
repo distribution.Repository
V2MetadataService *metadata.V2MetadataService
tmpFile *os.File
verifier digest.Verifier
}
func (ld *v2LayerDescriptor) Key() string {
@@ -131,17 +151,56 @@ func (ld *v2LayerDescriptor) DiffID() (layer.DiffID, error) {
func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) {
logrus.Debugf("pulling blob %q", ld.digest)
var (
err error
offset int64
)
if ld.tmpFile == nil {
ld.tmpFile, err = createDownloadFile()
if err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
} else {
offset, err = ld.tmpFile.Seek(0, os.SEEK_END)
if err != nil {
logrus.Debugf("error seeking to end of download file: %v", err)
offset = 0
ld.tmpFile.Close()
if err := os.Remove(ld.tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name())
}
ld.tmpFile, err = createDownloadFile()
if err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
} else if offset != 0 {
logrus.Debugf("attempting to resume download of %q from %d bytes", ld.digest, offset)
}
}
tmpFile := ld.tmpFile
blobs := ld.repo.Blobs(ctx)
layerDownload, err := blobs.Open(ctx, ld.digest)
if err != nil {
logrus.Debugf("Error statting layer: %v", err)
logrus.Errorf("Error initiating layer download: %v", err)
if err == distribution.ErrBlobUnknown {
return nil, 0, xfer.DoNotRetry{Err: err}
}
return nil, 0, retryOnError(err)
}
if offset != 0 {
_, err := layerDownload.Seek(offset, os.SEEK_SET)
if err != nil {
if err := ld.truncateDownloadFile(); err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
return nil, 0, err
}
}
size, err := layerDownload.Seek(0, os.SEEK_END)
if err != nil {
// Seek failed, perhaps because there was no Content-Length
@@ -149,41 +208,59 @@ func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progre
// still continue without a progress bar.
size = 0
} else {
// Restore the seek offset at the beginning of the stream.
_, err = layerDownload.Seek(0, os.SEEK_SET)
if size != 0 && offset > size {
logrus.Debugf("Partial download is larger than full blob. Starting over")
offset = 0
if err := ld.truncateDownloadFile(); err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
}
// Restore the seek offset either at the beginning of the
// stream, or just after the last byte we have from previous
// attempts.
_, err = layerDownload.Seek(offset, os.SEEK_SET)
if err != nil {
return nil, 0, err
}
}
reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, layerDownload), progressOutput, size, ld.ID(), "Downloading")
reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, layerDownload), progressOutput, size-offset, ld.ID(), "Downloading")
defer reader.Close()
verifier, err := digest.NewDigestVerifier(ld.digest)
if err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
if ld.verifier == nil {
ld.verifier, err = digest.NewDigestVerifier(ld.digest)
if err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
}
tmpFile, err := ioutil.TempFile("", "GetImageBlob")
if err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
_, err = io.Copy(tmpFile, io.TeeReader(reader, verifier))
_, err = io.Copy(tmpFile, io.TeeReader(reader, ld.verifier))
if err != nil {
if err == transport.ErrWrongCodeForByteRange {
if err := ld.truncateDownloadFile(); err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
return nil, 0, err
}
return nil, 0, retryOnError(err)
}
progress.Update(progressOutput, ld.ID(), "Verifying Checksum")
if !verifier.Verified() {
if !ld.verifier.Verified() {
err = fmt.Errorf("filesystem layer verification failed for digest %s", ld.digest)
logrus.Error(err)
tmpFile.Close()
if err := os.RemoveAll(tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name())
}
// Allow a retry if this digest verification error happened
// after a resumed download.
if offset != 0 {
if err := ld.truncateDownloadFile(); err != nil {
return nil, 0, xfer.DoNotRetry{Err: err}
}
return nil, 0, err
}
return nil, 0, xfer.DoNotRetry{Err: err}
}
@@ -191,8 +268,43 @@ func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progre
logrus.Debugf("Downloaded %s to tempfile %s", ld.ID(), tmpFile.Name())
tmpFile.Seek(0, 0)
return ioutils.NewReadCloserWrapper(tmpFile, tmpFileCloser(tmpFile)), size, nil
_, err = tmpFile.Seek(0, os.SEEK_SET)
if err != nil {
tmpFile.Close()
if err := os.Remove(tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", tmpFile.Name())
}
ld.tmpFile = nil
ld.verifier = nil
return nil, 0, xfer.DoNotRetry{Err: err}
}
return tmpFile, size, nil
}
func (ld *v2LayerDescriptor) Close() {
if ld.tmpFile != nil {
ld.tmpFile.Close()
if err := os.RemoveAll(ld.tmpFile.Name()); err != nil {
logrus.Errorf("Failed to remove temp file: %s", ld.tmpFile.Name())
}
}
}
func (ld *v2LayerDescriptor) truncateDownloadFile() error {
// Need a new hash context since we will be redoing the download
ld.verifier = nil
if _, err := ld.tmpFile.Seek(0, os.SEEK_SET); err != nil {
logrus.Errorf("error seeking to beginning of download file: %v", err)
return err
}
if err := ld.tmpFile.Truncate(0); err != nil {
logrus.Errorf("error truncating download file: %v", err)
return err
}
return nil
}
func (ld *v2LayerDescriptor) Registered(diffID layer.DiffID) {
@@ -238,7 +350,7 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
p.confirmedV2 = true
logrus.Debugf("Pulling ref from V2 registry: %s", ref.String())
progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+p.repo.Name())
progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+p.repo.Named().Name())
var (
imageID image.ID
@@ -387,7 +499,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
go func() {
configJSON, err := p.pullSchema2ImageConfig(ctx, target.Digest)
if err != nil {
errChan <- err
errChan <- ImageConfigPullError{Err: err}
cancel()
return
}
@@ -607,12 +719,24 @@ func allowV1Fallback(err error) error {
switch v := err.(type) {
case errcode.Errors:
if len(v) != 0 {
if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
return fallbackError{err: err, confirmedV2: false}
if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) {
return fallbackError{
err: err,
confirmedV2: false,
transportOK: true,
}
}
}
case errcode.Error:
if registry.ShouldV2Fallback(v) {
if shouldV2Fallback(v) {
return fallbackError{
err: err,
confirmedV2: false,
transportOK: true,
}
}
case *url.Error:
if v.Err == auth.ErrNoBasicAuthCredentials {
return fallbackError{err: err, confirmedV2: false}
}
}
@@ -699,3 +823,7 @@ func fixManifestLayers(m *schema1.Manifest) error {
return nil
}
func createDownloadFile() (*os.File, error) {
return ioutil.TempFile("", "GetImageBlob")
}

View File

@@ -22,7 +22,6 @@ import (
// ImagePushConfig stores push configuration.
type ImagePushConfig struct {
// MetaHeaders store HTTP headers with metadata about the image
// (DockerHeaders with prefix X-Meta- in the request).
MetaHeaders map[string][]string
// AuthConfig holds authentication credentials for authenticating with
// the registry.
@@ -101,7 +100,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
return err
}
endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo)
endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo.Hostname())
if err != nil {
return err
}
@@ -120,6 +119,11 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
// confirm that it was talking to a v2 registry. This will
// prevent fallback to the v1 protocol.
confirmedV2 bool
// confirmedTLSRegistries is a map indicating which registries
// are known to be using TLS. There should never be a plaintext
// retry for any of these.
confirmedTLSRegistries = make(map[string]struct{})
)
for _, endpoint := range endpoints {
@@ -128,6 +132,13 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
continue
}
if endpoint.URL.Scheme != "https" {
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
continue
}
}
logrus.Debugf("Trying to push %s to %s %s", repoInfo.FullName(), endpoint.URL, endpoint.Version)
pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig)
@@ -143,13 +154,17 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
default:
if fallbackErr, ok := err.(fallbackError); ok {
confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
if fallbackErr.transportOK && endpoint.URL.Scheme == "https" {
confirmedTLSRegistries[endpoint.URL.Host] = struct{}{}
}
err = fallbackErr.err
lastErr = err
logrus.Errorf("Attempting next endpoint for push after error: %v", err)
continue
}
}
logrus.Debugf("Not continuing with error: %v", err)
logrus.Errorf("Not continuing with push after error: %v", err)
return err
}
@@ -171,7 +186,14 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
// argument so that it can be used with httpBlobWriter's ReadFrom method.
// Using httpBlobWriter's Write method would send a PATCH request for every
// Write call.
func compress(in io.Reader) io.ReadCloser {
//
// The second return value is a channel that gets closed when the goroutine
// is finished. This allows the caller to make sure the goroutine finishes
// before it releases any resources connected with the reader that was
// passed in.
func compress(in io.Reader) (io.ReadCloser, chan struct{}) {
compressionDone := make(chan struct{})
pipeReader, pipeWriter := io.Pipe()
// Use a bufio.Writer to avoid excessive chunking in HTTP request.
bufWriter := bufio.NewWriterSize(pipeWriter, compressionBufSize)
@@ -190,7 +212,8 @@ func compress(in io.Reader) io.ReadCloser {
} else {
pipeWriter.Close()
}
close(compressionDone)
}()
return pipeReader
return pipeReader, compressionDone
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/distribution/metadata"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/image"
"github.com/docker/docker/image/v1"
"github.com/docker/docker/layer"
@@ -20,7 +21,6 @@ import (
)
type v1Pusher struct {
ctx context.Context
v1IDService *metadata.V1IDService
endpoint registry.APIEndpoint
ref reference.Named
@@ -38,10 +38,10 @@ func (p *v1Pusher) Push(ctx context.Context) error {
tr := transport.NewTransport(
// TODO(tiborvass): was NoTimeout
registry.NewTransport(tlsConfig),
registry.DockerHeaders(p.config.MetaHeaders)...,
registry.DockerHeaders(dockerversion.DockerUserAgent(), p.config.MetaHeaders)...,
)
client := registry.HTTPClient(tr)
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), p.config.MetaHeaders)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return fallbackError{err: err}

View File

@@ -42,7 +42,7 @@ type v2Pusher struct {
config *ImagePushConfig
repo distribution.Repository
// pushState is state built by the Download functions.
// pushState is state built by the Upload functions.
pushState pushState
}
@@ -64,12 +64,16 @@ func (p *v2Pusher) Push(ctx context.Context) (err error) {
p.repo, p.pushState.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull")
if err != nil {
logrus.Debugf("Error getting v2 registry: %v", err)
return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
return err
}
if err = p.pushV2Repository(ctx); err != nil {
if registry.ContinueOnError(err) {
return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
if continueOnError(err) {
return fallbackError{
err: err,
confirmedV2: p.pushState.confirmedV2,
transportOK: true,
}
}
}
return err
@@ -166,7 +170,11 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, ima
if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil {
logrus.Warnf("failed to upload schema2 manifest: %v - falling back to schema1", err)
builder = schema1.NewConfigManifestBuilder(p.repo.Blobs(ctx), p.config.TrustKey, p.repo.Name(), ref.Tag(), img.RawJSON())
manifestRef, err := distreference.WithTag(p.repo.Named(), ref.Tag())
if err != nil {
return err
}
builder = schema1.NewConfigManifestBuilder(p.repo.Blobs(ctx), p.config.TrustKey, manifestRef, img.RawJSON())
manifest, err = manifestFromBuilder(ctx, builder, descriptors)
if err != nil {
return err
@@ -216,10 +224,11 @@ type v2PushDescriptor struct {
repoInfo reference.Named
repo distribution.Repository
pushState *pushState
remoteDescriptor distribution.Descriptor
}
func (pd *v2PushDescriptor) Key() string {
return "v2push:" + pd.repo.Name() + " " + pd.layer.DiffID().String()
return "v2push:" + pd.repo.Named().Name() + " " + pd.layer.DiffID().String()
}
func (pd *v2PushDescriptor) ID() string {
@@ -230,16 +239,16 @@ func (pd *v2PushDescriptor) DiffID() layer.DiffID {
return pd.layer.DiffID()
}
func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) error {
func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) {
diffID := pd.DiffID()
pd.pushState.Lock()
if _, ok := pd.pushState.remoteLayers[diffID]; ok {
if descriptor, ok := pd.pushState.remoteLayers[diffID]; ok {
// it is already known that the push is not needed and
// therefore doing a stat is unnecessary
pd.pushState.Unlock()
progress.Update(progressOutput, pd.ID(), "Layer already exists")
return nil
return descriptor, nil
}
pd.pushState.Unlock()
@@ -249,14 +258,14 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
descriptor, exists, err := layerAlreadyExists(ctx, v2Metadata, pd.repoInfo, pd.repo, pd.pushState)
if err != nil {
progress.Update(progressOutput, pd.ID(), "Image push failed")
return retryOnError(err)
return distribution.Descriptor{}, retryOnError(err)
}
if exists {
progress.Update(progressOutput, pd.ID(), "Layer already exists")
pd.pushState.Lock()
pd.pushState.remoteLayers[diffID] = descriptor
pd.pushState.Unlock()
return nil
return descriptor, nil
}
}
@@ -266,27 +275,29 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
// then push the blob.
bs := pd.repo.Blobs(ctx)
var mountFrom metadata.V2Metadata
var layerUpload distribution.BlobWriter
mountAttemptsRemaining := 3
// Attempt to find another repository in the same registry to mount the layer from to avoid an unnecessary upload
for _, metadata := range v2Metadata {
sourceRepo, err := reference.ParseNamed(metadata.SourceRepository)
// Attempt to find another repository in the same registry to mount the layer
// from to avoid an unnecessary upload.
// Note: metadata is stored from oldest to newest, so we iterate through this
// slice in reverse to maximize our chances of the blob still existing in the
// remote repository.
for i := len(v2Metadata) - 1; i >= 0 && mountAttemptsRemaining > 0; i-- {
mountFrom := v2Metadata[i]
sourceRepo, err := reference.ParseNamed(mountFrom.SourceRepository)
if err != nil {
continue
}
if pd.repoInfo.Hostname() == sourceRepo.Hostname() {
logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, metadata.Digest, sourceRepo.FullName())
mountFrom = metadata
break
if pd.repoInfo.Hostname() != sourceRepo.Hostname() {
// don't mount blobs from another registry
continue
}
}
var createOpts []distribution.BlobCreateOption
if mountFrom.SourceRepository != "" {
namedRef, err := reference.WithName(mountFrom.SourceRepository)
if err != nil {
return err
continue
}
// TODO (brianbland): We need to construct a reference where the Name is
@@ -294,59 +305,66 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
// richer reference package
remoteRef, err := distreference.WithName(namedRef.RemoteName())
if err != nil {
return err
continue
}
canonicalRef, err := distreference.WithDigest(remoteRef, mountFrom.Digest)
if err != nil {
return err
continue
}
createOpts = append(createOpts, client.WithMountFrom(canonicalRef))
}
logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, mountFrom.Digest, sourceRepo.FullName())
// Send the layer
layerUpload, err := bs.Create(ctx, createOpts...)
switch err := err.(type) {
case distribution.ErrBlobMounted:
progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name())
layerUpload, err = bs.Create(ctx, client.WithMountFrom(canonicalRef))
switch err := err.(type) {
case distribution.ErrBlobMounted:
progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name())
err.Descriptor.MediaType = schema2.MediaTypeLayer
err.Descriptor.MediaType = schema2.MediaTypeLayer
pd.pushState.Lock()
pd.pushState.confirmedV2 = true
pd.pushState.remoteLayers[diffID] = err.Descriptor
pd.pushState.Unlock()
pd.pushState.Lock()
pd.pushState.confirmedV2 = true
pd.pushState.remoteLayers[diffID] = err.Descriptor
pd.pushState.Unlock()
// Cache mapping from this layer's DiffID to the blobsum
if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil {
return xfer.DoNotRetry{Err: err}
// Cache mapping from this layer's DiffID to the blobsum
if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil {
return distribution.Descriptor{}, xfer.DoNotRetry{Err: err}
}
return err.Descriptor, nil
case nil:
// blob upload session created successfully, so begin the upload
mountAttemptsRemaining = 0
default:
// unable to mount layer from this repository, so this source mapping is no longer valid
logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository)
pd.v2MetadataService.Remove(mountFrom)
mountAttemptsRemaining--
}
return nil
}
if mountFrom.SourceRepository != "" {
// unable to mount layer from this repository, so this source mapping is no longer valid
logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository)
pd.v2MetadataService.Remove(mountFrom)
}
if err != nil {
return retryOnError(err)
if layerUpload == nil {
layerUpload, err = bs.Create(ctx)
if err != nil {
return distribution.Descriptor{}, retryOnError(err)
}
}
defer layerUpload.Close()
arch, err := pd.layer.TarStream()
if err != nil {
return xfer.DoNotRetry{Err: err}
return distribution.Descriptor{}, xfer.DoNotRetry{Err: err}
}
// don't care if this fails; best effort
size, _ := pd.layer.DiffSize()
reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, arch), progressOutput, size, pd.ID(), "Pushing")
defer reader.Close()
compressedReader := compress(reader)
compressedReader, compressionDone := compress(reader)
defer func() {
reader.Close()
<-compressionDone
}()
digester := digest.Canonical.New()
tee := io.TeeReader(compressedReader, digester.Hash())
@@ -354,12 +372,12 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
nn, err := layerUpload.ReadFrom(tee)
compressedReader.Close()
if err != nil {
return retryOnError(err)
return distribution.Descriptor{}, retryOnError(err)
}
pushDigest := digester.Digest()
if _, err := layerUpload.Commit(ctx, distribution.Descriptor{Digest: pushDigest}); err != nil {
return retryOnError(err)
return distribution.Descriptor{}, retryOnError(err)
}
logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn)
@@ -367,32 +385,33 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
// Cache mapping from this layer's DiffID to the blobsum
if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: pushDigest, SourceRepository: pd.repoInfo.FullName()}); err != nil {
return xfer.DoNotRetry{Err: err}
return distribution.Descriptor{}, xfer.DoNotRetry{Err: err}
}
pd.pushState.Lock()
// If Commit succeded, that's an indication that the remote registry
// If Commit succeeded, that's an indication that the remote registry
// speaks the v2 protocol.
pd.pushState.confirmedV2 = true
pd.pushState.remoteLayers[diffID] = distribution.Descriptor{
descriptor := distribution.Descriptor{
Digest: pushDigest,
MediaType: schema2.MediaTypeLayer,
Size: nn,
}
pd.pushState.remoteLayers[diffID] = descriptor
pd.pushState.Unlock()
return nil
return descriptor, nil
}
func (pd *v2PushDescriptor) SetRemoteDescriptor(descriptor distribution.Descriptor) {
pd.remoteDescriptor = descriptor
}
func (pd *v2PushDescriptor) Descriptor() distribution.Descriptor {
// Not necessary to lock pushStatus because this is always
// called after all the mutation in pushStatus.
// By the time this function is called, every layer will have
// an entry in remoteLayers.
return pd.pushState.remoteLayers[pd.DiffID()]
return pd.remoteDescriptor
}
// layerAlreadyExists checks if the registry already know about any of the

View File

@@ -5,37 +5,19 @@ import (
"net"
"net/http"
"net/url"
"strings"
"syscall"
"time"
"github.com/docker/distribution"
"github.com/docker/distribution/registry/api/errcode"
distreference "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/distribution/xfer"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
"golang.org/x/net/context"
)
// fallbackError wraps an error that can possibly allow fallback to a different
// endpoint.
type fallbackError struct {
// err is the error being wrapped.
err error
// confirmedV2 is set to true if it was confirmed that the registry
// supports the v2 protocol. This is used to limit fallbacks to the v1
// protocol.
confirmedV2 bool
}
// Error renders the FallbackError as a string.
func (f fallbackError) Error() string {
return f.err.Error()
}
type dumbCredentialStore struct {
auth *types.AuthConfig
}
@@ -68,43 +50,21 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
DisableKeepAlives: true,
}
modifiers := registry.DockerHeaders(metaHeaders)
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders)
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.URL, "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)
if err != nil {
return nil, false, err
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()
v2Version := auth.APIVersion{
Type: "registry",
Version: "2.0",
}
versions := auth.APIVersions(resp, registry.DefaultRegistryVersionHeader)
for _, pingVersion := range versions {
if pingVersion == v2Version {
// The version header indicates we're definitely
// talking to a v2 registry. So don't allow future
// fallbacks to the v1 protocol.
foundVersion = true
break
transportOK := false
if responseErr, ok := err.(registry.PingResponseError); ok {
transportOK = true
err = responseErr.Err
}
return nil, foundVersion, fallbackError{
err: err,
confirmedV2: foundVersion,
transportOK: transportOK,
}
}
challengeManager := auth.NewSimpleChallengeManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, foundVersion, err
}
if authConfig.RegistryToken != "" {
@@ -118,8 +78,24 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
}
tr := transport.NewTransport(base, modifiers...)
repo, err = client.NewRepository(ctx, repoName, endpoint.URL, tr)
return repo, foundVersion, err
repoNameRef, err := distreference.ParseNamed(repoName)
if err != nil {
return nil, foundVersion, fallbackError{
err: err,
confirmedV2: foundVersion,
transportOK: true,
}
}
repo, err = client.NewRepository(ctx, repoNameRef, endpoint.URL.String(), tr)
if err != nil {
err = fallbackError{
err: err,
confirmedV2: foundVersion,
transportOK: true,
}
}
return
}
type existingTokenHandler struct {
@@ -134,30 +110,3 @@ func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[s
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
return nil
}
// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the
// operation after this error.
func retryOnError(err error) error {
switch v := err.(type) {
case errcode.Errors:
return retryOnError(v[0])
case errcode.Error:
switch v.Code {
case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied:
return xfer.DoNotRetry{Err: err}
}
case *url.Error:
return retryOnError(v.Err)
case *client.UnexpectedHTTPResponseError:
return xfer.DoNotRetry{Err: err}
case error:
if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) {
return xfer.DoNotRetry{Err: err}
}
}
// let's be nice and fallback if the error is a completely
// unexpected one.
// If new errors have to be handled in some way, please
// add them to the switch above.
return err
}

View File

@@ -59,6 +59,10 @@ type DownloadDescriptor interface {
DiffID() (layer.DiffID, error)
// Download is called to perform the download.
Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error)
// Close is called when the download manager is finished with this
// descriptor and will not call Download again or read from the reader
// that Download returned.
Close()
}
// DownloadDescriptorWithRegistered is a DownloadDescriptor that has an
@@ -229,6 +233,8 @@ func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor,
retries int
)
defer descriptor.Close()
for {
downloadReader, size, err = descriptor.Download(d.Transfer.Context(), progressOutput)
if err == nil {

View File

@@ -1,6 +1,7 @@
package xfer
import (
"runtime"
"sync"
"github.com/docker/docker/pkg/progress"
@@ -38,7 +39,7 @@ type Transfer interface {
Watch(progressOutput progress.Output) *Watcher
Release(*Watcher)
Context() context.Context
Cancel()
Close()
Done() <-chan struct{}
Released() <-chan struct{}
Broadcast(masterProgressChan <-chan progress.Progress)
@@ -61,11 +62,14 @@ type transfer struct {
// running remains open as long as the transfer is in progress.
running chan struct{}
// hasWatchers stays open until all watchers release the transfer.
hasWatchers chan struct{}
// released stays open until all watchers release the transfer and
// the transfer is no longer tracked by the transfer manager.
released chan struct{}
// broadcastDone is true if the master progress channel has closed.
broadcastDone bool
// closed is true if Close has been called
closed bool
// broadcastSyncChan allows watchers to "ping" the broadcasting
// goroutine to wait for it for deplete its input channel. This ensures
// a detaching watcher won't miss an event that was sent before it
@@ -78,7 +82,7 @@ func NewTransfer() Transfer {
t := &transfer{
watchers: make(map[chan struct{}]*Watcher),
running: make(chan struct{}),
hasWatchers: make(chan struct{}),
released: make(chan struct{}),
broadcastSyncChan: make(chan struct{}),
}
@@ -120,7 +124,6 @@ func (t *transfer) Broadcast(masterProgressChan <-chan progress.Progress) {
default:
}
}
} else {
t.broadcastDone = true
}
@@ -144,29 +147,34 @@ func (t *transfer) Watch(progressOutput progress.Output) *Watcher {
running: make(chan struct{}),
}
t.watchers[w.releaseChan] = w
if t.broadcastDone {
close(w.running)
return w
}
t.watchers[w.releaseChan] = w
go func() {
defer func() {
close(w.running)
}()
done := false
var (
done bool
lastWritten progress.Progress
hasLastWritten bool
)
for {
t.mu.Lock()
hasLastProgress := t.hasLastProgress
lastProgress := t.lastProgress
t.mu.Unlock()
// This might write the last progress item a
// second time (since channel closure also gets
// us here), but that's fine.
if hasLastProgress {
// Make sure we don't write the last progress item
// twice.
if hasLastProgress && (!done || !hasLastWritten || lastProgress != lastWritten) {
progressOutput.WriteProgress(lastProgress)
lastWritten = lastProgress
hasLastWritten = true
}
if done {
@@ -202,8 +210,19 @@ func (t *transfer) Release(watcher *Watcher) {
delete(t.watchers, watcher.releaseChan)
if len(t.watchers) == 0 {
close(t.hasWatchers)
t.cancel()
if t.closed {
// released may have been closed already if all
// watchers were released, then another one was added
// while waiting for a previous watcher goroutine to
// finish.
select {
case <-t.released:
default:
close(t.released)
}
} else {
t.cancel()
}
}
t.mu.Unlock()
@@ -223,9 +242,9 @@ func (t *transfer) Done() <-chan struct{} {
}
// Released returns a channel which is closed once all watchers release the
// transfer.
// transfer AND the transfer is no longer tracked by the transfer manager.
func (t *transfer) Released() <-chan struct{} {
return t.hasWatchers
return t.released
}
// Context returns the context associated with the transfer.
@@ -233,9 +252,15 @@ func (t *transfer) Context() context.Context {
return t.ctx
}
// Cancel cancels the context associated with the transfer.
func (t *transfer) Cancel() {
t.cancel()
// Close is called by the transfer manager when the transfer is no longer
// being tracked.
func (t *transfer) Close() {
t.mu.Lock()
t.closed = true
if len(t.watchers) == 0 {
close(t.released)
}
t.mu.Unlock()
}
// DoFunc is a function called by the transfer manager to actually perform
@@ -280,10 +305,33 @@ func (tm *transferManager) Transfer(key string, xferFunc DoFunc, progressOutput
tm.mu.Lock()
defer tm.mu.Unlock()
if xfer, present := tm.transfers[key]; present {
for {
xfer, present := tm.transfers[key]
if !present {
break
}
// Transfer is already in progress.
watcher := xfer.Watch(progressOutput)
return xfer, watcher
select {
case <-xfer.Context().Done():
// We don't want to watch a transfer that has been cancelled.
// Wait for it to be removed from the map and try again.
xfer.Release(watcher)
tm.mu.Unlock()
// The goroutine that removes this transfer from the
// map is also waiting for xfer.Done(), so yield to it.
// This could be avoided by adding a Closed method
// to Transfer to allow explicitly waiting for it to be
// removed the map, but forcing a scheduling round in
// this very rare case seems better than bloating the
// interface definition.
runtime.Gosched()
<-xfer.Done()
tm.mu.Lock()
default:
return xfer, watcher
}
}
start := make(chan struct{})
@@ -318,6 +366,7 @@ func (tm *transferManager) Transfer(key string, xferFunc DoFunc, progressOutput
}
delete(tm.transfers, key)
tm.mu.Unlock()
xfer.Close()
return
}
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/docker/layer"
"github.com/docker/docker/pkg/progress"
"golang.org/x/net/context"
@@ -28,8 +29,8 @@ func NewLayerUploadManager(concurrencyLimit int) *LayerUploadManager {
type uploadTransfer struct {
Transfer
diffID layer.DiffID
err error
remoteDescriptor distribution.Descriptor
err error
}
// An UploadDescriptor references a layer that may need to be uploaded.
@@ -41,7 +42,12 @@ type UploadDescriptor interface {
// DiffID should return the DiffID for this layer.
DiffID() layer.DiffID
// Upload is called to perform the Upload.
Upload(ctx context.Context, progressOutput progress.Output) error
Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error)
// SetRemoteDescriptor provides the distribution.Descriptor that was
// returned by Upload. This descriptor is not to be confused with
// the UploadDescriptor interface, which is used for internally
// identifying layers that are being uploaded.
SetRemoteDescriptor(descriptor distribution.Descriptor)
}
// Upload is a blocking function which ensures the listed layers are present on
@@ -50,7 +56,7 @@ type UploadDescriptor interface {
func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescriptor, progressOutput progress.Output) error {
var (
uploads []*uploadTransfer
dedupDescriptors = make(map[string]struct{})
dedupDescriptors = make(map[string]*uploadTransfer)
)
for _, descriptor := range layers {
@@ -60,12 +66,12 @@ func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescri
if _, present := dedupDescriptors[key]; present {
continue
}
dedupDescriptors[key] = struct{}{}
xferFunc := lum.makeUploadFunc(descriptor)
upload, watcher := lum.tm.Transfer(descriptor.Key(), xferFunc, progressOutput)
defer upload.Release(watcher)
uploads = append(uploads, upload.(*uploadTransfer))
dedupDescriptors[key] = upload.(*uploadTransfer)
}
for _, upload := range uploads {
@@ -78,6 +84,9 @@ func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescri
}
}
}
for _, l := range layers {
l.SetRemoteDescriptor(dedupDescriptors[l.Key()].remoteDescriptor)
}
return nil
}
@@ -86,7 +95,6 @@ func (lum *LayerUploadManager) makeUploadFunc(descriptor UploadDescriptor) DoFun
return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer {
u := &uploadTransfer{
Transfer: NewTransfer(),
diffID: descriptor.DiffID(),
}
go func() {
@@ -105,8 +113,9 @@ func (lum *LayerUploadManager) makeUploadFunc(descriptor UploadDescriptor) DoFun
retries := 0
for {
err := descriptor.Upload(u.Transfer.Context(), progressOutput)
remoteDescriptor, err := descriptor.Upload(u.Transfer.Context(), progressOutput)
if err == nil {
u.remoteDescriptor = remoteDescriptor
break
}

View File

@@ -0,0 +1,24 @@
package dockerversion
import (
"runtime"
"github.com/docker/docker/pkg/parsers/kernel"
"github.com/docker/docker/pkg/useragent"
)
// DockerUserAgent is the User-Agent the Docker client uses to identify itself.
// It is populated from version information of different components.
func DockerUserAgent() string {
httpVersion := make([]useragent.VersionInfo, 0, 6)
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: GitCommit})
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()})
}
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
return useragent.AppendVersions("", httpVersion...)
}

View File

@@ -9,8 +9,5 @@ const (
GitCommit string = "library-import"
Version string = "library-import"
BuildTime string = "library-import"
IAmStatic string = "library-import"
InitSHA1 string = "library-import"
InitPath string = "library-import"
)

View File

@@ -70,6 +70,16 @@ func (img *Image) ID() ID {
return img.computedID
}
// ImageID stringizes ID.
func (img *Image) ImageID() string {
return string(img.ID())
}
// RunConfig returns the image's container config.
func (img *Image) RunConfig() *container.Config {
return img.Config
}
// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
// that JSON that's been manipulated by a push/pull cycle with a legacy
// registry won't end up with a different key order.
@@ -106,7 +116,7 @@ type History struct {
// Exporter provides interface for exporting and importing images
type Exporter interface {
Load(io.ReadCloser, io.Writer) error
Load(io.ReadCloser, io.Writer, bool) error
// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
Save([]string, io.Writer) error
}

View File

@@ -231,6 +231,9 @@ func (is *store) SetParent(id, parent ID) error {
if parentMeta == nil {
return fmt.Errorf("unknown parent image ID %s", parent.String())
}
if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
delete(is.images[parent].children, id)
}
parentMeta.children[id] = struct{}{}
return is.fs.SetMetadata(id, "parent", []byte(parent))
}

View File

@@ -31,7 +31,7 @@ func HistoryFromConfig(imageJSON []byte, emptyLayer bool) (image.History, error)
return image.History{
Author: v1Image.Author,
Created: v1Image.Created,
CreatedBy: strings.Join(v1Image.ContainerConfig.Cmd.Slice(), " "),
CreatedBy: strings.Join(v1Image.ContainerConfig.Cmd, " "),
Comment: v1Image.Comment,
EmptyLayer: emptyLayer,
}, nil
@@ -97,7 +97,7 @@ func MakeConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []im
delete(c, "id")
delete(c, "parent")
delete(c, "Size") // Size is calculated from data on disk and is inconsitent
delete(c, "Size") // Size is calculated from data on disk and is inconsistent
delete(c, "parent_id")
delete(c, "layer_id")
delete(c, "throwaway")
@@ -142,7 +142,7 @@ func rawJSON(value interface{}) *json.RawMessage {
// ValidateID checks whether an ID string is a valid image ID.
func ValidateID(id string) error {
if ok := validHex.MatchString(id); !ok {
return fmt.Errorf("image ID '%s' is invalid ", id)
return fmt.Errorf("image ID %q is invalid", id)
}
return nil
}

View File

@@ -498,18 +498,21 @@ func (ls *layerStore) ReleaseRWLayer(l RWLayer) ([]Metadata, error) {
if err := ls.driver.Remove(m.mountID); err != nil {
logrus.Errorf("Error removing mounted layer %s: %s", m.name, err)
m.retakeReference(l)
return nil, err
}
if m.initID != "" {
if err := ls.driver.Remove(m.initID); err != nil {
logrus.Errorf("Error removing init layer %s: %s", m.name, err)
m.retakeReference(l)
return nil, err
}
}
if err := ls.store.RemoveMount(m.name); err != nil {
logrus.Errorf("Error removing mount metadata: %s: %s", m.name, err)
m.retakeReference(l)
return nil, err
}
@@ -574,11 +577,7 @@ func (ls *layerStore) initMount(graphID, parent, mountLabel string, initFunc Mou
}
func (ls *layerStore) assembleTarTo(graphID string, metadata io.ReadCloser, size *int64, w io.Writer) error {
type diffPathDriver interface {
DiffPath(string) (string, func() error, error)
}
diffDriver, ok := ls.driver.(diffPathDriver)
diffDriver, ok := ls.driver.(graphdriver.DiffGetterDriver)
if !ok {
diffDriver = &naiveDiffPathDriver{ls.driver}
}
@@ -586,17 +585,16 @@ func (ls *layerStore) assembleTarTo(graphID string, metadata io.ReadCloser, size
defer metadata.Close()
// get our relative path to the container
fsPath, releasePath, err := diffDriver.DiffPath(graphID)
fileGetCloser, err := diffDriver.DiffGetter(graphID)
if err != nil {
return err
}
defer releasePath()
defer fileGetCloser.Close()
metaUnpacker := storage.NewJSONUnpacker(metadata)
upackerCounter := &unpackSizeCounter{metaUnpacker, size}
fileGetter := storage.NewPathFileGetter(fsPath)
logrus.Debugf("Assembling tar data for %s from %s", graphID, fsPath)
return asm.WriteOutputTarStream(fileGetter, upackerCounter, w)
logrus.Debugf("Assembling tar data for %s", graphID)
return asm.WriteOutputTarStream(fileGetCloser, upackerCounter, w)
}
func (ls *layerStore) Cleanup() error {
@@ -615,12 +613,20 @@ type naiveDiffPathDriver struct {
graphdriver.Driver
}
func (n *naiveDiffPathDriver) DiffPath(id string) (string, func() error, error) {
type fileGetPutter struct {
storage.FileGetter
driver graphdriver.Driver
id string
}
func (w *fileGetPutter) Close() error {
return w.driver.Put(w.id)
}
func (n *naiveDiffPathDriver) DiffGetter(id string) (graphdriver.FileGetCloser, error) {
p, err := n.Driver.Get(id, "")
if err != nil {
return "", nil, err
return nil, err
}
return p, func() error {
return n.Driver.Put(id)
}, nil
return &fileGetPutter{storage.NewPathFileGetter(p), n.Driver, id}, nil
}

View File

@@ -32,7 +32,7 @@ func (ls *layerStore) CreateRWLayerByGraphID(name string, graphID string, parent
}
if !ls.driver.Exists(graphID) {
return errors.New("graph ID does not exist")
return fmt.Errorf("graph ID does not exist: %q", graphID)
}
var p *roLayer
@@ -127,6 +127,7 @@ func (ls *layerStore) checksumForGraphIDNoTarsplit(id, parent, newTarDataPath st
}
defer f.Close()
mfz := gzip.NewWriter(f)
defer mfz.Close()
metaPacker := storage.NewJSONPacker(mfz)
packerCounter := &packSizeCounter{metaPacker, &size}

View File

@@ -96,6 +96,13 @@ func (ml *mountedLayer) deleteReference(ref RWLayer) error {
return nil
}
func (ml *mountedLayer) retakeReference(r RWLayer) {
if ref, ok := r.(*referencedRWLayer); ok {
ref.activityCount = 0
ml.references[ref] = ref
}
}
type referencedRWLayer struct {
*mountedLayer

View File

@@ -1,6 +1,11 @@
package layer
import "io"
import (
"fmt"
"io"
"github.com/docker/distribution/digest"
)
type roLayer struct {
chainID ChainID
@@ -29,7 +34,11 @@ func (rl *roLayer) TarStream() (io.ReadCloser, error) {
pw.Close()
}
}()
return pr, nil
rc, err := newVerifiedReadCloser(pr, digest.Digest(rl.diffID))
if err != nil {
return nil, err
}
return rc, nil
}
func (rl *roLayer) ChainID() ChainID {
@@ -117,3 +126,39 @@ func storeLayer(tx MetadataTransaction, layer *roLayer) error {
return nil
}
func newVerifiedReadCloser(rc io.ReadCloser, dgst digest.Digest) (io.ReadCloser, error) {
verifier, err := digest.NewDigestVerifier(dgst)
if err != nil {
return nil, err
}
return &verifiedReadCloser{
rc: rc,
dgst: dgst,
verifier: verifier,
}, nil
}
type verifiedReadCloser struct {
rc io.ReadCloser
dgst digest.Digest
verifier digest.Verifier
}
func (vrc *verifiedReadCloser) Read(p []byte) (n int, err error) {
n, err = vrc.rc.Read(p)
if n > 0 {
if n, err := vrc.verifier.Write(p[:n]); err != nil {
return n, err
}
}
if err == io.EOF {
if !vrc.verifier.Verified() {
err = fmt.Errorf("could not verify layer data for: %s. This may be because internal files in the layer store were modified. Re-pulling or rebuilding this image may resolve the issue", vrc.dgst)
}
}
return
}
func (vrc *verifiedReadCloser) Close() error {
return vrc.rc.Close()
}

View File

@@ -4,16 +4,12 @@ import (
"fmt"
"net"
"net/url"
"runtime"
"strconv"
"strings"
)
var (
// DefaultHTTPPort Default HTTP Port used if only the protocol is provided to -H flag e.g. docker daemon -H tcp://
// TODO Windows. DefaultHTTPPort is only used on Windows if a -H parameter
// is not supplied. A better longer term solution would be to use a named
// pipe as the default on the Windows daemon.
// These are the IANA registered port numbers for use with Docker
// see http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker
DefaultHTTPPort = 2375 // Default HTTP Port
@@ -26,13 +22,19 @@ var (
DefaultTCPHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultHTTPPort)
// DefaultTLSHost constant defines the default host string used by docker for TLS sockets
DefaultTLSHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultTLSHTTPPort)
// DefaultNamedPipe defines the default named pipe used by docker on Windows
DefaultNamedPipe = `//./pipe/docker_engine`
)
// ValidateHost validates that the specified string is a valid host and returns it.
func ValidateHost(val string) (string, error) {
_, err := parseDockerDaemonHost(DefaultTCPHost, DefaultTLSHost, DefaultUnixSocket, "", val)
if err != nil {
return val, err
host := strings.TrimSpace(val)
// The empty string means default and is not handled by parseDockerDaemonHost
if host != "" {
_, err := parseDockerDaemonHost(host)
if err != nil {
return val, err
}
}
// Note: unlike most flag validators, we don't return the mutated value here
// we need to know what the user entered later (using ParseHost) to adjust for tls
@@ -40,39 +42,39 @@ func ValidateHost(val string) (string, error) {
}
// ParseHost and set defaults for a Daemon host string
func ParseHost(defaultHost, val string) (string, error) {
host, err := parseDockerDaemonHost(DefaultTCPHost, DefaultTLSHost, DefaultUnixSocket, defaultHost, val)
if err != nil {
return val, err
func ParseHost(defaultToTLS bool, val string) (string, error) {
host := strings.TrimSpace(val)
if host == "" {
if defaultToTLS {
host = DefaultTLSHost
} else {
host = DefaultHost
}
} else {
var err error
host, err = parseDockerDaemonHost(host)
if err != nil {
return val, err
}
}
return host, nil
}
// parseDockerDaemonHost parses the specified address and returns an address that will be used as the host.
// Depending of the address specified, will use the defaultTCPAddr or defaultUnixAddr
// defaultUnixAddr must be a absolute file path (no `unix://` prefix)
// defaultTCPAddr must be the full `tcp://host:port` form
func parseDockerDaemonHost(defaultTCPAddr, defaultTLSHost, defaultUnixAddr, defaultAddr, addr string) (string, error) {
addr = strings.TrimSpace(addr)
if addr == "" {
if defaultAddr == defaultTLSHost {
return defaultTLSHost, nil
}
if runtime.GOOS != "windows" {
return fmt.Sprintf("unix://%s", defaultUnixAddr), nil
}
return defaultTCPAddr, nil
}
// Depending of the address specified, this may return one of the global Default* strings defined in hosts.go.
func parseDockerDaemonHost(addr string) (string, error) {
addrParts := strings.Split(addr, "://")
if len(addrParts) == 1 {
if len(addrParts) == 1 && addrParts[0] != "" {
addrParts = []string{"tcp", addrParts[0]}
}
switch addrParts[0] {
case "tcp":
return parseTCPAddr(addrParts[1], defaultTCPAddr)
return parseTCPAddr(addrParts[1], DefaultTCPHost)
case "unix":
return parseUnixAddr(addrParts[1], defaultUnixAddr)
return parseSimpleProtoAddr("unix", addrParts[1], DefaultUnixSocket)
case "npipe":
return parseSimpleProtoAddr("npipe", addrParts[1], DefaultNamedPipe)
case "fd":
return addr, nil
default:
@@ -80,19 +82,19 @@ func parseDockerDaemonHost(defaultTCPAddr, defaultTLSHost, defaultUnixAddr, defa
}
}
// parseUnixAddr parses and validates that the specified address is a valid UNIX
// socket address. It returns a formatted UNIX socket address, either using the
// address parsed from addr, or the contents of defaultAddr if addr is a blank
// string.
func parseUnixAddr(addr string, defaultAddr string) (string, error) {
addr = strings.TrimPrefix(addr, "unix://")
// parseSimpleProtoAddr parses and validates that the specified address is a valid
// socket address for simple protocols like unix and npipe. It returns a formatted
// socket address, either using the address parsed from addr, or the contents of
// defaultAddr if addr is a blank string.
func parseSimpleProtoAddr(proto, addr, defaultAddr string) (string, error) {
addr = strings.TrimPrefix(addr, proto+"://")
if strings.Contains(addr, "://") {
return "", fmt.Errorf("Invalid proto, expected unix: %s", addr)
return "", fmt.Errorf("Invalid proto, expected %s: %s", proto, addr)
}
if addr == "" {
addr = defaultAddr
}
return fmt.Sprintf("unix://%s", addr), nil
return fmt.Sprintf("%s://%s", proto, addr), nil
}
// parseTCPAddr parses and validates that the specified address is a valid TCP

View File

@@ -3,4 +3,4 @@
package opts
// DefaultHost constant defines the default host string used by docker on Windows
var DefaultHost = DefaultTCPHost
var DefaultHost = "npipe://" + DefaultNamedPipe

View File

@@ -502,13 +502,13 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
defer func() {
// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil {
logrus.Debugf("Can't close tar writer: %s", err)
logrus.Errorf("Can't close tar writer: %s", err)
}
if err := compressWriter.Close(); err != nil {
logrus.Debugf("Can't close compress writer: %s", err)
logrus.Errorf("Can't close compress writer: %s", err)
}
if err := pipeWriter.Close(); err != nil {
logrus.Debugf("Can't close pipe writer: %s", err)
logrus.Errorf("Can't close pipe writer: %s", err)
}
}()
@@ -551,7 +551,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
walkRoot := getWalkRoot(srcPath, include)
filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error {
if err != nil {
logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
logrus.Errorf("Tar: Can't stat file %s to tar: %s", srcPath, err)
return nil
}
@@ -576,16 +576,42 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
if include != relFilePath {
skip, err = fileutils.OptimizedMatches(relFilePath, patterns, patDirs)
if err != nil {
logrus.Debugf("Error matching %s: %v", relFilePath, err)
logrus.Errorf("Error matching %s: %v", relFilePath, err)
return err
}
}
if skip {
if !exceptions && f.IsDir() {
// If we want to skip this file and its a directory
// then we should first check to see if there's an
// excludes pattern (eg !dir/file) that starts with this
// dir. If so then we can't skip this dir.
// Its not a dir then so we can just return/skip.
if !f.IsDir() {
return nil
}
// No exceptions (!...) in patterns so just skip dir
if !exceptions {
return filepath.SkipDir
}
return nil
dirSlash := relFilePath + string(filepath.Separator)
for _, pat := range patterns {
if pat[0] != '!' {
continue
}
pat = pat[1:] + string(filepath.Separator)
if strings.HasPrefix(pat, dirSlash) {
// found a match - so can't skip this dir
return nil
}
}
// No matching exclusion dir so just skip dir
return filepath.SkipDir
}
if seen[relFilePath] {
@@ -607,7 +633,11 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
}
if err := ta.addTarFile(filePath, relFilePath); err != nil {
logrus.Debugf("Can't add file %s to tar: %s", filePath, err)
logrus.Errorf("Can't add file %s to tar: %s", filePath, err)
// if pipe is broken, stop writting tar stream to it
if err == io.ErrClosedPipe {
return err
}
}
return nil
})
@@ -660,7 +690,7 @@ loop:
parent := filepath.Dir(hdr.Name)
parentPath := filepath.Join(dest, parent)
if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) {
err = system.MkdirAll(parentPath, 0777)
err = idtools.MkdirAllNewAs(parentPath, 0777, remappedRootUID, remappedRootGID)
if err != nil {
return err
}

View File

@@ -52,7 +52,7 @@ func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) {
if exclusion(pattern) {
pattern = pattern[1:]
}
patternDirs = append(patternDirs, strings.Split(pattern, "/"))
patternDirs = append(patternDirs, strings.Split(pattern, string(os.PathSeparator)))
}
return cleanedPatterns, patternDirs, exceptions, nil
@@ -83,8 +83,9 @@ func Matches(file string, patterns []string) (bool, error) {
// The more generic fileutils.Matches() can't make these assumptions.
func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) {
matched := false
file = filepath.FromSlash(file)
parentPath := filepath.Dir(file)
parentPathDirs := strings.Split(parentPath, "/")
parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
for i, pattern := range patterns {
negative := false
@@ -102,8 +103,8 @@ func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool,
if !match && parentPath != "." {
// Check to see if the pattern matches one of our parent dirs.
if len(patDirs[i]) <= len(parentPathDirs) {
match, _ = regexpMatch(strings.Join(patDirs[i], "/"),
strings.Join(parentPathDirs[:len(patDirs[i])], "/"))
match, _ = regexpMatch(strings.Join(patDirs[i], string(os.PathSeparator)),
strings.Join(parentPathDirs[:len(patDirs[i])], string(os.PathSeparator)))
}
}
@@ -125,6 +126,9 @@ func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool,
// of directories. This means that we should be backwards compatible
// with filepath.Match(). We'll end up supporting more stuff, due to
// the fact that we're using regexp, but that's ok - it does no harm.
//
// As per the comment in golangs filepath.Match, on Windows, escaping
// is disabled. Instead, '\\' is treated as path separator.
func regexpMatch(pattern, path string) (bool, error) {
regStr := "^"

View File

@@ -171,7 +171,7 @@ func parseSubidFile(path, username string) (ranges, error) {
}
text := strings.TrimSpace(s.Text())
if text == "" {
if text == "" || strings.HasPrefix(text, "#") {
continue
}
parts := strings.Split(text, ":")

View File

@@ -1,9 +1,7 @@
package ioutils
import (
"errors"
"io"
"net/http"
"sync"
)
@@ -11,45 +9,43 @@ import (
// is a flush. In addition, the Close method can be called to intercept
// Read/Write calls if the targets lifecycle has already ended.
type WriteFlusher struct {
mu sync.Mutex
w io.Writer
flusher http.Flusher
flushed bool
closed error
// TODO(stevvooe): Use channel for closed instead, remove mutex. Using a
// channel will allow one to properly order the operations.
w io.Writer
flusher flusher
flushed chan struct{}
flushedOnce sync.Once
closed chan struct{}
closeLock sync.Mutex
}
var errWriteFlusherClosed = errors.New("writeflusher: closed")
type flusher interface {
Flush()
}
var errWriteFlusherClosed = io.EOF
func (wf *WriteFlusher) Write(b []byte) (n int, err error) {
wf.mu.Lock()
defer wf.mu.Unlock()
if wf.closed != nil {
return 0, wf.closed
select {
case <-wf.closed:
return 0, errWriteFlusherClosed
default:
}
n, err = wf.w.Write(b)
wf.flush() // every write is a flush.
wf.Flush() // every write is a flush.
return n, err
}
// Flush the stream immediately.
func (wf *WriteFlusher) Flush() {
wf.mu.Lock()
defer wf.mu.Unlock()
wf.flush()
}
// flush the stream immediately without taking a lock. Used internally.
func (wf *WriteFlusher) flush() {
if wf.closed != nil {
select {
case <-wf.closed:
return
default:
}
wf.flushed = true
wf.flushedOnce.Do(func() {
close(wf.flushed)
})
wf.flusher.Flush()
}
@@ -59,34 +55,38 @@ func (wf *WriteFlusher) Flushed() bool {
// BUG(stevvooe): Remove this method. Its use is inherently racy. Seems to
// be used to detect whether or a response code has been issued or not.
// Another hook should be used instead.
wf.mu.Lock()
defer wf.mu.Unlock()
return wf.flushed
var flushed bool
select {
case <-wf.flushed:
flushed = true
default:
}
return flushed
}
// Close closes the write flusher, disallowing any further writes to the
// target. After the flusher is closed, all calls to write or flush will
// result in an error.
func (wf *WriteFlusher) Close() error {
wf.mu.Lock()
defer wf.mu.Unlock()
wf.closeLock.Lock()
defer wf.closeLock.Unlock()
if wf.closed != nil {
return wf.closed
select {
case <-wf.closed:
return errWriteFlusherClosed
default:
close(wf.closed)
}
wf.closed = errWriteFlusherClosed
return nil
}
// NewWriteFlusher returns a new WriteFlusher.
func NewWriteFlusher(w io.Writer) *WriteFlusher {
var flusher http.Flusher
if f, ok := w.(http.Flusher); ok {
flusher = f
var fl flusher
if f, ok := w.(flusher); ok {
fl = f
} else {
flusher = &NopFlusher{}
fl = &NopFlusher{}
}
return &WriteFlusher{w: w, flusher: flusher}
return &WriteFlusher{w: w, flusher: fl, closed: make(chan struct{}), flushed: make(chan struct{})}
}

View File

@@ -43,5 +43,10 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
return err
}
// Take platform specific action for setting create time.
if err := setCTime(name, mtime); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,14 @@
// +build !windows
package system
import (
"time"
)
//setCTime will set the create time on a file. On Unix, the create
//time is updated as a side effect of setting the modified time, so
//no action is required.
func setCTime(path string, ctime time.Time) error {
return nil
}

View File

@@ -0,0 +1,27 @@
// +build windows
package system
import (
"syscall"
"time"
)
//setCTime will set the create time on a file. On Windows, this requires
//calling SetFileTime and explicitly including the create time.
func setCTime(path string, ctime time.Time) error {
ctimespec := syscall.NsecToTimespec(ctime.UnixNano())
pathp, e := syscall.UTF16PtrFromString(path)
if e != nil {
return e
}
h, e := syscall.CreateFile(pathp,
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil,
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if e != nil {
return e
}
defer syscall.Close(h)
c := syscall.NsecToFiletime(syscall.TimespecToNsec(ctimespec))
return syscall.SetFileTime(h, &c, nil, nil)
}

View File

@@ -223,7 +223,7 @@ with matching paths, and orders the list of file sums accordingly [3].
* [2] Tar http://en.wikipedia.org/wiki/Tar_%28computing%29
* [3] Name collision https://github.com/docker/docker/commit/c5e6362c53cbbc09ddbabd5a7323e04438b57d31
## Acknowledgements
## Acknowledgments
Joffrey F (shin-) and Guillaume J. Charmes (creack) on the initial work of the
TarSum calculation.

View File

@@ -27,7 +27,6 @@ func MakeRaw(fd uintptr) (*State, error) {
newState := oldState.termios
C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&newState)))
newState.Oflag = newState.Oflag | C.OPOST
if err := tcset(fd, &newState); err != 0 {
return nil, err
}

View File

@@ -127,6 +127,5 @@ func handleInterrupt(fd uintptr, state *State) {
go func() {
_ = <-sigchan
RestoreTerminal(fd, state)
os.Exit(0)
}()
}

View File

@@ -40,8 +40,8 @@ var usingNativeConsole bool
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
switch {
case os.Getenv("ConEmuANSI") == "ON":
// The ConEmu shell emulates ANSI well by default.
return os.Stdin, os.Stdout, os.Stderr
// The ConEmu terminal emulates ANSI on output streams well.
return windows.ConEmuStreams()
case os.Getenv("MSYSTEM") != "":
// MSYS (mingw) does not emulate ANSI well.
return windows.ConsoleStreams()

View File

@@ -1,6 +1,7 @@
package reference
import (
"errors"
"fmt"
"strings"
@@ -72,7 +73,10 @@ func ParseNamed(s string) (Named, error) {
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
name = normalize(name)
name, err := normalize(name)
if err != nil {
return nil, err
}
if err := validateName(name); err != nil {
return nil, err
}
@@ -172,15 +176,18 @@ func splitHostname(name string) (hostname, remoteName string) {
// normalize returns a repository name in its normalized form, meaning it
// will not contain default hostname nor library/ prefix for official images.
func normalize(name string) string {
func normalize(name string) (string, error) {
host, remoteName := splitHostname(name)
if strings.ToLower(remoteName) != remoteName {
return "", errors.New("invalid reference format: repository name must be lowercase")
}
if host == DefaultHostname {
if strings.HasPrefix(remoteName, DefaultRepoPrefix) {
return strings.TrimPrefix(remoteName, DefaultRepoPrefix)
return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil
}
return remoteName
return remoteName, nil
}
return name
return name, nil
}
func validateName(name string) error {

View File

@@ -1,35 +1,28 @@
package registry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
)
// Login tries to register/login to the registry server.
func Login(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
// Separates the v2 registry login logic from the v1 logic.
if registryEndpoint.Version == APIVersion2 {
return loginV2(authConfig, registryEndpoint, "" /* scope */)
}
return loginV1(authConfig, registryEndpoint)
}
// loginV1 tries to register/login to the v1 registry server.
func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
var (
status string
respBody []byte
err error
respStatusCode = 0
serverAddress = authConfig.ServerAddress
)
func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
if err != nil {
return "", err
}
serverAddress := registryEndpoint.String()
logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
@@ -39,186 +32,121 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
loginAgainstOfficialIndex := serverAddress == IndexServer
// to avoid sending the server address to the server it should be removed before being marshaled
authCopy := *authConfig
authCopy.ServerAddress = ""
jsonBody, err := json.Marshal(authCopy)
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
if err != nil {
return "", fmt.Errorf("Config Error: %s", err)
return "", err
}
// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
b := strings.NewReader(string(jsonBody))
resp1, err := registryEndpoint.client.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
if err != nil {
return "", fmt.Errorf("Server Error: %s", err)
}
defer resp1.Body.Close()
respStatusCode = resp1.StatusCode
respBody, err = ioutil.ReadAll(resp1.Body)
if err != nil {
return "", fmt.Errorf("Server Error: [%#v] %s", respStatusCode, err)
}
if respStatusCode == 201 {
if loginAgainstOfficialIndex {
status = "Account created. Please use the confirmation link we sent" +
" to your e-mail to activate it."
} else {
// *TODO: Use registry configuration to determine what this says, if anything?
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
}
} else if respStatusCode == 400 {
if string(respBody) == "\"Username or email already exists\"" {
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := registryEndpoint.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode == 200 {
return "Login Succeeded", nil
} else if resp.StatusCode == 401 {
return "", fmt.Errorf("Wrong login/password, please try again")
} else if resp.StatusCode == 403 {
if loginAgainstOfficialIndex {
return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
}
// *TODO: Use registry configuration to determine what this says, if anything?
return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
} else if resp.StatusCode == 500 { // Issue #14326
logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
return "", fmt.Errorf("Internal Server Error")
}
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
}
return "", fmt.Errorf("Registration: %s", respBody)
} else if respStatusCode == 401 {
// This case would happen with private registries where /v1/users is
// protected, so people can use `docker login` as an auth check.
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := registryEndpoint.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode == 200 {
return "Login Succeeded", nil
} else if resp.StatusCode == 401 {
return "", fmt.Errorf("Wrong login/password, please try again")
} else {
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
}
} else {
return "", fmt.Errorf("Unexpected status code [%d] : %s", respStatusCode, respBody)
}
return status, nil
}
// loginV2 tries to login to the v2 registry server. The given registry endpoint has been
// pinged or setup with a list of authorization challenges. Each of these challenges are
// tried until one of them succeeds. Currently supported challenge schemes are:
// HTTP Basic Authorization
// Token Authorization with a separate token issuing server
// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
// now, users should create their account through other means like directly from a web page
// served by the v2 registry service provider. Whether this will be supported in the future
// is to be determined.
func loginV2(authConfig *types.AuthConfig, registryEndpoint *Endpoint, scope string) (string, error) {
logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
var (
err error
allErrors []error
)
for _, challenge := range registryEndpoint.AuthChallenges {
params := make(map[string]string, len(challenge.Parameters)+1)
for k, v := range challenge.Parameters {
params[k] = v
}
params["scope"] = scope
logrus.Debugf("trying %q auth challenge with params %v", challenge.Scheme, params)
switch strings.ToLower(challenge.Scheme) {
case "basic":
err = tryV2BasicAuthLogin(authConfig, params, registryEndpoint)
case "bearer":
err = tryV2TokenAuthLogin(authConfig, params, registryEndpoint)
default:
// Unsupported challenge types are explicitly skipped.
err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
}
if err == nil {
return "Login Succeeded", nil
}
logrus.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
allErrors = append(allErrors, err)
}
return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
}
func tryV2BasicAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
if err != nil {
return err
}
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := registryEndpoint.client.Do(req)
if err != nil {
return err
// fallback when request could not be completed
return "", fallbackError{
err: err,
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode == http.StatusOK {
return "Login Succeeded", nil
} else if resp.StatusCode == http.StatusUnauthorized {
if loginAgainstOfficialIndex {
return "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
}
return "", fmt.Errorf("Wrong login/password, please try again")
} else if resp.StatusCode == http.StatusForbidden {
if loginAgainstOfficialIndex {
return "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
}
// *TODO: Use registry configuration to determine what this says, if anything?
return "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
return "", fmt.Errorf("Internal Server Error")
} else {
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
}
return nil
}
func tryV2TokenAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint)
type loginCredentialStore struct {
authConfig *types.AuthConfig
}
func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
return lcs.authConfig.Username, lcs.authConfig.Password
}
type fallbackError struct {
err error
}
func (err fallbackError) Error() string {
return err.err.Error()
}
// loginV2 tries to login to the v2 registry server. The given registry
// endpoint will be pinged to get authorization challenges. These challenges
// will be used to authenticate against the registry to validate credentials.
func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
modifiers := DockerHeaders(userAgent, nil)
authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
if err != nil {
return err
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
if err != nil {
return err
creds := loginCredentialStore{
authConfig: authConfig,
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
tr := transport.NewTransport(authTransport, modifiers...)
resp, err := registryEndpoint.client.Do(req)
loginClient := &http.Client{
Transport: tr,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return err
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
resp, err := loginClient.Do(req)
if err != nil {
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
return nil
return "Login Succeeded", nil
}
// ResolveAuthConfig matches an auth configuration to a server address or a URL
@@ -253,3 +181,63 @@ func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registryt
// When all else fails, return an empty auth config
return types.AuthConfig{}
}
// PingResponseError is used when the response from a ping
// was received but invalid.
type PingResponseError struct {
Err error
}
func (err PingResponseError) Error() string {
return err.Error()
}
// PingV2Registry attempts to ping a v2 registry and on success return a
// challenge manager for the supported authentication types and
// whether v2 was confirmed by the response. If a response is received but
// cannot be interpreted a PingResponseError will be returned.
func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
var (
foundV2 = false
v2Version = auth.APIVersion{
Type: "registry",
Version: "2.0",
}
)
pingClient := &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return nil, false, err
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()
versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
for _, pingVersion := range versions {
if pingVersion == v2Version {
// The version header indicates we're definitely
// talking to a v2 registry. So don't allow future
// fallbacks to the v1 protocol.
foundV2 = true
break
}
}
challengeManager := auth.NewSimpleChallengeManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, foundV2, PingResponseError{
Err: err,
}
}
return challengeManager, foundV2, nil
}

View File

@@ -1,150 +0,0 @@
package registry
import (
"net/http"
"strings"
)
// Octet types from RFC 2616.
type octetType byte
// AuthorizationChallenge carries information
// from a WWW-Authenticate response header.
type AuthorizationChallenge struct {
Scheme string
Parameters map[string]string
}
var octetTypes [256]octetType
const (
isToken octetType = 1 << iota
isSpace
)
func init() {
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
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 {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {
t |= isToken
}
octetTypes[c] = t
}
}
func parseAuthHeader(header http.Header) []*AuthorizationChallenge {
var challenges []*AuthorizationChallenge
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
v, p := parseValueAndParams(h)
if v != "" {
challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p})
}
}
return challenges
}
func parseValueAndParams(header string) (value string, params map[string]string) {
params = make(map[string]string)
value, s := expectToken(header)
if value == "" {
return
}
value = strings.ToLower(value)
s = "," + skipSpace(s)
for strings.HasPrefix(s, ",") {
var pkey string
pkey, s = expectToken(skipSpace(s[1:]))
if pkey == "" {
return
}
if !strings.HasPrefix(s, "=") {
return
}
var pvalue string
pvalue, s = expectTokenOrQuoted(s[1:])
if pvalue == "" {
return
}
pkey = strings.ToLower(pkey)
params[pkey] = pvalue
s = skipSpace(s)
}
return
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
break
}
}
return s[i:]
}
func expectToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isToken == 0 {
break
}
}
return s[:i], s[i:]
}
func expectTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return expectToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + i; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j++
}
}
return "", ""
}
}
return "", ""
}

View File

@@ -19,7 +19,7 @@ type Options struct {
InsecureRegistries opts.ListOpts
}
const (
var (
// DefaultNamespace is the default namespace
DefaultNamespace = "docker.io"
// DefaultRegistryVersionHeader is the name of the default HTTP header
@@ -27,7 +27,7 @@ const (
DefaultRegistryVersionHeader = "Docker-Distribution-Api-Version"
// IndexServer is the v1 registry server used for user auth + account creation
IndexServer = DefaultV1Registry + "/v1/"
IndexServer = DefaultV1Registry.String() + "/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
@@ -49,6 +49,9 @@ var (
V2Only = false
)
// for mocking in unit tests
var lookupIP = net.LookupIP
// InstallFlags adds command-line options to the top-level flag parser for
// the current process.
func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) {

View File

@@ -2,12 +2,22 @@
package registry
const (
import (
"net/url"
)
var (
// DefaultV1Registry is the URI of the default v1 registry
DefaultV1Registry = "https://index.docker.io"
DefaultV1Registry = &url.URL{
Scheme: "https",
Host: "index.docker.io",
}
// DefaultV2Registry is the URI of the default v2 registry
DefaultV2Registry = "https://registry-1.docker.io"
DefaultV2Registry = &url.URL{
Scheme: "https",
Host: "registry-1.docker.io",
}
)
var (

View File

@@ -1,21 +1,28 @@
package registry
import (
"net/url"
"os"
"path/filepath"
"strings"
)
const (
var (
// DefaultV1Registry is the URI of the default v1 registry
DefaultV1Registry = "https://registry-win-tp3.docker.io"
DefaultV1Registry = &url.URL{
Scheme: "https",
Host: "registry-win-tp3.docker.io",
}
// DefaultV2Registry is the URI of the default (official) v2 registry.
// This is the windows-specific endpoint.
//
// Currently it is a TEMPORARY link that allows Microsoft to continue
// development of Docker Engine for Windows.
DefaultV2Registry = "https://registry-win-tp3.docker.io"
DefaultV2Registry = &url.URL{
Scheme: "https",
Host: "registry-win-tp3.docker.io",
}
)
// CertsDir is the directory where certificates are stored

View File

@@ -1,277 +0,0 @@
package registry
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/engine-api/types/registry"
)
// for mocking in unit tests
var lookupIP = net.LookupIP
// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
func scanForAPIVersion(address string) (string, APIVersion) {
var (
chunks []string
apiVersionStr string
)
if strings.HasSuffix(address, "/") {
address = address[:len(address)-1]
}
chunks = strings.Split(address, "/")
apiVersionStr = chunks[len(chunks)-1]
for k, v := range apiVersions {
if apiVersionStr == v {
address = strings.Join(chunks[:len(chunks)-1], "/")
return address, k
}
}
return address, APIVersionUnknown
}
// NewEndpoint parses the given address to return a registry endpoint. v can be used to
// specify a specific endpoint version
func NewEndpoint(index *registrytypes.IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
if err != nil {
return nil, err
}
endpoint, err := newEndpoint(GetAuthConfigKey(index), tlsConfig, metaHeaders)
if err != nil {
return nil, err
}
if v != APIVersionUnknown {
endpoint.Version = v
}
if err := validateEndpoint(endpoint); err != nil {
return nil, err
}
return endpoint, nil
}
func validateEndpoint(endpoint *Endpoint) error {
logrus.Debugf("pinging registry endpoint %s", endpoint)
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.Ping(); err != nil {
if endpoint.IsSecure {
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
}
// If registry is insecure and HTTPS failed, fallback to HTTP.
logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
endpoint.URL.Scheme = "http"
var err2 error
if _, err2 = endpoint.Ping(); err2 == nil {
return nil
}
return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
return nil
}
func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) {
var (
endpoint = new(Endpoint)
trimmedAddress string
err error
)
if !strings.HasPrefix(address, "http") {
address = "https://" + address
}
endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify)
trimmedAddress, endpoint.Version = scanForAPIVersion(address)
if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
return nil, err
}
// TODO(tiborvass): make sure a ConnectTimeout transport is used
tr := NewTransport(tlsConfig)
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
return endpoint, nil
}
// Endpoint stores basic information about a registry endpoint.
type Endpoint struct {
client *http.Client
URL *url.URL
Version APIVersion
IsSecure bool
AuthChallenges []*AuthorizationChallenge
URLBuilder *v2.URLBuilder
}
// Get the formatted URL for the root of this registry Endpoint
func (e *Endpoint) String() string {
return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
}
// VersionString returns a formatted string of this
// endpoint address using the given API Version.
func (e *Endpoint) VersionString(version APIVersion) string {
return fmt.Sprintf("%s/v%d/", e.URL, version)
}
// Path returns a formatted string for the URL
// of this endpoint with the given path appended.
func (e *Endpoint) Path(path string) string {
return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
}
// Ping pings the remote endpoint with v2 and v1 pings to determine the API
// version. It returns a PingResult containing the discovered version. The
// PingResult also indicates whether the registry is standalone or not.
func (e *Endpoint) Ping() (PingResult, error) {
// The ping logic to use is determined by the registry endpoint version.
switch e.Version {
case APIVersion1:
return e.pingV1()
case APIVersion2:
return e.pingV2()
}
// APIVersionUnknown
// We should try v2 first...
e.Version = APIVersion2
regInfo, errV2 := e.pingV2()
if errV2 == nil {
return regInfo, nil
}
// ... then fallback to v1.
e.Version = APIVersion1
regInfo, errV1 := e.pingV1()
if errV1 == nil {
return regInfo, nil
}
e.Version = APIVersionUnknown
return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
}
func (e *Endpoint) pingV1() (PingResult, error) {
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
if e.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fallback to http in case of error)
return PingResult{Standalone: false}, nil
}
req, err := http.NewRequest("GET", e.Path("_ping"), nil)
if err != nil {
return PingResult{Standalone: false}, err
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{Standalone: false}, err
}
defer resp.Body.Close()
jsonString, err := ioutil.ReadAll(resp.Body)
if err != nil {
return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
}
// If the header is absent, we assume true for compatibility with earlier
// versions of the registry. default to true
info := PingResult{
Standalone: true,
}
if err := json.Unmarshal(jsonString, &info); err != nil {
logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
// don't stop here. Just assume sane defaults
}
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
logrus.Debugf("Registry version header: '%s'", hdr)
info.Version = hdr
}
logrus.Debugf("PingResult.Version: %q", info.Version)
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
logrus.Debugf("Registry standalone header: '%s'", standalone)
// Accepted values are "true" (case-insensitive) and "1".
if strings.EqualFold(standalone, "true") || standalone == "1" {
info.Standalone = true
} else if len(standalone) > 0 {
// there is a header set, and it is not "true" or "1", so assume fails
info.Standalone = false
}
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
return info, nil
}
func (e *Endpoint) pingV2() (PingResult, error) {
logrus.Debugf("attempting v2 ping for registry endpoint %s", e)
req, err := http.NewRequest("GET", e.Path(""), nil)
if err != nil {
return PingResult{}, err
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{}, err
}
defer resp.Body.Close()
// The endpoint may have multiple supported versions.
// Ensure it supports the v2 Registry API.
var supportsV2 bool
HeaderLoop:
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
for _, versionName := range strings.Fields(supportedVersions) {
if versionName == "registry/2.0" {
supportsV2 = true
break HeaderLoop
}
}
}
if !supportsV2 {
return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e)
}
if resp.StatusCode == http.StatusOK {
// It would seem that no authentication/authorization is required.
// So we don't need to parse/add any authorization schemes.
return PingResult{Standalone: true}, nil
}
if resp.StatusCode == http.StatusUnauthorized {
// Parse the WWW-Authenticate Header and store the challenges
// on this endpoint object.
e.AuthChallenges = parseAuthHeader(resp.Header)
return PingResult{}, nil
}
return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
}

199
vendor/github.com/docker/docker/registry/endpoint_v1.go generated vendored Normal file
View File

@@ -0,0 +1,199 @@
package registry
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/engine-api/types/registry"
)
// V1Endpoint stores basic information about a V1 registry endpoint.
type V1Endpoint struct {
client *http.Client
URL *url.URL
IsSecure bool
}
// NewV1Endpoint parses the given address to return a registry endpoint. v can be used to
// specify a specific endpoint version
func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
if err != nil {
return nil, err
}
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
if err != nil {
return nil, err
}
if err := validateEndpoint(endpoint); err != nil {
return nil, err
}
return endpoint, nil
}
func validateEndpoint(endpoint *V1Endpoint) error {
logrus.Debugf("pinging registry endpoint %s", endpoint)
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.Ping(); err != nil {
if endpoint.IsSecure {
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
}
// If registry is insecure and HTTPS failed, fallback to HTTP.
logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
endpoint.URL.Scheme = "http"
var err2 error
if _, err2 = endpoint.Ping(); err2 == nil {
return nil
}
return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
return nil
}
func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
endpoint := &V1Endpoint{
IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
URL: new(url.URL),
}
*endpoint.URL = address
// TODO(tiborvass): make sure a ConnectTimeout transport is used
tr := NewTransport(tlsConfig)
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...))
return endpoint, nil
}
// trimV1Address trims the version off the address and returns the
// trimmed address or an error if there is a non-V1 version.
func trimV1Address(address string) (string, error) {
var (
chunks []string
apiVersionStr string
)
if strings.HasSuffix(address, "/") {
address = address[:len(address)-1]
}
chunks = strings.Split(address, "/")
apiVersionStr = chunks[len(chunks)-1]
if apiVersionStr == "v1" {
return strings.Join(chunks[:len(chunks)-1], "/"), nil
}
for k, v := range apiVersions {
if k != APIVersion1 && apiVersionStr == v {
return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr)
}
}
return address, nil
}
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "https://" + address
}
address, err := trimV1Address(address)
if err != nil {
return nil, err
}
uri, err := url.Parse(address)
if err != nil {
return nil, err
}
endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders)
if err != nil {
return nil, err
}
return endpoint, nil
}
// Get the formatted URL for the root of this registry Endpoint
func (e *V1Endpoint) String() string {
return e.URL.String() + "/v1/"
}
// Path returns a formatted string for the URL
// of this endpoint with the given path appended.
func (e *V1Endpoint) Path(path string) string {
return e.URL.String() + "/v1/" + path
}
// Ping returns a PingResult which indicates whether the registry is standalone or not.
func (e *V1Endpoint) Ping() (PingResult, error) {
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
if e.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fallback to http in case of error)
return PingResult{Standalone: false}, nil
}
req, err := http.NewRequest("GET", e.Path("_ping"), nil)
if err != nil {
return PingResult{Standalone: false}, err
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{Standalone: false}, err
}
defer resp.Body.Close()
jsonString, err := ioutil.ReadAll(resp.Body)
if err != nil {
return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
}
// If the header is absent, we assume true for compatibility with earlier
// versions of the registry. default to true
info := PingResult{
Standalone: true,
}
if err := json.Unmarshal(jsonString, &info); err != nil {
logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
// don't stop here. Just assume sane defaults
}
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
logrus.Debugf("Registry version header: '%s'", hdr)
info.Version = hdr
}
logrus.Debugf("PingResult.Version: %q", info.Version)
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
logrus.Debugf("Registry standalone header: '%s'", standalone)
// Accepted values are "true" (case-insensitive) and "1".
if strings.EqualFold(standalone, "true") || standalone == "1" {
info.Standalone = true
} else if len(standalone) > 0 {
// there is a header set, and it is not "true" or "1", so assume fails
info.Standalone = false
}
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
return info, nil
}

View File

@@ -13,17 +13,10 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/pkg/parsers/kernel"
"github.com/docker/docker/pkg/useragent"
"github.com/docker/go-connections/tlsconfig"
)
@@ -31,26 +24,9 @@ var (
// ErrAlreadyExists is an error returned if an image being pushed
// already exists on the remote side
ErrAlreadyExists = errors.New("Image already exists")
errLoginRequired = errors.New("Authentication is required.")
)
// dockerUserAgent is the User-Agent the Docker client uses to identify itself.
// It is populated on init(), comprising version information of different components.
var dockerUserAgent string
func init() {
httpVersion := make([]useragent.VersionInfo, 0, 6)
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: dockerversion.Version})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: dockerversion.GitCommit})
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()})
}
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
dockerUserAgent = useragent.AppendVersions("", httpVersion...)
if runtime.GOOS != "linux" {
V2Only = true
}
@@ -130,12 +106,13 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
return nil
}
// DockerHeaders returns request modifiers that ensure requests have
// the User-Agent header set to dockerUserAgent and that metaHeaders
// are added.
func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier {
modifiers := []transport.RequestModifier{
transport.NewHeaderRequestModifier(http.Header{"User-Agent": []string{dockerUserAgent}}),
// DockerHeaders returns request modifiers with a User-Agent and metaHeaders
func DockerHeaders(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
modifiers := []transport.RequestModifier{}
if userAgent != "" {
modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
"User-Agent": []string{userAgent},
}))
}
if metaHeaders != nil {
modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
@@ -188,51 +165,6 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
return nil
}
// ShouldV2Fallback returns true if this error is a reason to fall back to v1.
func ShouldV2Fallback(err errcode.Error) bool {
switch err.Code {
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
return true
}
return false
}
// ErrNoSupport is an error type used for errors indicating that an operation
// is not supported. It encapsulates a more specific error.
type ErrNoSupport struct{ Err error }
func (e ErrNoSupport) Error() string {
if e.Err == nil {
return "not supported"
}
return e.Err.Error()
}
// ContinueOnError returns true if we should fallback to the next endpoint
// as a result of this error.
func ContinueOnError(err error) bool {
switch v := err.(type) {
case errcode.Errors:
if len(v) == 0 {
return true
}
return ContinueOnError(v[0])
case ErrNoSupport:
return ContinueOnError(v.Err)
case errcode.Error:
return ShouldV2Fallback(v)
case *client.UnexpectedHTTPResponseError:
return true
case error:
return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error()))
}
// let's be nice and fallback if the error is a completely
// unexpected one.
// If new errors have to be handled in some way, please
// add them to the switch above.
return true
}
// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
// default TLS configuration.
func NewTransport(tlsConfig *tls.Config) *http.Transport {

View File

@@ -6,6 +6,7 @@ import (
"net/url"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/reference"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
@@ -28,29 +29,31 @@ func NewService(options *Options) *Service {
// Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials.
func (s *Service) Auth(authConfig *types.AuthConfig) (string, error) {
addr := authConfig.ServerAddress
if addr == "" {
// Use the official registry address if not specified.
addr = IndexServer
}
index, err := s.ResolveIndex(addr)
func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) {
endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress)
if err != nil {
return "", err
}
endpointVersion := APIVersion(APIVersionUnknown)
if V2Only {
// Override the endpoint to only attempt a v2 ping
endpointVersion = APIVersion2
}
for _, endpoint := range endpoints {
login := loginV2
if endpoint.Version == APIVersion1 {
login = loginV1
}
endpoint, err := NewEndpoint(index, nil, endpointVersion)
if err != nil {
status, err = login(authConfig, endpoint, userAgent)
if err == nil {
return
}
if fErr, ok := err.(fallbackError); ok {
err = fErr.err
logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
continue
}
return "", err
}
authConfig.ServerAddress = endpoint.String()
return Login(authConfig, endpoint)
return "", err
}
// splitReposSearchTerm breaks a search term into an index name and remote name
@@ -72,7 +75,7 @@ func splitReposSearchTerm(reposName string) (string, string) {
// Search queries the public registry for images matching the specified
// search terms, and returns the results.
func (s *Service) Search(term string, authConfig *types.AuthConfig, headers map[string][]string) (*registrytypes.SearchResults, error) {
func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
if err := validateNoSchema(term); err != nil {
return nil, err
}
@@ -85,7 +88,7 @@ func (s *Service) Search(term string, authConfig *types.AuthConfig, headers map[
}
// *TODO: Search multiple indexes.
endpoint, err := NewEndpoint(index, http.Header(headers), APIVersionUnknown)
endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers))
if err != nil {
return nil, err
}
@@ -121,7 +124,7 @@ func (s *Service) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
// APIEndpoint represents a remote API endpoint
type APIEndpoint struct {
Mirror bool
URL string
URL *url.URL
Version APIVersion
Official bool
TrimHostname bool
@@ -129,8 +132,8 @@ type APIEndpoint struct {
}
// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
func (e APIEndpoint) ToV1Endpoint(metaHeaders http.Header) (*Endpoint, error) {
return newEndpoint(e.URL, e.TLSConfig, metaHeaders)
func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
}
// TLSConfig constructs a client TLS configuration based on server defaults
@@ -138,26 +141,22 @@ func (s *Service) TLSConfig(hostname string) (*tls.Config, error) {
return newTLSConfig(hostname, isSecureIndex(s.Config, hostname))
}
func (s *Service) tlsConfigForMirror(mirror string) (*tls.Config, error) {
mirrorURL, err := url.Parse(mirror)
if err != nil {
return nil, err
}
func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
return s.TLSConfig(mirrorURL.Host)
}
// LookupPullEndpoints creates an list of endpoints to try to pull from, in order of preference.
// It gives preference to v2 endpoints over v1, mirrors over the actual
// registry, and HTTPS over plain HTTP.
func (s *Service) LookupPullEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
return s.lookupEndpoints(repoName)
func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
return s.lookupEndpoints(hostname)
}
// LookupPushEndpoints creates an list of endpoints to try to push to, in order of preference.
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
// Mirrors are not included.
func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
allEndpoints, err := s.lookupEndpoints(repoName)
func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
allEndpoints, err := s.lookupEndpoints(hostname)
if err == nil {
for _, endpoint := range allEndpoints {
if !endpoint.Mirror {
@@ -168,8 +167,8 @@ func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []API
return endpoints, err
}
func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
endpoints, err = s.lookupV2Endpoints(repoName)
func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
endpoints, err = s.lookupV2Endpoints(hostname)
if err != nil {
return nil, err
}
@@ -178,7 +177,7 @@ func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndp
return endpoints, nil
}
legacyEndpoints, err := s.lookupV1Endpoints(repoName)
legacyEndpoints, err := s.lookupV1Endpoints(hostname)
if err != nil {
return nil, err
}

View File

@@ -1,18 +1,15 @@
package registry
import (
"fmt"
"strings"
"net/url"
"github.com/docker/docker/reference"
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
nameString := repoName.FullName()
if strings.HasPrefix(nameString, DefaultNamespace+"/") {
if hostname == DefaultNamespace {
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV1Registry,
Version: APIVersion1,
@@ -23,12 +20,6 @@ func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEn
return endpoints, nil
}
slashIndex := strings.IndexRune(nameString, '/')
if slashIndex <= 0 {
return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString)
}
hostname := nameString[:slashIndex]
tlsConfig, err = s.TLSConfig(hostname)
if err != nil {
return nil, err
@@ -36,7 +27,10 @@ func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEn
endpoints = []APIEndpoint{
{
URL: "https://" + hostname,
URL: &url.URL{
Scheme: "https",
Host: hostname,
},
Version: APIVersion1,
TrimHostname: true,
TLSConfig: tlsConfig,
@@ -45,7 +39,10 @@ func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEn
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{ // or this
URL: "http://" + hostname,
URL: &url.URL{
Scheme: "http",
Host: hostname,
},
Version: APIVersion1,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify

View File

@@ -1,26 +1,31 @@
package registry
import (
"fmt"
"net/url"
"strings"
"github.com/docker/docker/reference"
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
nameString := repoName.FullName()
if strings.HasPrefix(nameString, DefaultNamespace+"/") {
if hostname == DefaultNamespace {
// v2 mirrors
for _, mirror := range s.Config.Mirrors {
mirrorTLSConfig, err := s.tlsConfigForMirror(mirror)
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
mirror = "https://" + mirror
}
mirrorURL, err := url.Parse(mirror)
if err != nil {
return nil, err
}
mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL)
if err != nil {
return nil, err
}
endpoints = append(endpoints, APIEndpoint{
URL: mirror,
URL: mirrorURL,
// guess mirrors are v2
Version: APIVersion2,
Mirror: true,
@@ -40,12 +45,6 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn
return endpoints, nil
}
slashIndex := strings.IndexRune(nameString, '/')
if slashIndex <= 0 {
return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString)
}
hostname := nameString[:slashIndex]
tlsConfig, err = s.TLSConfig(hostname)
if err != nil {
return nil, err
@@ -53,7 +52,10 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn
endpoints = []APIEndpoint{
{
URL: "https://" + hostname,
URL: &url.URL{
Scheme: "https",
Host: hostname,
},
Version: APIVersion2,
TrimHostname: true,
TLSConfig: tlsConfig,
@@ -62,7 +64,10 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{
URL: "http://" + hostname,
URL: &url.URL{
Scheme: "http",
Host: hostname,
},
Version: APIVersion2,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify

View File

@@ -19,6 +19,7 @@ import (
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/docker/pkg/httputils"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stringid"
@@ -36,7 +37,7 @@ var (
// A Session is used to communicate with a V1 registry
type Session struct {
indexEndpoint *Endpoint
indexEndpoint *V1Endpoint
client *http.Client
// TODO(tiborvass): remove authConfig
authConfig *types.AuthConfig
@@ -162,7 +163,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
// NewSession creates a new session
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *Endpoint) (r *Session, err error) {
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) {
r = &Session{
authConfig: authConfig,
client: client,
@@ -174,7 +175,7 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *End
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
// alongside all our requests.
if endpoint.VersionString(1) != IndexServer && endpoint.URL.Scheme == "https" {
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
info, err := endpoint.Ping()
if err != nil {
return nil, err
@@ -213,7 +214,7 @@ func (r *Session) GetRemoteHistory(imgID, registry string) ([]string, error) {
defer res.Body.Close()
if res.StatusCode != 200 {
if res.StatusCode == 401 {
return nil, errLoginRequired
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
}
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res)
}
@@ -404,7 +405,7 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
// GetRepositoryData returns lists of images and endpoints for the repository
func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) {
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), name.RemoteName())
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName())
logrus.Debugf("[registry] Calling GET %s", repositoryTarget)
@@ -427,7 +428,7 @@ func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, erro
}
defer res.Body.Close()
if res.StatusCode == 401 {
return nil, errLoginRequired
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
}
// TODO: Right now we're ignoring checksums in the response body.
// In the future, we need to use them to check image validity.
@@ -443,7 +444,7 @@ func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, erro
var endpoints []string
if res.Header.Get("X-Docker-Endpoints") != "" {
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
if err != nil {
return nil, err
}
@@ -633,7 +634,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
if validate {
suffix = "images"
}
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote.RemoteName(), suffix)
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix)
logrus.Debugf("[registry] PUT %s", u)
logrus.Debugf("Image list pushed to index:\n%s", imgListJSON)
headers := map[string][]string{
@@ -661,7 +662,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
defer res.Body.Close()
if res.StatusCode == 401 {
return nil, errLoginRequired
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
}
var tokens, endpoints []string
@@ -679,7 +680,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
if res.Header.Get("X-Docker-Endpoints") == "" {
return nil, fmt.Errorf("Index response didn't contain any endpoints")
}
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
if err != nil {
return nil, err
}
@@ -721,7 +722,7 @@ func shouldRedirect(response *http.Response) bool {
// SearchRepositories performs a search against the remote repository
func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) {
logrus.Debugf("Index server: %s", r.indexEndpoint)
u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term)
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
@@ -751,7 +752,6 @@ func (r *Session) GetAuthConfig(withPasswd bool) *types.AuthConfig {
return &types.AuthConfig{
Username: r.authConfig.Username,
Password: password,
Email: r.authConfig.Email,
}
}

View File

@@ -1,81 +0,0 @@
package registry
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
type tokenResponse struct {
Token string `json:"token"`
}
func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint) (string, error) {
realm, ok := params["realm"]
if !ok {
return "", errors.New("no realm specified for token auth challenge")
}
realmURL, err := url.Parse(realm)
if err != nil {
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
}
if realmURL.Scheme == "" {
if registryEndpoint.IsSecure {
realmURL.Scheme = "https"
} else {
realmURL.Scheme = "http"
}
}
req, err := http.NewRequest("GET", realmURL.String(), nil)
if err != nil {
return "", err
}
reqParams := req.URL.Query()
service := params["service"]
scope := params["scope"]
if service != "" {
reqParams.Add("service", service)
}
for _, scopeField := range strings.Fields(scope) {
reqParams.Add("scope", scopeField)
}
if username != "" {
reqParams.Add("account", username)
req.SetBasicAuth(username, password)
}
req.URL.RawQuery = reqParams.Encode()
resp, err := registryEndpoint.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
}
decoder := json.NewDecoder(resp.Body)
tr := new(tokenResponse)
if err = decoder.Decode(tr); err != nil {
return "", fmt.Errorf("unable to decode token response: %s", err)
}
if tr.Token == "" {
return "", errors.New("authorization server did not include a token in the response")
}
return tr.Token, nil
}

View File

@@ -46,18 +46,18 @@ func (av APIVersion) String() string {
return apiVersions[av]
}
var apiVersions = map[APIVersion]string{
1: "v1",
2: "v2",
}
// API Version identifiers.
const (
APIVersionUnknown = iota
APIVersion1
_ = iota
APIVersion1 APIVersion = iota
APIVersion2
)
var apiVersions = map[APIVersion]string{
APIVersion1: "v1",
APIVersion2: "v2",
}
// RepositoryInfo describes a repository
type RepositoryInfo struct {
reference.Named

View File

@@ -2,10 +2,15 @@ package types
// AuthConfig contains authorization information for connecting to a Registry
type AuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth"`
Email string `json:"email"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth,omitempty"`
// Email is an optional value associated with the username.
// This field is deprecated and will be removed in a later
// version of docker.
Email string `json:"email,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"`
RegistryToken string `json:"registrytoken,omitempty"`
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/filters"
"github.com/docker/engine-api/types/image"
"github.com/docker/go-units"
)
@@ -127,8 +126,8 @@ type ImageBuildOptions struct {
NoCache bool
Remove bool
ForceRemove bool
PullParent image.PullBehavior
IsolationLevel container.IsolationLevel
PullParent bool
Isolation container.Isolation
CPUSetCPUs string
CPUSetMems string
CPUShares int64

View File

@@ -24,12 +24,12 @@ type Config struct {
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Env []string // List of environment variable to set in the container
Cmd *strslice.StrSlice // Command to run when starting the container
Cmd strslice.StrSlice // Command to run when starting the container
ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
Volumes map[string]struct{} // List of volumes (mounts) used for the container
WorkingDir string // Current directory (PWD) in the command will be launched
Entrypoint *strslice.StrSlice // Entrypoint to run when starting the container
Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
NetworkDisabled bool `json:",omitempty"` // Is network disabled
MacAddress string `json:",omitempty"` // Mac Address of the container
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile

View File

@@ -12,13 +12,13 @@ import (
// NetworkMode represents the container network stack.
type NetworkMode string
// IsolationLevel represents the isolation level of a container. The supported
// Isolation represents the isolation technology of a container. The supported
// values are platform specific
type IsolationLevel string
type Isolation string
// IsDefault indicates the default isolation level of a container. On Linux this
// IsDefault indicates the default isolation technology of a container. On Linux this
// is the native driver. On Windows, this is a Windows Server Container.
func (i IsolationLevel) IsDefault() bool {
func (i Isolation) IsDefault() bool {
return strings.ToLower(string(i)) == "default" || string(i) == ""
}
@@ -65,6 +65,30 @@ func (n IpcMode) Container() string {
return ""
}
// UsernsMode represents userns mode in the container.
type UsernsMode string
// IsHost indicates whether the container uses the host's userns.
func (n UsernsMode) IsHost() bool {
return n == "host"
}
// IsPrivate indicates whether the container uses the a private userns.
func (n UsernsMode) IsPrivate() bool {
return !(n.IsHost())
}
// Valid indicates whether the userns is valid.
func (n UsernsMode) Valid() bool {
parts := strings.Split(string(n), ":")
switch mode := parts[0]; mode {
case "", "host":
default:
return false
}
return true
}
// UTSMode represents the UTS namespace of the container.
type UTSMode string
@@ -166,6 +190,7 @@ type LogConfig struct {
type Resources struct {
// Applicable to all platforms
CPUShares int64 `json:"CpuShares"` // CPU shares (relative weight vs. other containers)
Memory int64 // Memory limit (in bytes)
// Applicable to UNIX platforms
CgroupParent string // Parent cgroup.
@@ -180,14 +205,19 @@ type Resources struct {
CpusetCpus string // CpusetCpus 0-2, 0,1
CpusetMems string // CpusetMems 0-2, 0,1
Devices []DeviceMapping // List of devices to map inside the container
DiskQuota int64 // Disk limit (in bytes)
KernelMemory int64 // Kernel memory limit (in bytes)
Memory int64 // Memory limit (in bytes)
MemoryReservation int64 // Memory soft limit (in bytes)
MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap
MemorySwappiness *int64 // Tuning container memory swappiness behaviour
OomKillDisable *bool // Whether to disable OOM Killer or not
PidsLimit int64 // Setting pids limit for a container
Ulimits []*units.Ulimit // List of ulimits to be set in the container
// Applicable to Windows
BlkioIOps uint64 // Maximum IOps for the container system drive
BlkioBps uint64 // Maximum Bytes per second for the container system drive
SandboxSize uint64 // System drive will be expanded to at least this size (in bytes)
}
// UpdateConfig holds the mutable attributes of a Container.
@@ -213,28 +243,31 @@ type HostConfig struct {
VolumesFrom []string // List of volumes to take from other container
// Applicable to UNIX platforms
CapAdd *strslice.StrSlice // List of kernel capabilities to add to the container
CapDrop *strslice.StrSlice // List of kernel capabilities to remove from the container
DNS []string `json:"Dns"` // List of DNS server to lookup
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
ExtraHosts []string // List of extra hosts
GroupAdd []string // List of additional groups that the container process will run as
IpcMode IpcMode // IPC namespace to use for the container
Links []string // List of links (in the name:alias form)
OomScoreAdj int // Container preference for OOM-killing
PidMode PidMode // PID namespace to use for the container
Privileged bool // Is the container in privileged mode
PublishAllPorts bool // Should docker publish all exposed port for the container
ReadonlyRootfs bool // Is the container root filesystem in read-only
SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux.
Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container
UTSMode UTSMode // UTS namespace to use for the container
ShmSize int64 // Total shm memory usage
CapAdd strslice.StrSlice // List of kernel capabilities to add to the container
CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container
DNS []string `json:"Dns"` // List of DNS server to lookup
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
ExtraHosts []string // List of extra hosts
GroupAdd []string // List of additional groups that the container process will run as
IpcMode IpcMode // IPC namespace to use for the container
Links []string // List of links (in the name:alias form)
OomScoreAdj int // Container preference for OOM-killing
PidMode PidMode // PID namespace to use for the container
Privileged bool // Is the container in privileged mode
PublishAllPorts bool // Should docker publish all exposed port for the container
ReadonlyRootfs bool // Is the container root filesystem in read-only
SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux.
StorageOpt []string // Storage driver options per container.
Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container
UTSMode UTSMode // UTS namespace to use for the container
UsernsMode UsernsMode // The user namespace to use for the container
ShmSize int64 // Total shm memory usage
Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container
// Applicable to Windows
ConsoleSize [2]int // Initial console size
Isolation IsolationLevel // Isolation level of the container (eg default, hyperv)
ConsoleSize [2]int // Initial console size
Isolation Isolation // Isolation technology of the container (eg default, hyperv)
// Contains container's resources (cgroups, ulimits)
Resources

View File

@@ -4,8 +4,8 @@ package container
import "strings"
// IsValid indicates is an isolation level is valid
func (i IsolationLevel) IsValid() bool {
// IsValid indicates if an isolation technology is valid
func (i Isolation) IsValid() bool {
return i.IsDefault()
}
@@ -72,12 +72,6 @@ func (n NetworkMode) IsUserDefined() bool {
return !n.IsDefault() && !n.IsBridge() && !n.IsHost() && !n.IsNone() && !n.IsContainer()
}
// IsPreDefinedNetwork indicates if a network is predefined by the daemon
func IsPreDefinedNetwork(network string) bool {
n := NetworkMode(network)
return n.IsBridge() || n.IsHost() || n.IsNone()
}
//UserDefined indicates user-created network
func (n NetworkMode) UserDefined() string {
if n.IsUserDefined() {

View File

@@ -15,65 +15,74 @@ func (n NetworkMode) IsNone() bool {
return n == "none"
}
// IsContainer indicates whether container uses a container network stack.
// Returns false as windows doesn't support this mode
func (n NetworkMode) IsContainer() bool {
return false
}
// IsBridge indicates whether container uses the bridge network stack
// in windows it is given the name NAT
func (n NetworkMode) IsBridge() bool {
return n == "nat"
}
// IsHost indicates whether container uses the host network stack.
// returns false as this is not supported by windows
func (n NetworkMode) IsHost() bool {
return false
}
// IsPrivate indicates whether container uses it's private network stack.
func (n NetworkMode) IsPrivate() bool {
return !(n.IsHost() || n.IsContainer())
}
// ConnectedContainer is the id of the container which network this container is connected to.
// Returns blank string on windows
func (n NetworkMode) ConnectedContainer() string {
return ""
}
// IsUserDefined indicates user-created network
func (n NetworkMode) IsUserDefined() bool {
return !n.IsDefault() && !n.IsNone()
return !n.IsDefault() && !n.IsNone() && !n.IsBridge()
}
// IsHyperV indicates the use of a Hyper-V partition for isolation
func (i IsolationLevel) IsHyperV() bool {
func (i Isolation) IsHyperV() bool {
return strings.ToLower(string(i)) == "hyperv"
}
// IsProcess indicates the use of process isolation
func (i IsolationLevel) IsProcess() bool {
func (i Isolation) IsProcess() bool {
return strings.ToLower(string(i)) == "process"
}
// IsValid indicates is an isolation level is valid
func (i IsolationLevel) IsValid() bool {
// IsValid indicates if an isolation technology is valid
func (i Isolation) IsValid() bool {
return i.IsDefault() || i.IsHyperV() || i.IsProcess()
}
// DefaultDaemonNetworkMode returns the default network stack the daemon should
// use.
func DefaultDaemonNetworkMode() NetworkMode {
return NetworkMode("default")
}
// NetworkName returns the name of the network stack.
func (n NetworkMode) NetworkName() string {
if n.IsDefault() {
return "default"
} else if n.IsBridge() {
return "nat"
} else if n.IsNone() {
return "none"
} else if n.IsUserDefined() {
return n.UserDefined()
}
return ""
}
// IsPreDefinedNetwork indicates if a network is predefined by the daemon
func IsPreDefinedNetwork(network string) bool {
return false
}
// ValidateNetMode ensures that the various combinations of requested
// network settings are valid.
func ValidateNetMode(c *Config, hc *HostConfig) error {
// We may not be passed a host config, such as in the case of docker commit
if hc == nil {
return nil
}
parts := strings.Split(string(hc.NetworkMode), ":")
switch mode := parts[0]; mode {
case "default", "none":
default:
return fmt.Errorf("invalid --net: %s", hc.NetworkMode)
}
return nil
}
// ValidateIsolationLevel performs platform specific validation of the
// isolation level in the hostconfig structure. Windows supports 'default' (or
// ValidateIsolation performs platform specific validation of the
// isolation technology in the hostconfig structure. Windows supports 'default' (or
// blank), 'process', or 'hyperv'.
func ValidateIsolationLevel(hc *HostConfig) error {
func ValidateIsolation(hc *HostConfig) error {
// We may not be passed a host config, such as in the case of docker commit
if hc == nil {
return nil
@@ -83,3 +92,11 @@ func ValidateIsolationLevel(hc *HostConfig) error {
}
return nil
}
//UserDefined indicates user-created network
func (n NetworkMode) UserDefined() string {
if n.IsUserDefined() {
return string(n)
}
return ""
}

View File

@@ -1,41 +0,0 @@
package image
import (
"fmt"
)
// PullBehavior can be one of: never, always, or missing
type PullBehavior int
// PullBehavior can be one of: never, always, or missing
const (
PullNever PullBehavior = iota
PullAlways
PullMissing
)
// ParsePullBehavior validates and converts a string into a PullBehavior
func ParsePullBehavior(pullVal string) (PullBehavior, error) {
switch pullVal {
case "never":
return PullNever, nil
case "always":
return PullAlways, nil
case "missing", "":
return PullMissing, nil
}
return PullNever, fmt.Errorf("Invalid pull behavior '%s'", pullVal)
}
// String returns a string representation of a PullBehavior
func (p PullBehavior) String() string {
switch p {
case PullNever:
return "never"
case PullAlways:
return "always"
case PullMissing:
return "missing"
}
return ""
}

View File

@@ -1,29 +1,18 @@
package strslice
import (
"encoding/json"
"strings"
)
import "encoding/json"
// StrSlice represents a string or an array of strings.
// We need to override the json decoder to accept both options.
type StrSlice struct {
parts []string
}
type StrSlice []string
// MarshalJSON Marshals (or serializes) the StrSlice into the json format.
// This method is needed to implement json.Marshaller.
func (e *StrSlice) MarshalJSON() ([]byte, error) {
if e == nil {
return []byte{}, nil
}
return json.Marshal(e.Slice())
}
// UnmarshalJSON decodes the byte slice whether it's a string or an array of strings.
// This method is needed to implement json.Unmarshaler.
// UnmarshalJSON decodes the byte slice whether it's a string or an array of
// strings. This method is needed to implement json.Unmarshaler.
func (e *StrSlice) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
// With no input, we preserve the existing value by returning nil and
// leaving the target alone. This allows defining default values for
// the type.
return nil
}
@@ -36,36 +25,6 @@ func (e *StrSlice) UnmarshalJSON(b []byte) error {
p = append(p, s)
}
e.parts = p
*e = p
return nil
}
// Len returns the number of parts of the StrSlice.
func (e *StrSlice) Len() int {
if e == nil {
return 0
}
return len(e.parts)
}
// Slice gets the parts of the StrSlice as a Slice of string.
func (e *StrSlice) Slice() []string {
if e == nil {
return nil
}
return e.parts
}
// ToString gets space separated string of all the parts.
func (e *StrSlice) ToString() string {
s := e.Slice()
if s == nil {
return ""
}
return strings.Join(s, " ")
}
// New creates an StrSlice based on the specified parts (as strings).
func New(parts ...string) *StrSlice {
return &StrSlice{parts}
}

View File

@@ -148,6 +148,7 @@ type Container struct {
NetworkMode string `json:",omitempty"`
}
NetworkSettings *SummaryNetworkSettings
Mounts []MountPoint
}
// CopyConfig contains request body of Remote API:
@@ -203,6 +204,7 @@ type Info struct {
Plugins PluginsInfo
MemoryLimit bool
SwapLimit bool
KernelMemory bool
CPUCfsPeriod bool `json:"CpuCfsPeriod"`
CPUCfsQuota bool `json:"CpuCfsQuota"`
CPUShares bool
@@ -217,6 +219,7 @@ type Info struct {
SystemTime string
ExecutionDriver string
LoggingDriver string
CgroupDriver string
NEventsListener int
KernelVersion string
OperatingSystem string
@@ -238,8 +241,8 @@ type Info struct {
ClusterAdvertise string
}
// PluginsInfo is temp struct holds Plugins name
// registered with docker daemon. It used by Info struct
// PluginsInfo is a temp struct holding Plugins name
// registered with docker daemon. It is used by Info struct
type PluginsInfo struct {
// List of Volume plugins registered
Volume []string
@@ -387,6 +390,7 @@ type NetworkResource struct {
ID string `json:"Id"`
Scope string
Driver string
EnableIPv6 bool
IPAM network.IPAM
Internal bool
Containers map[string]EndpointResource
@@ -407,6 +411,7 @@ type NetworkCreate struct {
Name string
CheckDuplicate bool
Driver string
EnableIPv6 bool
IPAM network.IPAM
Internal bool
Options map[string]string

View File

@@ -85,7 +85,7 @@ func certPool(caFile string) (*x509.CertPool, error) {
func Client(options Options) (*tls.Config, error) {
tlsConfig := ClientDefault
tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify
if !options.InsecureSkipVerify {
if !options.InsecureSkipVerify && options.CAFile != "" {
CAs, err := certPool(options.CAFile)
if err != nil {
return nil, err
@@ -93,7 +93,7 @@ func Client(options Options) (*tls.Config, error) {
tlsConfig.RootCAs = CAs
}
if options.CertFile != "" && options.KeyFile != "" {
if options.CertFile != "" || options.KeyFile != "" {
tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile)
if err != nil {
return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err)

View File

@@ -31,7 +31,7 @@ type unitMap map[string]int64
var (
decimalMap = unitMap{"k": KB, "m": MB, "g": GB, "t": TB, "p": PB}
binaryMap = unitMap{"k": KiB, "m": MiB, "g": GiB, "t": TiB, "p": PiB}
sizeRegex = regexp.MustCompile(`^(\d+)([kKmMgGtTpP])?[bB]?$`)
sizeRegex = regexp.MustCompile(`^(\d+(\.\d+)*) ?([kKmMgGtTpP])?[bB]?$`)
)
var decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
@@ -77,19 +77,19 @@ func RAMInBytes(size string) (int64, error) {
// Parses the human-readable size string into the amount it represents.
func parseSize(sizeStr string, uMap unitMap) (int64, error) {
matches := sizeRegex.FindStringSubmatch(sizeStr)
if len(matches) != 3 {
if len(matches) != 4 {
return -1, fmt.Errorf("invalid size: '%s'", sizeStr)
}
size, err := strconv.ParseInt(matches[1], 10, 0)
size, err := strconv.ParseFloat(matches[1], 64)
if err != nil {
return -1, err
}
unitPrefix := strings.ToLower(matches[2])
unitPrefix := strings.ToLower(matches[3])
if mul, ok := uMap[unitPrefix]; ok {
size *= mul
size *= float64(mul)
}
return size, nil
return int64(size), nil
}

View File

@@ -1,8 +1,19 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
matrix:
include:
- go: 1.3
- go: 1.4
- go: 1.5
- go: 1.6
- go: tip
install:
- go get golang.org/x/tools/cmd/vet
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- go test -v -race ./...

View File

@@ -1,14 +1,20 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
matrix:
include:
- go: 1.2
- go: 1.3
- go: 1.4
- go: 1.5
- go: 1.6
- go: tip
install:
- go get golang.org/x/tools/cmd/vet
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- go test -v -race ./...

View File

@@ -3,6 +3,8 @@ mux
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
http://www.gorillatoolkit.org/pkg/mux
Package `gorilla/mux` implements a request router and dispatcher.
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:

View File

@@ -3,7 +3,7 @@
// license that can be found in the LICENSE file.
/*
Package gorilla/mux implements a request router and dispatcher.
Package mux implements a request router and dispatcher.
The name mux stands for "HTTP request multiplexer". Like the standard
http.ServeMux, mux.Router matches incoming requests against a list of

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