Merge component 'engine' from git@github.com:moby/moby master
This commit is contained in:
2
components/engine/.github/CODEOWNERS
vendored
2
components/engine/.github/CODEOWNERS
vendored
@ -14,6 +14,8 @@ daemon/graphdriver/windows/** @johnstep @jhowardmsft
|
||||
daemon/logger/awslogs/** @samuelkarp
|
||||
hack/** @dnephin @tianon
|
||||
hack/integration-cli-on-swarm/** @AkihiroSuda
|
||||
integration-cli/** @dnephin @vdemeester
|
||||
integration/** @dnephin @vdemeester
|
||||
pkg/testutil/** @dnephin
|
||||
plugin/** @cpuguy83
|
||||
project/** @thaJeztah
|
||||
|
||||
@ -2284,7 +2284,7 @@ by another client (#15489)
|
||||
- Use mock for search tests.
|
||||
- Update to double-dash everywhere.
|
||||
- Move .dockerenv parsing to lxc driver.
|
||||
- Move all bind-mounts in the container inside the namespace.
|
||||
- Move all bind mounts in the container inside the namespace.
|
||||
- Don't use separate bind mount for container.
|
||||
- Always symlink /dev/ptmx for libcontainer.
|
||||
- Don't kill by pid for other drivers.
|
||||
@ -2719,7 +2719,7 @@ With the ongoing changes to the networking and execution subsystems of docker te
|
||||
+ Implement `docker log -f` to stream logs
|
||||
+ Add env variable to disable kernel version warning
|
||||
+ Add -format to `docker inspect`
|
||||
+ Support bind-mount for files
|
||||
+ Support bind mount for files
|
||||
- Fix bridge creation on RHEL
|
||||
- Fix image size calculation
|
||||
- Make sure iptables are called even if the bridge already exists
|
||||
|
||||
@ -155,10 +155,7 @@ Fork the repository and make changes on your fork in a feature branch:
|
||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
||||
issue.
|
||||
|
||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||
it! Take a look at existing tests for inspiration. [Run the full test
|
||||
suite](https://docs.docker.com/opensource/project/test-and-docs/) on your branch before
|
||||
submitting a pull request.
|
||||
Submit tests for your changes. See [TESTING.md](./TESTING.md) for details.
|
||||
|
||||
If your changes need integration tests, write them against the API. The `cli`
|
||||
integration tests are slowly either migrated to API tests or moved away as unit
|
||||
@ -255,10 +252,9 @@ calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
After every commit, [make sure the test suite passes]
|
||||
(https://docs.docker.com/opensource/project/test-and-docs/). Include documentation
|
||||
changes in the same pull request so that a revert would remove all traces of
|
||||
the feature or fix.
|
||||
After every commit, [make sure the test suite passes](./TESTING.md). Include
|
||||
documentation changes in the same pull request so that a revert would remove
|
||||
all traces of the feature or fix.
|
||||
|
||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that
|
||||
close an issue. Including references automatically closes the issue on a merge.
|
||||
|
||||
@ -115,17 +115,6 @@ RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH /go
|
||||
|
||||
# Dependency for golint
|
||||
ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
|
||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
|
||||
|
||||
# Grab Go's lint tool
|
||||
ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
|
||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
||||
&& go install -v github.com/golang/lint/golint
|
||||
|
||||
# Install CRIU for checkpoint/restore support
|
||||
ENV CRIU_VERSION 2.12.1
|
||||
# Install dependancy packages specific to criu
|
||||
@ -215,7 +204,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
|
||||
# Please edit hack/dockerfile/install-binaries.sh to update them.
|
||||
COPY hack/dockerfile/binaries-commits /tmp/binaries-commits
|
||||
COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh
|
||||
RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli
|
||||
RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli gometalinter
|
||||
ENV PATH=/usr/local/cli:$PATH
|
||||
|
||||
# Activate bash completion and include Docker's completion if mounted with DOCKER_BASH_COMPLETION_PATH
|
||||
|
||||
@ -98,17 +98,6 @@ RUN mkdir /usr/src/go && curl -fsSL https://golang.org/dl/go${GO_VERSION}.src.ta
|
||||
ENV PATH /go/bin:/usr/src/go/bin:$PATH
|
||||
ENV GOPATH /go
|
||||
|
||||
# Dependency for golint
|
||||
ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
|
||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
|
||||
|
||||
# Grab Go's lint tool
|
||||
ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
|
||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
||||
&& go install -v github.com/golang/lint/golint
|
||||
|
||||
# Only install one version of the registry, because old version which support
|
||||
# schema1 manifests is not working on ARM64, we should skip integration-cli
|
||||
# tests for schema1 manifests on ARM64.
|
||||
|
||||
@ -81,17 +81,6 @@ ENV GOPATH /go
|
||||
ENV GOARCH arm
|
||||
ENV GOARM 7
|
||||
|
||||
# Dependency for golint
|
||||
ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
|
||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
|
||||
|
||||
# Grab Go's lint tool
|
||||
ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
|
||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
||||
&& go install -v github.com/golang/lint/golint
|
||||
|
||||
# Install seccomp: the version shipped upstream is too old
|
||||
ENV SECCOMP_VERSION 2.3.2
|
||||
RUN set -x \
|
||||
|
||||
@ -94,17 +94,6 @@ RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-ppc64le.tar.gz" \
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH /go
|
||||
|
||||
# Dependency for golint
|
||||
ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
|
||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
|
||||
|
||||
# Grab Go's lint tool
|
||||
ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
|
||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
||||
&& go install -v github.com/golang/lint/golint
|
||||
|
||||
# Install two 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
|
||||
|
||||
@ -87,17 +87,6 @@ RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-s390x.tar.gz" \
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH /go
|
||||
|
||||
# Dependency for golint
|
||||
ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3
|
||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT)
|
||||
|
||||
# Grab Go's lint tool
|
||||
ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456
|
||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
||||
&& go install -v github.com/golang/lint/golint
|
||||
|
||||
# Install two 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
|
||||
|
||||
@ -132,8 +132,8 @@
|
||||
# Important notes:
|
||||
# ---------------
|
||||
#
|
||||
# Don't attempt to use a bind-mount to pass a local directory as the bundles target
|
||||
# directory. It does not work (golang attempts for follow a mapped folder incorrectly).
|
||||
# Don't attempt to use a bind mount to pass a local directory as the bundles target
|
||||
# directory. It does not work (golang attempts for follow a mapped folder incorrectly).
|
||||
# Instead, use docker cp as per the example.
|
||||
#
|
||||
# go.zip is not removed from the image as it is used by the Windows CI servers
|
||||
|
||||
@ -110,7 +110,7 @@ dynbinary: build ## build the linux dynbinaries
|
||||
$(DOCKER_RUN_DOCKER) hack/make.sh dynbinary
|
||||
|
||||
build: bundles init-go-pkg-cache
|
||||
$(warning The docker client CLI has moved to github.com/docker/cli. By default, it is built from the git sha specified in hack/dockerfile/binaries-commits. For a dev-test cycle involving the CLI, run:${\n} DOCKER_CLI_PATH=/host/path/to/cli/binary make shell ${\n} then change the cli and compile into a binary at the same location.${\n})
|
||||
$(warning The docker client CLI has moved to github.com/docker/cli. For a dev-test cycle involving the CLI, run:${\n} DOCKER_CLI_PATH=/host/path/to/cli/binary make shell ${\n} then change the cli and compile into a binary at the same location.${\n})
|
||||
docker build ${BUILD_APT_MIRROR} ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" .
|
||||
|
||||
bundles:
|
||||
@ -160,7 +160,7 @@ test-integration: build ## run the integration tests
|
||||
$(DOCKER_RUN_DOCKER) hack/make.sh dynbinary test-integration
|
||||
|
||||
test-unit: build ## run the unit tests
|
||||
$(DOCKER_RUN_DOCKER) hack/make.sh test-unit
|
||||
$(DOCKER_RUN_DOCKER) hack/test/unit
|
||||
|
||||
tgz: build ## build the archives (.zip on windows and .tgz\notherwise) containing the binaries
|
||||
$(DOCKER_RUN_DOCKER) hack/make.sh dynbinary binary cross tgz
|
||||
|
||||
71
components/engine/TESTING.md
Normal file
71
components/engine/TESTING.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Testing
|
||||
|
||||
This document contains the Moby code testing guidelines. It should answer any
|
||||
questions you may have as an aspiring Moby contributor.
|
||||
|
||||
## Test suites
|
||||
|
||||
Moby has two test suites (and one legacy test suite):
|
||||
|
||||
* Unit tests - use standard `go test` and
|
||||
[testify](https://github.com/stretchr/testify) assertions. They are located in
|
||||
the package they test. Unit tests should be fast and test only their own
|
||||
package.
|
||||
* API integration tests - use standard `go test` and
|
||||
[testify](https://github.com/stretchr/testify) assertions. They are located in
|
||||
`./integration/<component>` directories, where `component` is: container,
|
||||
image, volume, etc. These tests perform HTTP requests to an API endpoint and
|
||||
check the HTTP response and daemon state after the call.
|
||||
|
||||
The legacy test suite `integration-cli/` is deprecated. No new tests will be
|
||||
added to this suite. Any tests in this suite which require updates should be
|
||||
ported to either the unit test suite or the new API integration test suite.
|
||||
|
||||
## Writing new tests
|
||||
|
||||
Most code changes will fall into one of the following categories.
|
||||
|
||||
### Writing tests for new features
|
||||
|
||||
New code should be covered by unit tests. If the code is difficult to test with
|
||||
a unit tests then that is a good sign that it should be refactored to make it
|
||||
easier to reuse and maintain. Consider accepting unexported interfaces instead
|
||||
of structs so that fakes can be provided for dependencies.
|
||||
|
||||
If the new feature includes a completely new API endpoint then a new API
|
||||
integration test should be added to cover the success case of that endpoint.
|
||||
|
||||
If the new feature does not include a completely new API endpoint consider
|
||||
adding the new API fields to the existing test for that endpoint. A new
|
||||
integration test should **not** be added for every new API field or API error
|
||||
case. Error cases should be handled by unit tests.
|
||||
|
||||
### Writing tests for bug fixes
|
||||
|
||||
Bugs fixes should include a unit test case which exercises the bug.
|
||||
|
||||
A bug fix may also include new assertions in an existing integration tests for the
|
||||
API endpoint.
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the unit test suite:
|
||||
|
||||
```
|
||||
make test-unit
|
||||
```
|
||||
|
||||
or `hack/test/unit` from inside a `BINDDIR=. make shell` container or properly
|
||||
configured environment.
|
||||
|
||||
The following environment variables may be used to run a subset of tests:
|
||||
|
||||
* `TESTDIRS` - paths to directories to be tested, defaults to `./...`
|
||||
* `TESTFLAGS` - flags passed to `go test`, to run tests which match a pattern
|
||||
use `TESTFLAGS="-test.run TestNameOrPrefix"`
|
||||
|
||||
To run the integration test suite:
|
||||
|
||||
```
|
||||
make test-integration
|
||||
```
|
||||
54
components/engine/api/errdefs/defs.go
Normal file
54
components/engine/api/errdefs/defs.go
Normal file
@ -0,0 +1,54 @@
|
||||
package errdefs
|
||||
|
||||
// ErrNotFound signals that the requested object doesn't exist
|
||||
type ErrNotFound interface {
|
||||
NotFound()
|
||||
}
|
||||
|
||||
// ErrInvalidParameter signals that the user input is invalid
|
||||
type ErrInvalidParameter interface {
|
||||
InvalidParameter()
|
||||
}
|
||||
|
||||
// ErrConflict signals that some internal state conflicts with the requested action and can't be performed.
|
||||
// A change in state should be able to clear this error.
|
||||
type ErrConflict interface {
|
||||
Conflict()
|
||||
}
|
||||
|
||||
// ErrUnauthorized is used to signify that the user is not authorized to perform a specific action
|
||||
type ErrUnauthorized interface {
|
||||
Unauthorized()
|
||||
}
|
||||
|
||||
// ErrUnavailable signals that the requested action/subsystem is not available.
|
||||
type ErrUnavailable interface {
|
||||
Unavailable()
|
||||
}
|
||||
|
||||
// ErrForbidden signals that the requested action cannot be performed under any circumstances.
|
||||
// When a ErrForbidden is returned, the caller should never retry the action.
|
||||
type ErrForbidden interface {
|
||||
Forbidden()
|
||||
}
|
||||
|
||||
// ErrSystem signals that some internal error occurred.
|
||||
// An example of this would be a failed mount request.
|
||||
type ErrSystem interface {
|
||||
ErrSystem()
|
||||
}
|
||||
|
||||
// ErrNotModified signals that an action can't be performed because it's already in the desired state
|
||||
type ErrNotModified interface {
|
||||
NotModified()
|
||||
}
|
||||
|
||||
// ErrNotImplemented signals that the requested action/feature is not implemented on the system as configured.
|
||||
type ErrNotImplemented interface {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// ErrUnknown signals that the kind of error that occurred is not known.
|
||||
type ErrUnknown interface {
|
||||
Unknown()
|
||||
}
|
||||
8
components/engine/api/errdefs/doc.go
Normal file
8
components/engine/api/errdefs/doc.go
Normal file
@ -0,0 +1,8 @@
|
||||
// Package errdefs defines a set of error interfaces that packages should use for communicating classes of errors.
|
||||
// Errors that cross the package boundary should implement one (and only one) of these interfaces.
|
||||
//
|
||||
// Packages should not reference these interfaces directly, only implement them.
|
||||
// To check if a particular error implements one of these interfaces, there are helper
|
||||
// functions provided (e.g. `Is<SomeError>`) which can be used rather than asserting the interfaces directly.
|
||||
// If you must assert on these interfaces, be sure to check the causal chain (`err.Cause()`).
|
||||
package errdefs
|
||||
86
components/engine/api/errdefs/is.go
Normal file
86
components/engine/api/errdefs/is.go
Normal file
@ -0,0 +1,86 @@
|
||||
package errdefs
|
||||
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
func getImplementer(err error) error {
|
||||
switch e := err.(type) {
|
||||
case
|
||||
ErrNotFound,
|
||||
ErrInvalidParameter,
|
||||
ErrConflict,
|
||||
ErrUnauthorized,
|
||||
ErrUnavailable,
|
||||
ErrForbidden,
|
||||
ErrSystem,
|
||||
ErrNotModified,
|
||||
ErrNotImplemented,
|
||||
ErrUnknown:
|
||||
return e
|
||||
case causer:
|
||||
return getImplementer(e.Cause())
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotFound returns if the passed in error is a ErrNotFound
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsInvalidParameter returns if the passed in error is an ErrInvalidParameter
|
||||
func IsInvalidParameter(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrInvalidParameter)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsConflict returns if the passed in error is a ErrConflict
|
||||
func IsConflict(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrConflict)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsUnauthorized returns if the the passed in error is an ErrUnauthorized
|
||||
func IsUnauthorized(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrUnauthorized)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsUnavailable returns if the passed in error is an ErrUnavailable
|
||||
func IsUnavailable(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrUnavailable)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsForbidden returns if the passed in error is a ErrForbidden
|
||||
func IsForbidden(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrForbidden)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsSystem returns if the passed in error is a ErrSystem
|
||||
func IsSystem(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrSystem)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsNotModified returns if the passed in error is a NotModified error
|
||||
func IsNotModified(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrNotModified)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsNotImplemented returns if the passed in error is a ErrNotImplemented
|
||||
func IsNotImplemented(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrNotImplemented)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsUnknown returns if the passed in error is an ErrUnknown
|
||||
func IsUnknown(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrUnknown)
|
||||
return ok
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
// apiError is an error wrapper that also
|
||||
// holds information about response status codes.
|
||||
type apiError struct {
|
||||
error
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// HTTPErrorStatusCode returns a status code.
|
||||
func (e apiError) HTTPErrorStatusCode() int {
|
||||
return e.statusCode
|
||||
}
|
||||
|
||||
// NewErrorWithStatusCode allows you to associate
|
||||
// a specific HTTP Status Code to an error.
|
||||
// The server will take that code and set
|
||||
// it as the response status.
|
||||
func NewErrorWithStatusCode(err error, code int) error {
|
||||
return apiError{err, code}
|
||||
}
|
||||
|
||||
// NewBadRequestError creates a new API error
|
||||
// that has the 400 HTTP status code associated to it.
|
||||
func NewBadRequestError(err error) error {
|
||||
return NewErrorWithStatusCode(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// NewRequestForbiddenError creates a new API error
|
||||
// that has the 403 HTTP status code associated to it.
|
||||
func NewRequestForbiddenError(err error) error {
|
||||
return NewErrorWithStatusCode(err, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// NewRequestNotFoundError creates a new API error
|
||||
// that has the 404 HTTP status code associated to it.
|
||||
func NewRequestNotFoundError(err error) error {
|
||||
return NewErrorWithStatusCode(err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// NewRequestConflictError creates a new API error
|
||||
// that has the 409 HTTP status code associated to it.
|
||||
func NewRequestConflictError(err error) error {
|
||||
return NewErrorWithStatusCode(err, http.StatusConflict)
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newError(errorname string) error {
|
||||
|
||||
return fmt.Errorf("test%v", errorname)
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
errmsg := newError("apiError")
|
||||
err := apiError{
|
||||
error: errmsg,
|
||||
statusCode: 0,
|
||||
}
|
||||
assert.Equal(t, err.HTTPErrorStatusCode(), err.statusCode)
|
||||
|
||||
errmsg = newError("ErrorWithStatusCode")
|
||||
errcode := 1
|
||||
serr := NewErrorWithStatusCode(errmsg, errcode)
|
||||
apierr, ok := serr.(apiError)
|
||||
if !ok {
|
||||
t.Fatal("excepted err is apiError type")
|
||||
}
|
||||
assert.Equal(t, errcode, apierr.statusCode)
|
||||
|
||||
errmsg = newError("NewBadRequestError")
|
||||
baderr := NewBadRequestError(errmsg)
|
||||
apierr, ok = baderr.(apiError)
|
||||
if !ok {
|
||||
t.Fatal("excepted err is apiError type")
|
||||
}
|
||||
assert.Equal(t, http.StatusBadRequest, apierr.statusCode)
|
||||
|
||||
errmsg = newError("RequestForbiddenError")
|
||||
ferr := NewRequestForbiddenError(errmsg)
|
||||
apierr, ok = ferr.(apiError)
|
||||
if !ok {
|
||||
t.Fatal("excepted err is apiError type")
|
||||
}
|
||||
assert.Equal(t, http.StatusForbidden, apierr.statusCode)
|
||||
|
||||
errmsg = newError("RequestNotFoundError")
|
||||
nerr := NewRequestNotFoundError(errmsg)
|
||||
apierr, ok = nerr.(apiError)
|
||||
if !ok {
|
||||
t.Fatal("excepted err is apiError type")
|
||||
}
|
||||
assert.Equal(t, http.StatusNotFound, apierr.statusCode)
|
||||
|
||||
errmsg = newError("RequestConflictError")
|
||||
cerr := NewRequestConflictError(errmsg)
|
||||
apierr, ok = cerr.(apiError)
|
||||
if !ok {
|
||||
t.Fatal("excepted err is apiError type")
|
||||
}
|
||||
assert.Equal(t, http.StatusConflict, apierr.statusCode)
|
||||
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
package httputils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/errdefs"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/gorilla/mux"
|
||||
@ -12,21 +13,8 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// httpStatusError is an interface
|
||||
// that errors with custom status codes
|
||||
// implement to tell the api layer
|
||||
// which response status to set.
|
||||
type httpStatusError interface {
|
||||
HTTPErrorStatusCode() int
|
||||
}
|
||||
|
||||
// inputValidationError is an interface
|
||||
// that errors generated by invalid
|
||||
// inputs can implement to tell the
|
||||
// api layer to set a 400 status code
|
||||
// in the response.
|
||||
type inputValidationError interface {
|
||||
IsValidationError() bool
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
// GetHTTPErrorStatusCode retrieves status code from error message.
|
||||
@ -37,49 +25,44 @@ func GetHTTPErrorStatusCode(err error) int {
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
errMsg := err.Error()
|
||||
|
||||
switch e := err.(type) {
|
||||
case httpStatusError:
|
||||
statusCode = e.HTTPErrorStatusCode()
|
||||
case inputValidationError:
|
||||
// Stop right there
|
||||
// Are you sure you should be adding a new error class here? Do one of the existing ones work?
|
||||
|
||||
// Note that the below functions are already checking the error causal chain for matches.
|
||||
switch {
|
||||
case errdefs.IsNotFound(err):
|
||||
statusCode = http.StatusNotFound
|
||||
case errdefs.IsInvalidParameter(err):
|
||||
statusCode = http.StatusBadRequest
|
||||
case errdefs.IsConflict(err):
|
||||
statusCode = http.StatusConflict
|
||||
case errdefs.IsUnauthorized(err):
|
||||
statusCode = http.StatusUnauthorized
|
||||
case errdefs.IsUnavailable(err):
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
case errdefs.IsForbidden(err):
|
||||
statusCode = http.StatusForbidden
|
||||
case errdefs.IsNotModified(err):
|
||||
statusCode = http.StatusNotModified
|
||||
case errdefs.IsNotImplemented(err):
|
||||
statusCode = http.StatusNotImplemented
|
||||
case errdefs.IsSystem(err) || errdefs.IsUnknown(err):
|
||||
statusCode = http.StatusInternalServerError
|
||||
default:
|
||||
statusCode = statusCodeFromGRPCError(err)
|
||||
if statusCode != http.StatusInternalServerError {
|
||||
return statusCode
|
||||
}
|
||||
|
||||
// FIXME: this is brittle and should not be necessary, but we still need to identify if
|
||||
// there are errors falling back into this logic.
|
||||
// If we need to differentiate between different possible error types,
|
||||
// we should create appropriate error types that implement the httpStatusError interface.
|
||||
errStr := strings.ToLower(errMsg)
|
||||
|
||||
for _, status := range []struct {
|
||||
keyword string
|
||||
code int
|
||||
}{
|
||||
{"not found", http.StatusNotFound},
|
||||
{"cannot find", http.StatusNotFound},
|
||||
{"no such", http.StatusNotFound},
|
||||
{"bad parameter", http.StatusBadRequest},
|
||||
{"no command", http.StatusBadRequest},
|
||||
{"conflict", http.StatusConflict},
|
||||
{"impossible", http.StatusNotAcceptable},
|
||||
{"wrong login/password", http.StatusUnauthorized},
|
||||
{"unauthorized", http.StatusUnauthorized},
|
||||
{"hasn't been activated", http.StatusForbidden},
|
||||
{"this node", http.StatusServiceUnavailable},
|
||||
{"needs to be unlocked", http.StatusServiceUnavailable},
|
||||
{"certificates have expired", http.StatusServiceUnavailable},
|
||||
{"repository does not exist", http.StatusNotFound},
|
||||
} {
|
||||
if strings.Contains(errStr, status.keyword) {
|
||||
statusCode = status.code
|
||||
break
|
||||
}
|
||||
if e, ok := err.(causer); ok {
|
||||
return GetHTTPErrorStatusCode(e.Cause())
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"module": "api",
|
||||
"error_type": fmt.Sprintf("%T", err),
|
||||
}).Debugf("FIXME: Got an API for which error does not match any expected type!!!: %+v", err)
|
||||
}
|
||||
|
||||
if statusCode == 0 {
|
||||
@ -133,6 +116,9 @@ func statusCodeFromGRPCError(err error) int {
|
||||
case codes.Unavailable: // code 14
|
||||
return http.StatusServiceUnavailable
|
||||
default:
|
||||
if e, ok := err.(causer); ok {
|
||||
return statusCodeFromGRPCError(e.Cause())
|
||||
}
|
||||
// codes.Canceled(1)
|
||||
// codes.Unknown(2)
|
||||
// codes.DeadlineExceeded(4)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package httputils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -49,6 +48,16 @@ type ArchiveOptions struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
type badParameterError struct {
|
||||
param string
|
||||
}
|
||||
|
||||
func (e badParameterError) Error() string {
|
||||
return "bad parameter: " + e.param + "cannot be empty"
|
||||
}
|
||||
|
||||
func (e badParameterError) InvalidParameter() {}
|
||||
|
||||
// ArchiveFormValues parses form values and turns them into ArchiveOptions.
|
||||
// It fails if the archive name and path are not in the request.
|
||||
func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, error) {
|
||||
@ -57,14 +66,13 @@ func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions,
|
||||
}
|
||||
|
||||
name := vars["name"]
|
||||
path := filepath.FromSlash(r.Form.Get("path"))
|
||||
|
||||
switch {
|
||||
case name == "":
|
||||
return ArchiveOptions{}, errors.New("bad parameter: 'name' cannot be empty")
|
||||
case path == "":
|
||||
return ArchiveOptions{}, errors.New("bad parameter: 'path' cannot be empty")
|
||||
if name == "" {
|
||||
return ArchiveOptions{}, badParameterError{"name"}
|
||||
}
|
||||
|
||||
path := filepath.FromSlash(r.Form.Get("path"))
|
||||
if path == "" {
|
||||
return ArchiveOptions{}, badParameterError{"path"}
|
||||
}
|
||||
return ArchiveOptions{name, path}, nil
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package httputils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -43,6 +43,20 @@ func CloseStreams(streams ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
// CheckForJSON makes sure that the request's Content-Type is application/json.
|
||||
func CheckForJSON(r *http.Request) error {
|
||||
ct := r.Header.Get("Content-Type")
|
||||
@ -58,7 +72,7 @@ func CheckForJSON(r *http.Request) error {
|
||||
if matchesContentType(ct, "application/json") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct)
|
||||
return validationError{errors.Errorf("Content-Type specified (%s) must be 'application/json'", ct)}
|
||||
}
|
||||
|
||||
// ParseForm ensures the request form is parsed even with invalid content types.
|
||||
@ -68,7 +82,7 @@ func ParseForm(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -28,6 +27,16 @@ func NewVersionMiddleware(s, d, m string) VersionMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
type versionUnsupportedError struct {
|
||||
version, minVersion string
|
||||
}
|
||||
|
||||
func (e versionUnsupportedError) Error() string {
|
||||
return fmt.Sprintf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", e.version, e.minVersion)
|
||||
}
|
||||
|
||||
func (e versionUnsupportedError) InvalidParameter() {}
|
||||
|
||||
// WrapHandler returns a new handler function wrapping the previous one in the request chain.
|
||||
func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
@ -37,13 +46,14 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.
|
||||
}
|
||||
|
||||
if versions.LessThan(apiVersion, v.minVersion) {
|
||||
return errors.NewBadRequestError(fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, v.minVersion))
|
||||
return versionUnsupportedError{apiVersion, v.minVersion}
|
||||
}
|
||||
|
||||
header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS)
|
||||
w.Header().Set("Server", header)
|
||||
w.Header().Set("API-Version", v.defaultVersion)
|
||||
w.Header().Set("OSType", runtime.GOOS)
|
||||
// nolint: golint
|
||||
ctx = context.WithValue(ctx, "api-version", apiVersion)
|
||||
return handler(ctx, w, r, vars)
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
@ -27,6 +26,14 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type invalidIsolationError string
|
||||
|
||||
func (e invalidIsolationError) Error() string {
|
||||
return fmt.Sprintf("Unsupported isolation: %q", string(e))
|
||||
}
|
||||
|
||||
func (e invalidIsolationError) InvalidParameter() {}
|
||||
|
||||
func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) {
|
||||
version := httputils.VersionFromContext(ctx)
|
||||
options := &types.ImageBuildOptions{}
|
||||
@ -71,20 +78,20 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
|
||||
if i := container.Isolation(r.FormValue("isolation")); i != "" {
|
||||
if !container.Isolation.IsValid(i) {
|
||||
return nil, fmt.Errorf("Unsupported isolation: %q", i)
|
||||
return nil, invalidIsolationError(i)
|
||||
}
|
||||
options.Isolation = i
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && options.SecurityOpt != nil {
|
||||
return nil, fmt.Errorf("The daemon on this platform does not support setting security options on build")
|
||||
return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")}
|
||||
}
|
||||
|
||||
var buildUlimits = []*units.Ulimit{}
|
||||
ulimitsJSON := r.FormValue("ulimits")
|
||||
if ulimitsJSON != "" {
|
||||
if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(validationError{err}, "error reading ulimit settings")
|
||||
}
|
||||
options.Ulimits = buildUlimits
|
||||
}
|
||||
@ -105,7 +112,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
if buildArgsJSON != "" {
|
||||
var buildArgs = map[string]*string{}
|
||||
if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(validationError{err}, "error reading build args")
|
||||
}
|
||||
options.BuildArgs = buildArgs
|
||||
}
|
||||
@ -114,7 +121,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
if labelsJSON != "" {
|
||||
var labels = map[string]string{}
|
||||
if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(validationError{err}, "error reading labels")
|
||||
}
|
||||
options.Labels = labels
|
||||
}
|
||||
@ -140,6 +147,16 @@ func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *
|
||||
return httputils.WriteJSON(w, http.StatusOK, report)
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var (
|
||||
notVerboseBuffer = bytes.NewBuffer(nil)
|
||||
@ -173,8 +190,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
|
||||
buildOptions.AuthConfigs = getAuthConfigs(r.Header)
|
||||
|
||||
if buildOptions.Squash && !br.daemon.HasExperimental() {
|
||||
return apierrors.NewBadRequestError(
|
||||
errors.New("squash is only supported with experimental mode"))
|
||||
return validationError{errors.New("squash is only supported with experimental mode")}
|
||||
}
|
||||
|
||||
out := io.Writer(output)
|
||||
|
||||
@ -51,7 +51,7 @@ type stateBackend interface {
|
||||
type monitorBackend interface {
|
||||
ContainerChanges(name string) ([]archive.Change, error)
|
||||
ContainerInspect(name string, size bool, version string) (interface{}, error)
|
||||
ContainerLogs(ctx context.Context, name string, config *types.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
|
||||
ContainerLogs(ctx context.Context, name string, config *types.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error)
|
||||
ContainerStats(ctx context.Context, name string, config *backend.ContainerStatsConfig) error
|
||||
ContainerTop(name string, psArgs string) (*container.ContainerTopOKBody, error)
|
||||
|
||||
|
||||
@ -6,13 +6,19 @@ import (
|
||||
)
|
||||
|
||||
type validationError struct {
|
||||
error
|
||||
cause error
|
||||
}
|
||||
|
||||
func (validationError) IsValidationError() bool {
|
||||
return true
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
// containerRouter is a router to talk with the container controller
|
||||
type containerRouter struct {
|
||||
backend Backend
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/errdefs"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
@ -18,6 +18,7 @@ import (
|
||||
containerpkg "github.com/docker/docker/container"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/websocket"
|
||||
@ -87,7 +88,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
|
||||
// with the appropriate status code.
|
||||
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
|
||||
if !(stdout || stderr) {
|
||||
return fmt.Errorf("Bad parameters: you must choose at least one stream")
|
||||
return validationError{errors.New("Bad parameters: you must choose at least one stream")}
|
||||
}
|
||||
|
||||
containerName := vars["name"]
|
||||
@ -101,19 +102,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
|
||||
Details: httputils.BoolValue(r, "details"),
|
||||
}
|
||||
|
||||
// doesn't matter what version the client is on, we're using this internally only
|
||||
// also do we need size? i'm thinking no we don't
|
||||
raw, err := s.backend.ContainerInspect(containerName, false, api.DefaultVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
container, ok := raw.(*types.ContainerJSON)
|
||||
if !ok {
|
||||
// %T prints the type. handy!
|
||||
return fmt.Errorf("expected container to be *types.ContainerJSON but got %T", raw)
|
||||
}
|
||||
|
||||
msgs, err := s.backend.ContainerLogs(ctx, containerName, logsConfig)
|
||||
msgs, tty, err := s.backend.ContainerLogs(ctx, containerName, logsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -122,7 +111,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
|
||||
// this is the point of no return for writing a response. once we call
|
||||
// WriteLogStream, the response has been started and errors will be
|
||||
// returned in band by WriteLogStream
|
||||
httputils.WriteLogStream(ctx, w, msgs, logsConfig, !container.Config.Tty)
|
||||
httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -130,6 +119,14 @@ func (s *containerRouter) getContainersExport(ctx context.Context, w http.Respon
|
||||
return s.backend.ContainerExport(vars["name"], w)
|
||||
}
|
||||
|
||||
type bodyOnStartError struct{}
|
||||
|
||||
func (bodyOnStartError) Error() string {
|
||||
return "starting container with non-empty request body was deprecated since API v1.22 and removed in v1.24"
|
||||
}
|
||||
|
||||
func (bodyOnStartError) InvalidParameter() {}
|
||||
|
||||
func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// If contentLength is -1, we can assumed chunked encoding
|
||||
// or more technically that the length is unknown
|
||||
@ -143,7 +140,7 @@ func (s *containerRouter) postContainersStart(ctx context.Context, w http.Respon
|
||||
// A non-nil json object is at least 7 characters.
|
||||
if r.ContentLength > 7 || r.ContentLength == -1 {
|
||||
if versions.GreaterThanOrEqualTo(version, "1.24") {
|
||||
return validationError{fmt.Errorf("starting container with non-empty request body was deprecated since v1.10 and removed in v1.12")}
|
||||
return bodyOnStartError{}
|
||||
}
|
||||
|
||||
if err := httputils.CheckForJSON(r); err != nil {
|
||||
@ -193,10 +190,6 @@ func (s *containerRouter) postContainersStop(ctx context.Context, w http.Respons
|
||||
return nil
|
||||
}
|
||||
|
||||
type errContainerIsRunning interface {
|
||||
ContainerIsRunning() bool
|
||||
}
|
||||
|
||||
func (s *containerRouter) postContainersKill(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
@ -209,14 +202,14 @@ func (s *containerRouter) postContainersKill(ctx context.Context, w http.Respons
|
||||
if sigStr := r.Form.Get("signal"); sigStr != "" {
|
||||
var err error
|
||||
if sig, err = signal.ParseSignal(sigStr); err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.backend.ContainerKill(name, uint64(sig)); err != nil {
|
||||
var isStopped bool
|
||||
if e, ok := err.(errContainerIsRunning); ok {
|
||||
isStopped = !e.ContainerIsRunning()
|
||||
if errdefs.IsConflict(err) {
|
||||
isStopped = true
|
||||
}
|
||||
|
||||
// Return error that's not caused because the container is stopped.
|
||||
@ -224,7 +217,7 @@ func (s *containerRouter) postContainersKill(ctx context.Context, w http.Respons
|
||||
// to keep backwards compatibility.
|
||||
version := httputils.VersionFromContext(ctx)
|
||||
if versions.GreaterThanOrEqualTo(version, "1.20") || !isStopped {
|
||||
return fmt.Errorf("Cannot kill container %s: %v", name, err)
|
||||
return errors.Wrapf(err, "Cannot kill container: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,11 +451,11 @@ func (s *containerRouter) postContainersResize(ctx context.Context, w http.Respo
|
||||
|
||||
height, err := strconv.Atoi(r.Form.Get("h"))
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
width, err := strconv.Atoi(r.Form.Get("w"))
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
return s.backend.ContainerResize(vars["name"], height, width)
|
||||
@ -480,7 +473,7 @@ func (s *containerRouter) postContainersAttach(ctx context.Context, w http.Respo
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
return fmt.Errorf("error attaching to container %s, hijack connection missing", containerName)
|
||||
return validationError{errors.Errorf("error attaching to container %s, hijack connection missing", containerName)}
|
||||
}
|
||||
|
||||
setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) {
|
||||
@ -597,7 +590,7 @@ func (s *containerRouter) postContainersPrune(ctx context.Context, w http.Respon
|
||||
|
||||
pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters)
|
||||
|
||||
@ -3,11 +3,8 @@ package container
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -15,6 +12,14 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type pathError struct{}
|
||||
|
||||
func (pathError) Error() string {
|
||||
return "Path cannot be empty"
|
||||
}
|
||||
|
||||
func (pathError) InvalidParameter() {}
|
||||
|
||||
// postContainersCopy is deprecated in favor of getContainersArchive.
|
||||
func (s *containerRouter) postContainersCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// Deprecated since 1.8, Errors out since 1.12
|
||||
@ -33,18 +38,11 @@ func (s *containerRouter) postContainersCopy(ctx context.Context, w http.Respons
|
||||
}
|
||||
|
||||
if cfg.Resource == "" {
|
||||
return fmt.Errorf("Path cannot be empty")
|
||||
return pathError{}
|
||||
}
|
||||
|
||||
data, err := s.backend.ContainerCopy(vars["name"], cfg.Resource)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "no such container") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("Could not find the file %s in container %s", cfg.Resource, vars["name"])
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer data.Close()
|
||||
|
||||
@ -24,6 +24,14 @@ func (s *containerRouter) getExecByID(ctx context.Context, w http.ResponseWriter
|
||||
return httputils.WriteJSON(w, http.StatusOK, eConfig)
|
||||
}
|
||||
|
||||
type execCommandError struct{}
|
||||
|
||||
func (execCommandError) Error() string {
|
||||
return "No exec command specified"
|
||||
}
|
||||
|
||||
func (execCommandError) InvalidParameter() {}
|
||||
|
||||
func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
@ -39,7 +47,7 @@ func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.Re
|
||||
}
|
||||
|
||||
if len(execConfig.Cmd) == 0 {
|
||||
return fmt.Errorf("No exec command specified")
|
||||
return execCommandError{}
|
||||
}
|
||||
|
||||
// Register an instance of Exec in container.
|
||||
@ -129,11 +137,11 @@ func (s *containerRouter) postContainerExecResize(ctx context.Context, w http.Re
|
||||
}
|
||||
height, err := strconv.Atoi(r.Form.Get("h"))
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
width, err := strconv.Atoi(r.Form.Get("w"))
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
return s.backend.ContainerExecResize(vars["name"], height, width)
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
)
|
||||
|
||||
var (
|
||||
errExperimentalFeature = errors.New("This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it.")
|
||||
)
|
||||
|
||||
// ExperimentalRoute defines an experimental API route that can be enabled or disabled.
|
||||
type ExperimentalRoute interface {
|
||||
Route
|
||||
@ -39,8 +33,16 @@ func (r *experimentalRoute) Disable() {
|
||||
r.handler = experimentalHandler
|
||||
}
|
||||
|
||||
type notImplementedError struct{}
|
||||
|
||||
func (notImplementedError) Error() string {
|
||||
return "This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it."
|
||||
}
|
||||
|
||||
func (notImplementedError) NotImplemented() {}
|
||||
|
||||
func experimentalHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
return apierrors.NewErrorWithStatusCode(errExperimentalFeature, http.StatusNotImplemented)
|
||||
return notImplementedError{}
|
||||
}
|
||||
|
||||
// Handler returns returns the APIFunc to let the server wrap it in middlewares.
|
||||
|
||||
@ -3,7 +3,6 @@ package image
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@ -20,6 +19,7 @@ import (
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -161,6 +161,20 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
|
||||
return nil
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (validationError) InvalidParameter() {}
|
||||
|
||||
func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
metaHeaders := map[string][]string{}
|
||||
for k, v := range r.Header {
|
||||
@ -184,7 +198,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
|
||||
} else {
|
||||
// the old format is supported for compatibility if there was no authConfig header
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err)
|
||||
return errors.Wrap(validationError{err}, "Bad parameters and missing X-Registry-Auth")
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,6 +260,14 @@ func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter,
|
||||
return nil
|
||||
}
|
||||
|
||||
type missingImageError struct{}
|
||||
|
||||
func (missingImageError) Error() string {
|
||||
return "image name cannot be blank"
|
||||
}
|
||||
|
||||
func (missingImageError) InvalidParameter() {}
|
||||
|
||||
func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
@ -254,7 +276,7 @@ func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r
|
||||
name := vars["name"]
|
||||
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return fmt.Errorf("image name cannot be blank")
|
||||
return missingImageError{}
|
||||
}
|
||||
|
||||
force := httputils.BoolValue(r, "force")
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/runconfig"
|
||||
@ -24,11 +22,19 @@ func filterNetworkByType(nws []types.NetworkResource, netType string) ([]types.N
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid filter: 'type'='%s'", netType)
|
||||
return nil, invalidFilter(netType)
|
||||
}
|
||||
return retNws, nil
|
||||
}
|
||||
|
||||
type invalidFilter string
|
||||
|
||||
func (e invalidFilter) Error() string {
|
||||
return "Invalid filter: 'type'='" + string(e) + "'"
|
||||
}
|
||||
|
||||
func (e invalidFilter) InvalidParameter() {}
|
||||
|
||||
// filterNetworks filters network list according to user specified filter
|
||||
// and returns user chosen networks
|
||||
func filterNetworks(nws []types.NetworkResource, filter filters.Args) ([]types.NetworkResource, error) {
|
||||
|
||||
@ -2,14 +2,12 @@ package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -18,6 +16,7 @@ import (
|
||||
"github.com/docker/libnetwork"
|
||||
netconst "github.com/docker/libnetwork/datastore"
|
||||
"github.com/docker/libnetwork/networkdb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -83,6 +82,24 @@ SKIP:
|
||||
return httputils.WriteJSON(w, http.StatusOK, list)
|
||||
}
|
||||
|
||||
type invalidRequestError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequestError) InvalidParameter() {}
|
||||
|
||||
type ambigousResultsError string
|
||||
|
||||
func (e ambigousResultsError) Error() string {
|
||||
return "network " + string(e) + " is ambiguous"
|
||||
}
|
||||
|
||||
func (ambigousResultsError) InvalidParameter() {}
|
||||
|
||||
func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
@ -95,8 +112,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
|
||||
)
|
||||
if v := r.URL.Query().Get("verbose"); v != "" {
|
||||
if verbose, err = strconv.ParseBool(v); err != nil {
|
||||
err = fmt.Errorf("invalid value for verbose: %s", v)
|
||||
return errors.NewBadRequestError(err)
|
||||
return errors.Wrapf(invalidRequestError{err}, "invalid value for verbose: %s", v)
|
||||
}
|
||||
}
|
||||
scope := r.URL.Query().Get("scope")
|
||||
@ -177,7 +193,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
if len(listByFullName) > 1 {
|
||||
return fmt.Errorf("network %s is ambiguous (%d matches found based on name)", term, len(listByFullName))
|
||||
return errors.Wrapf(ambigousResultsError(term), "%d matches found based on name", len(listByFullName))
|
||||
}
|
||||
|
||||
// Find based on partial ID, returns true only if no duplicates
|
||||
@ -187,7 +203,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
if len(listByPartialID) > 1 {
|
||||
return fmt.Errorf("network %s is ambiguous (%d matches found based on ID prefix)", term, len(listByPartialID))
|
||||
return errors.Wrapf(ambigousResultsError(term), "%d matches found based on ID prefix", len(listByPartialID))
|
||||
}
|
||||
|
||||
return libnetwork.ErrNoSuchNetwork(term)
|
||||
|
||||
@ -3,14 +3,27 @@ package session
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type invalidRequest struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e invalidRequest) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequest) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e invalidRequest) InvalidParameter() {}
|
||||
|
||||
func (sr *sessionRouter) startSession(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
err := sr.backend.HandleHTTPRequest(ctx, w, r)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return invalidRequest{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -6,13 +6,13 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
basictypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -57,6 +57,20 @@ func (sr *swarmRouter) inspectCluster(ctx context.Context, w http.ResponseWriter
|
||||
return httputils.WriteJSON(w, http.StatusOK, swarm)
|
||||
}
|
||||
|
||||
type invalidRequestError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Cause() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e invalidRequestError) InvalidParameter() {}
|
||||
|
||||
func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var swarm types.Spec
|
||||
if err := json.NewDecoder(r.Body).Decode(&swarm); err != nil {
|
||||
@ -67,7 +81,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid swarm version '%s': %v", rawVersion, err)
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
var flags types.UpdateFlags
|
||||
@ -76,7 +90,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for rotateWorkerToken: %s", value)
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
flags.RotateWorkerToken = rot
|
||||
@ -86,7 +100,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for rotateManagerToken: %s", value)
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
flags.RotateManagerToken = rot
|
||||
@ -95,7 +109,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
if value := r.URL.Query().Get("rotateManagerUnlockKey"); value != "" {
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.NewBadRequestError(fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value))
|
||||
return invalidRequestError{fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value)}
|
||||
}
|
||||
|
||||
flags.RotateManagerUnlockKey = rot
|
||||
@ -139,7 +153,7 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
|
||||
}
|
||||
filter, err := filters.FromParam(r.Form.Get("filters"))
|
||||
if err != nil {
|
||||
return err
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter})
|
||||
@ -158,7 +172,7 @@ func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r
|
||||
insertDefaults, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for insertDefaults: %s", value)
|
||||
return errors.NewBadRequestError(err)
|
||||
return errors.Wrapf(invalidRequestError{err}, "invalid value for insertDefaults: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +218,7 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid service version '%s': %v", rawVersion, err)
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
var flags basictypes.ServiceUpdateOptions
|
||||
@ -297,7 +311,7 @@ func (sr *swarmRouter) updateNode(ctx context.Context, w http.ResponseWriter, r
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid node version '%s': %v", rawVersion, err)
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
if err := sr.backend.UpdateNode(vars["id"], version, node); err != nil {
|
||||
@ -403,13 +417,13 @@ func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *
|
||||
func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var secret types.SecretSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
rawVersion := r.URL.Query().Get("version")
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
return errors.NewBadRequestError(fmt.Errorf("invalid secret version"))
|
||||
return invalidRequestError{fmt.Errorf("invalid secret version")}
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
@ -474,13 +488,13 @@ func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *
|
||||
func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var config types.ConfigSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
return errors.NewBadRequestError(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
rawVersion := r.URL.Query().Get("version")
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
return errors.NewBadRequestError(fmt.Errorf("invalid config version"))
|
||||
return invalidRequestError{fmt.Errorf("invalid config version")}
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@ -85,6 +84,16 @@ func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter,
|
||||
return httputils.WriteJSON(w, http.StatusOK, du)
|
||||
}
|
||||
|
||||
type invalidRequestError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequestError) InvalidParameter() {}
|
||||
|
||||
func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
@ -105,7 +114,7 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
|
||||
)
|
||||
if !until.IsZero() {
|
||||
if until.Before(since) {
|
||||
return errors.NewBadRequestError(fmt.Errorf("`since` time (%s) cannot be after `until` time (%s)", r.Form.Get("since"), r.Form.Get("until")))
|
||||
return invalidRequestError{fmt.Errorf("`since` time (%s) cannot be after `until` time (%s)", r.Form.Get("since"), r.Form.Get("until"))}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@ -2,12 +2,10 @@ package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/server/middleware"
|
||||
"github.com/docker/docker/api/server/router"
|
||||
@ -158,6 +156,14 @@ func (s *Server) InitRouter(routers ...router.Router) {
|
||||
}
|
||||
}
|
||||
|
||||
type pageNotFoundError struct{}
|
||||
|
||||
func (pageNotFoundError) Error() string {
|
||||
return "page not found"
|
||||
}
|
||||
|
||||
func (pageNotFoundError) NotFound() {}
|
||||
|
||||
// createMux initializes the main router the server uses.
|
||||
func (s *Server) createMux() *mux.Router {
|
||||
m := mux.NewRouter()
|
||||
@ -180,8 +186,7 @@ func (s *Server) createMux() *mux.Router {
|
||||
m.Path("/debug" + r.Path()).Handler(f)
|
||||
}
|
||||
|
||||
err := errors.NewRequestNotFoundError(fmt.Errorf("page not found"))
|
||||
notFoundHandler := httputils.MakeErrorHandler(err)
|
||||
notFoundHandler := httputils.MakeErrorHandler(pageNotFoundError{})
|
||||
m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
|
||||
m.NotFoundHandler = notFoundHandler
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ package filters
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -258,12 +257,20 @@ func (filters Args) Include(field string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
type invalidFilter string
|
||||
|
||||
func (e invalidFilter) Error() string {
|
||||
return "Invalid filter '" + string(e) + "'"
|
||||
}
|
||||
|
||||
func (invalidFilter) InvalidParameter() {}
|
||||
|
||||
// Validate ensures that all the fields in the filter are valid.
|
||||
// It returns an error as soon as it finds an invalid field.
|
||||
func (filters Args) Validate(accepted map[string]bool) error {
|
||||
for name := range filters.fields {
|
||||
if !accepted[name] {
|
||||
return fmt.Errorf("Invalid filter '%s'", name)
|
||||
return invalidFilter(name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -67,7 +67,7 @@ var Propagations = []Propagation{
|
||||
type Consistency string
|
||||
|
||||
const (
|
||||
// ConsistencyFull guarantees bind-mount-like consistency
|
||||
// ConsistencyFull guarantees bind mount-like consistency
|
||||
ConsistencyFull Consistency = "consistent"
|
||||
// ConsistencyCached mounts can cache read data and FS structure
|
||||
ConsistencyCached Consistency = "cached"
|
||||
|
||||
@ -241,7 +241,7 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
|
||||
|
||||
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
||||
buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
|
||||
dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
|
||||
@ -357,7 +357,7 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
|
||||
|
||||
dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
|
||||
// TODO @jhowardmsft LCOW support. For now, if LCOW enabled, switch to linux.
|
||||
@ -374,7 +374,7 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
|
||||
// ensure that the commands are valid
|
||||
for _, n := range dockerfile.AST.Children {
|
||||
if !validCommitCommands[n.Value] {
|
||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
||||
return nil, validationError{errors.Errorf("%s is not a valid change command", n.Value)}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,7 +383,7 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
|
||||
b.disableCommit = true
|
||||
|
||||
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
dispatchState := newDispatchState()
|
||||
dispatchState.runConfig = config
|
||||
|
||||
@ -56,6 +56,7 @@ type copyInstruction struct {
|
||||
cmdName string
|
||||
infos []copyInfo
|
||||
dest string
|
||||
chownStr string
|
||||
allowLocalDecompression bool
|
||||
}
|
||||
|
||||
@ -369,6 +370,7 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b
|
||||
type copyFileOptions struct {
|
||||
decompress bool
|
||||
archiver *archive.Archiver
|
||||
chownPair idtools.IDPair
|
||||
}
|
||||
|
||||
func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions) error {
|
||||
@ -388,7 +390,7 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions)
|
||||
return errors.Wrapf(err, "source path not found")
|
||||
}
|
||||
if src.IsDir() {
|
||||
return copyDirectory(archiver, srcPath, destPath)
|
||||
return copyDirectory(archiver, srcPath, destPath, options.chownPair)
|
||||
}
|
||||
if options.decompress && archive.IsArchivePath(srcPath) && !source.noDecompress {
|
||||
return archiver.UntarPath(srcPath, destPath)
|
||||
@ -405,26 +407,28 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions)
|
||||
// is a symlink
|
||||
destPath = filepath.Join(destPath, filepath.Base(source.path))
|
||||
}
|
||||
return copyFile(archiver, srcPath, destPath)
|
||||
return copyFile(archiver, srcPath, destPath, options.chownPair)
|
||||
}
|
||||
|
||||
func copyDirectory(archiver *archive.Archiver, source, dest string) error {
|
||||
func copyDirectory(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error {
|
||||
destExists, err := isExistingDirectory(dest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to query destination path")
|
||||
}
|
||||
if err := archiver.CopyWithTar(source, dest); err != nil {
|
||||
return errors.Wrapf(err, "failed to copy directory")
|
||||
}
|
||||
return fixPermissions(source, dest, archiver.IDMappings.RootPair())
|
||||
return fixPermissions(source, dest, chownPair, !destExists)
|
||||
}
|
||||
|
||||
func copyFile(archiver *archive.Archiver, source, dest string) error {
|
||||
rootIDs := archiver.IDMappings.RootPair()
|
||||
|
||||
if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest), 0755, rootIDs); err != nil {
|
||||
func copyFile(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error {
|
||||
if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest), 0755, chownPair); err != nil {
|
||||
return errors.Wrapf(err, "failed to create new directory")
|
||||
}
|
||||
if err := archiver.CopyFileWithTar(source, dest); err != nil {
|
||||
return errors.Wrapf(err, "failed to copy file")
|
||||
}
|
||||
return fixPermissions(source, dest, rootIDs)
|
||||
return fixPermissions(source, dest, chownPair, false)
|
||||
}
|
||||
|
||||
func endsInSlash(path string) bool {
|
||||
|
||||
@ -3,14 +3,14 @@ package dockerfile
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/testutil/tempfile"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsExistingDirectory(t *testing.T) {
|
||||
tmpfile := tempfile.NewTempFile(t, "file-exists-test", "something")
|
||||
tmpfile := fs.NewFile(t, "file-exists-test", fs.WithContent("something"))
|
||||
defer tmpfile.Remove()
|
||||
tmpdir := tempfile.NewTempDir(t, "dir-exists-test")
|
||||
tmpdir := fs.NewDir(t, "dir-exists-test")
|
||||
defer tmpdir.Remove()
|
||||
|
||||
var testcases = []struct {
|
||||
@ -20,7 +20,7 @@ func TestIsExistingDirectory(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
doc: "directory exists",
|
||||
path: tmpdir.Path,
|
||||
path: tmpdir.Path(),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
@ -30,7 +30,7 @@ func TestIsExistingDirectory(t *testing.T) {
|
||||
},
|
||||
{
|
||||
doc: "file exists",
|
||||
path: tmpfile.Name(),
|
||||
path: tmpfile.Path(),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
@ -9,10 +9,16 @@ import (
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
)
|
||||
|
||||
func fixPermissions(source, destination string, rootIDs idtools.IDPair) error {
|
||||
skipChownRoot, err := isExistingDirectory(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
func fixPermissions(source, destination string, rootIDs idtools.IDPair, overrideSkip bool) error {
|
||||
var (
|
||||
skipChownRoot bool
|
||||
err error
|
||||
)
|
||||
if !overrideSkip {
|
||||
skipChownRoot, err = isExistingDirectory(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We Walk on the source rather than on the destination because we don't
|
||||
|
||||
@ -2,7 +2,7 @@ package dockerfile
|
||||
|
||||
import "github.com/docker/docker/pkg/idtools"
|
||||
|
||||
func fixPermissions(source, destination string, rootIDs idtools.IDPair) error {
|
||||
func fixPermissions(source, destination string, rootIDs idtools.IDPair, overrideSkip bool) error {
|
||||
// chown is not supported on Windows
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -146,6 +146,7 @@ func add(req dispatchRequest) error {
|
||||
return errAtLeastTwoArguments("ADD")
|
||||
}
|
||||
|
||||
flChown := req.flags.AddString("chown", "")
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -157,6 +158,7 @@ func add(req dispatchRequest) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copyInstruction.chownStr = flChown.Value
|
||||
copyInstruction.allowLocalDecompression = true
|
||||
|
||||
return req.builder.performCopy(req.state, copyInstruction)
|
||||
@ -172,6 +174,7 @@ func dispatchCopy(req dispatchRequest) error {
|
||||
}
|
||||
|
||||
flFrom := req.flags.AddString("from", "")
|
||||
flChown := req.flags.AddString("chown", "")
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -187,6 +190,7 @@ func dispatchCopy(req dispatchRequest) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copyInstruction.chownStr = flChown.Value
|
||||
|
||||
return req.builder.performCopy(req.state, copyInstruction)
|
||||
}
|
||||
@ -387,7 +391,7 @@ func workdir(req dispatchRequest) error {
|
||||
runConfig := req.state.runConfig
|
||||
// This is from the Dockerfile and will not necessarily be in platform
|
||||
// specific semantics, hence ensure it is converted.
|
||||
runConfig.WorkingDir, err = normaliseWorkdir(runConfig.WorkingDir, req.args[0])
|
||||
runConfig.WorkingDir, err = normalizeWorkdir(req.builder.platform, runConfig.WorkingDir, req.args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -784,7 +788,7 @@ func stopSignal(req dispatchRequest) error {
|
||||
sig := req.args[0]
|
||||
_, err := signal.ParseSignal(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
req.state.runConfig.StopSignal = sig
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@ -9,11 +9,11 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// normaliseWorkdir normalises a user requested working directory in a
|
||||
// normalizeWorkdir normalizes a user requested working directory in a
|
||||
// platform semantically consistent way.
|
||||
func normaliseWorkdir(current string, requested string) (string, error) {
|
||||
func normalizeWorkdir(_ string, current string, requested string) (string, error) {
|
||||
if requested == "" {
|
||||
return "", errors.New("cannot normalise nothing")
|
||||
return "", errors.New("cannot normalize nothing")
|
||||
}
|
||||
current = filepath.FromSlash(current)
|
||||
requested = filepath.FromSlash(requested)
|
||||
|
||||
@ -3,12 +3,13 @@
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormaliseWorkdir(t *testing.T) {
|
||||
func TestNormalizeWorkdir(t *testing.T) {
|
||||
testCases := []struct{ current, requested, expected, expectedError string }{
|
||||
{``, ``, ``, `cannot normalise nothing`},
|
||||
{``, ``, ``, `cannot normalize nothing`},
|
||||
{``, `foo`, `/foo`, ``},
|
||||
{``, `/foo`, `/foo`, ``},
|
||||
{`/foo`, `bar`, `/foo/bar`, ``},
|
||||
@ -16,18 +17,18 @@ func TestNormaliseWorkdir(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
normalised, err := normaliseWorkdir(test.current, test.requested)
|
||||
normalized, err := normalizeWorkdir(runtime.GOOS, test.current, test.requested)
|
||||
|
||||
if test.expectedError != "" && err == nil {
|
||||
t.Fatalf("NormaliseWorkdir should return an error %s, got nil", test.expectedError)
|
||||
t.Fatalf("NormalizeWorkdir should return an error %s, got nil", test.expectedError)
|
||||
}
|
||||
|
||||
if test.expectedError != "" && err.Error() != test.expectedError {
|
||||
t.Fatalf("NormaliseWorkdir returned wrong error. Expected %s, got %s", test.expectedError, err.Error())
|
||||
t.Fatalf("NormalizeWorkdir returned wrong error. Expected %s, got %s", test.expectedError, err.Error())
|
||||
}
|
||||
|
||||
if normalised != test.expected {
|
||||
t.Fatalf("NormaliseWorkdir error. Expected %s for current %s and requested %s, got %s", test.expected, test.current, test.requested, normalised)
|
||||
if normalized != test.expected {
|
||||
t.Fatalf("NormalizeWorkdir error. Expected %s for current %s and requested %s, got %s", test.expected, test.current, test.requested, normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@ -13,11 +14,37 @@ import (
|
||||
|
||||
var pattern = regexp.MustCompile(`^[a-zA-Z]:\.$`)
|
||||
|
||||
// normaliseWorkdir normalises a user requested working directory in a
|
||||
// normalizeWorkdir normalizes a user requested working directory in a
|
||||
// platform semantically consistent way.
|
||||
func normaliseWorkdir(current string, requested string) (string, error) {
|
||||
func normalizeWorkdir(platform string, current string, requested string) (string, error) {
|
||||
if platform == "" {
|
||||
platform = "windows"
|
||||
}
|
||||
if platform == "windows" {
|
||||
return normalizeWorkdirWindows(current, requested)
|
||||
}
|
||||
return normalizeWorkdirUnix(current, requested)
|
||||
}
|
||||
|
||||
// normalizeWorkdirUnix normalizes a user requested working directory in a
|
||||
// platform semantically consistent way.
|
||||
func normalizeWorkdirUnix(current string, requested string) (string, error) {
|
||||
if requested == "" {
|
||||
return "", errors.New("cannot normalise nothing")
|
||||
return "", errors.New("cannot normalize nothing")
|
||||
}
|
||||
current = strings.Replace(current, string(os.PathSeparator), "/", -1)
|
||||
requested = strings.Replace(requested, string(os.PathSeparator), "/", -1)
|
||||
if !path.IsAbs(requested) {
|
||||
return path.Join(`/`, current, requested), nil
|
||||
}
|
||||
return requested, nil
|
||||
}
|
||||
|
||||
// normalizeWorkdirWindows normalizes a user requested working directory in a
|
||||
// platform semantically consistent way.
|
||||
func normalizeWorkdirWindows(current string, requested string) (string, error) {
|
||||
if requested == "" {
|
||||
return "", errors.New("cannot normalize nothing")
|
||||
}
|
||||
|
||||
// `filepath.Clean` will replace "" with "." so skip in that case
|
||||
|
||||
@ -4,37 +4,43 @@ package dockerfile
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormaliseWorkdir(t *testing.T) {
|
||||
tests := []struct{ current, requested, expected, etext string }{
|
||||
{``, ``, ``, `cannot normalise nothing`},
|
||||
{``, `C:`, ``, `C:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{``, `C:.`, ``, `C:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{`c:`, `\a`, ``, `c:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{`c:.`, `\a`, ``, `c:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{``, `a`, `C:\a`, ``},
|
||||
{``, `c:\foo`, `C:\foo`, ``},
|
||||
{``, `c:\\foo`, `C:\foo`, ``},
|
||||
{``, `\foo`, `C:\foo`, ``},
|
||||
{``, `\\foo`, `C:\foo`, ``},
|
||||
{``, `/foo`, `C:\foo`, ``},
|
||||
{``, `C:/foo`, `C:\foo`, ``},
|
||||
{`C:\foo`, `bar`, `C:\foo\bar`, ``},
|
||||
{`C:\foo`, `/bar`, `C:\bar`, ``},
|
||||
{`C:\foo`, `\bar`, `C:\bar`, ``},
|
||||
func TestNormalizeWorkdir(t *testing.T) {
|
||||
tests := []struct{ platform, current, requested, expected, etext string }{
|
||||
{"windows", ``, ``, ``, `cannot normalize nothing`},
|
||||
{"windows", ``, `C:`, ``, `C:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{"windows", ``, `C:.`, ``, `C:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{"windows", `c:`, `\a`, ``, `c:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{"windows", `c:.`, `\a`, ``, `c:. is not a directory. If you are specifying a drive letter, please add a trailing '\'`},
|
||||
{"windows", ``, `a`, `C:\a`, ``},
|
||||
{"windows", ``, `c:\foo`, `C:\foo`, ``},
|
||||
{"windows", ``, `c:\\foo`, `C:\foo`, ``},
|
||||
{"windows", ``, `\foo`, `C:\foo`, ``},
|
||||
{"windows", ``, `\\foo`, `C:\foo`, ``},
|
||||
{"windows", ``, `/foo`, `C:\foo`, ``},
|
||||
{"windows", ``, `C:/foo`, `C:\foo`, ``},
|
||||
{"windows", `C:\foo`, `bar`, `C:\foo\bar`, ``},
|
||||
{"windows", `C:\foo`, `/bar`, `C:\bar`, ``},
|
||||
{"windows", `C:\foo`, `\bar`, `C:\bar`, ``},
|
||||
{"linux", ``, ``, ``, `cannot normalize nothing`},
|
||||
{"linux", ``, `foo`, `/foo`, ``},
|
||||
{"linux", ``, `/foo`, `/foo`, ``},
|
||||
{"linux", `/foo`, `bar`, `/foo/bar`, ``},
|
||||
{"linux", `/foo`, `/bar`, `/bar`, ``},
|
||||
{"linux", `\a`, `b\c`, `/a/b/c`, ``},
|
||||
}
|
||||
for _, i := range tests {
|
||||
r, e := normaliseWorkdir(i.current, i.requested)
|
||||
r, e := normalizeWorkdir(i.platform, i.current, i.requested)
|
||||
|
||||
if i.etext != "" && e == nil {
|
||||
t.Fatalf("TestNormaliseWorkingDir Expected error %s for '%s' '%s', got no error", i.etext, i.current, i.requested)
|
||||
t.Fatalf("TestNormalizeWorkingDir Expected error %s for '%s' '%s', got no error", i.etext, i.current, i.requested)
|
||||
}
|
||||
|
||||
if i.etext != "" && e.Error() != i.etext {
|
||||
t.Fatalf("TestNormaliseWorkingDir Expected error %s for '%s' '%s', got %s", i.etext, i.current, i.requested, e.Error())
|
||||
t.Fatalf("TestNormalizeWorkingDir Expected error %s for '%s' '%s', got %s", i.etext, i.current, i.requested, e.Error())
|
||||
}
|
||||
|
||||
if r != i.expected {
|
||||
t.Fatalf("TestNormaliseWorkingDir Expected '%s' for '%s' '%s', got '%s'", i.expected, i.current, i.requested, r)
|
||||
t.Fatalf("TestNormalizeWorkingDir Expected '%s' for '%s' '%s', got '%s'", i.expected, i.current, i.requested, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
components/engine/builder/dockerfile/errors.go
Normal file
15
components/engine/builder/dockerfile/errors.go
Normal file
@ -0,0 +1,15 @@
|
||||
package dockerfile
|
||||
|
||||
type validationError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.err
|
||||
}
|
||||
@ -139,7 +139,7 @@ func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
|
||||
// on which the daemon is running does not support a builder command.
|
||||
if err := platformSupports(strings.ToLower(cmd)); err != nil {
|
||||
buildsFailed.WithValues(metricsCommandNotSupportedError).Inc()
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
|
||||
msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
|
||||
@ -151,7 +151,7 @@ func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
|
||||
var err error
|
||||
ast, args, err = handleOnBuildNode(node, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
|
||||
words, err := getDispatchArgsFromNode(ast, processFunc, msg)
|
||||
if err != nil {
|
||||
buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc()
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
args = append(args, words...)
|
||||
|
||||
@ -170,7 +170,7 @@ func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
|
||||
f, ok := evaluateTable[cmd]
|
||||
if !ok {
|
||||
buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
|
||||
return nil, fmt.Errorf("unknown instruction: %s", upperCasedCmd)
|
||||
return nil, validationError{errors.Errorf("unknown instruction: %s", upperCasedCmd)}
|
||||
}
|
||||
options.state.updateRunConfig()
|
||||
err = f(newDispatchRequestFromOptions(options, b, args))
|
||||
@ -247,7 +247,7 @@ func (s *dispatchState) setDefaultPath() {
|
||||
|
||||
func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
|
||||
if ast.Next == nil {
|
||||
return nil, nil, errors.New("ONBUILD requires at least one argument")
|
||||
return nil, nil, validationError{errors.New("ONBUILD requires at least one argument")}
|
||||
}
|
||||
ast = ast.Next.Children[0]
|
||||
msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
|
||||
|
||||
@ -7,13 +7,18 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/symlink"
|
||||
lcUser "github.com/opencontainers/runc/libcontainer/user"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -107,10 +112,16 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC
|
||||
func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error {
|
||||
srcHash := getSourceHashFromInfos(inst.infos)
|
||||
|
||||
var chownComment string
|
||||
if inst.chownStr != "" {
|
||||
chownComment = fmt.Sprintf("--chown=%s", inst.chownStr)
|
||||
}
|
||||
commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)
|
||||
|
||||
// TODO: should this have been using origPaths instead of srcHash in the comment?
|
||||
runConfigWithCommentCmd := copyRunConfig(
|
||||
state.runConfig,
|
||||
withCmdCommentString(fmt.Sprintf("%s %s in %s ", inst.cmdName, srcHash, inst.dest), b.platform))
|
||||
withCmdCommentString(commentStr, b.platform))
|
||||
hit, err := b.probeCache(state, runConfigWithCommentCmd)
|
||||
if err != nil || hit {
|
||||
return err
|
||||
@ -125,9 +136,21 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error
|
||||
return err
|
||||
}
|
||||
|
||||
chownPair := b.archiver.IDMappings.RootPair()
|
||||
// if a chown was requested, perform the steps to get the uid, gid
|
||||
// translated (if necessary because of user namespaces), and replace
|
||||
// the root pair with the chown pair for copy operations
|
||||
if inst.chownStr != "" {
|
||||
chownPair, err = parseChownFlag(inst.chownStr, destInfo.root, b.archiver.IDMappings)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping")
|
||||
}
|
||||
}
|
||||
|
||||
opts := copyFileOptions{
|
||||
decompress: inst.allowLocalDecompression,
|
||||
archiver: b.archiver,
|
||||
chownPair: chownPair,
|
||||
}
|
||||
for _, info := range inst.infos {
|
||||
if err := performCopyForInfo(destInfo, info, opts); err != nil {
|
||||
@ -137,10 +160,92 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error
|
||||
return b.exportImage(state, imageMount, runConfigWithCommentCmd)
|
||||
}
|
||||
|
||||
func parseChownFlag(chown, ctrRootPath string, idMappings *idtools.IDMappings) (idtools.IDPair, error) {
|
||||
var userStr, grpStr string
|
||||
parts := strings.Split(chown, ":")
|
||||
if len(parts) > 2 {
|
||||
return idtools.IDPair{}, errors.New("invalid chown string format: " + chown)
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
// if no group specified, use the user spec as group as well
|
||||
userStr, grpStr = parts[0], parts[0]
|
||||
} else {
|
||||
userStr, grpStr = parts[0], parts[1]
|
||||
}
|
||||
|
||||
passwdPath, err := symlink.FollowSymlinkInScope(filepath.Join(ctrRootPath, "etc", "passwd"), ctrRootPath)
|
||||
if err != nil {
|
||||
return idtools.IDPair{}, errors.Wrapf(err, "can't resolve /etc/passwd path in container rootfs")
|
||||
}
|
||||
groupPath, err := symlink.FollowSymlinkInScope(filepath.Join(ctrRootPath, "etc", "group"), ctrRootPath)
|
||||
if err != nil {
|
||||
return idtools.IDPair{}, errors.Wrapf(err, "can't resolve /etc/group path in container rootfs")
|
||||
}
|
||||
uid, err := lookupUser(userStr, passwdPath)
|
||||
if err != nil {
|
||||
return idtools.IDPair{}, errors.Wrapf(err, "can't find uid for user "+userStr)
|
||||
}
|
||||
gid, err := lookupGroup(grpStr, groupPath)
|
||||
if err != nil {
|
||||
return idtools.IDPair{}, errors.Wrapf(err, "can't find gid for group "+grpStr)
|
||||
}
|
||||
|
||||
// convert as necessary because of user namespaces
|
||||
chownPair, err := idMappings.ToHost(idtools.IDPair{UID: uid, GID: gid})
|
||||
if err != nil {
|
||||
return idtools.IDPair{}, errors.Wrapf(err, "unable to convert uid/gid to host mapping")
|
||||
}
|
||||
return chownPair, nil
|
||||
}
|
||||
|
||||
func lookupUser(userStr, filepath string) (int, error) {
|
||||
// if the string is actually a uid integer, parse to int and return
|
||||
// as we don't need to translate with the help of files
|
||||
uid, err := strconv.Atoi(userStr)
|
||||
if err == nil {
|
||||
return uid, nil
|
||||
}
|
||||
users, err := lcUser.ParsePasswdFileFilter(filepath, func(u lcUser.User) bool {
|
||||
if u.Name == userStr {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return 0, errors.New("no such user: " + userStr)
|
||||
}
|
||||
return users[0].Uid, nil
|
||||
}
|
||||
|
||||
func lookupGroup(groupStr, filepath string) (int, error) {
|
||||
// if the string is actually a gid integer, parse to int and return
|
||||
// as we don't need to translate with the help of files
|
||||
gid, err := strconv.Atoi(groupStr)
|
||||
if err == nil {
|
||||
return gid, nil
|
||||
}
|
||||
groups, err := lcUser.ParseGroupFileFilter(filepath, func(g lcUser.Group) bool {
|
||||
if g.Name == groupStr {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return 0, errors.New("no such group: " + groupStr)
|
||||
}
|
||||
return groups[0].Gid, nil
|
||||
}
|
||||
|
||||
func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMount) (copyInfo, error) {
|
||||
// Twiddle the destination when it's a relative path - meaning, make it
|
||||
// relative to the WORKINGDIR
|
||||
dest, err := normaliseDest(workingDir, inst.dest)
|
||||
dest, err := normalizeDest(workingDir, inst.dest)
|
||||
if err != nil {
|
||||
return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName)
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package dockerfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@ -11,6 +13,7 @@ import (
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/builder/remotecontext"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -129,3 +132,130 @@ func TestCopyRunConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestChownFlagParsing(t *testing.T) {
|
||||
testFiles := map[string]string{
|
||||
"passwd": `root:x:0:0::/bin:/bin/false
|
||||
bin:x:1:1::/bin:/bin/false
|
||||
wwwwww:x:21:33::/bin:/bin/false
|
||||
unicorn:x:1001:1002::/bin:/bin/false
|
||||
`,
|
||||
"group": `root:x:0:
|
||||
bin:x:1:
|
||||
wwwwww:x:33:
|
||||
unicorn:x:1002:
|
||||
somegrp:x:5555:
|
||||
othergrp:x:6666:
|
||||
`,
|
||||
}
|
||||
// test mappings for validating use of maps
|
||||
idMaps := []idtools.IDMap{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: 100000,
|
||||
Size: 65536,
|
||||
},
|
||||
}
|
||||
remapped := idtools.NewIDMappingsFromMaps(idMaps, idMaps)
|
||||
unmapped := &idtools.IDMappings{}
|
||||
|
||||
contextDir, cleanup := createTestTempDir(t, "", "builder-chown-parse-test")
|
||||
defer cleanup()
|
||||
|
||||
if err := os.Mkdir(filepath.Join(contextDir, "etc"), 0755); err != nil {
|
||||
t.Fatalf("error creating test directory: %v", err)
|
||||
}
|
||||
|
||||
for filename, content := range testFiles {
|
||||
createTestTempFile(t, filepath.Join(contextDir, "etc"), filename, content, 0644)
|
||||
}
|
||||
|
||||
// positive tests
|
||||
for _, testcase := range []struct {
|
||||
name string
|
||||
chownStr string
|
||||
idMapping *idtools.IDMappings
|
||||
expected idtools.IDPair
|
||||
}{
|
||||
{
|
||||
name: "UIDNoMap",
|
||||
chownStr: "1",
|
||||
idMapping: unmapped,
|
||||
expected: idtools.IDPair{UID: 1, GID: 1},
|
||||
},
|
||||
{
|
||||
name: "UIDGIDNoMap",
|
||||
chownStr: "0:1",
|
||||
idMapping: unmapped,
|
||||
expected: idtools.IDPair{UID: 0, GID: 1},
|
||||
},
|
||||
{
|
||||
name: "UIDWithMap",
|
||||
chownStr: "0",
|
||||
idMapping: remapped,
|
||||
expected: idtools.IDPair{UID: 100000, GID: 100000},
|
||||
},
|
||||
{
|
||||
name: "UIDGIDWithMap",
|
||||
chownStr: "1:33",
|
||||
idMapping: remapped,
|
||||
expected: idtools.IDPair{UID: 100001, GID: 100033},
|
||||
},
|
||||
{
|
||||
name: "UserNoMap",
|
||||
chownStr: "bin:5555",
|
||||
idMapping: unmapped,
|
||||
expected: idtools.IDPair{UID: 1, GID: 5555},
|
||||
},
|
||||
{
|
||||
name: "GroupWithMap",
|
||||
chownStr: "0:unicorn",
|
||||
idMapping: remapped,
|
||||
expected: idtools.IDPair{UID: 100000, GID: 101002},
|
||||
},
|
||||
{
|
||||
name: "UserOnlyWithMap",
|
||||
chownStr: "unicorn",
|
||||
idMapping: remapped,
|
||||
expected: idtools.IDPair{UID: 101001, GID: 101002},
|
||||
},
|
||||
} {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
idPair, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping)
|
||||
require.NoError(t, err, "Failed to parse chown flag: %q", testcase.chownStr)
|
||||
assert.Equal(t, testcase.expected, idPair, "chown flag mapping failure")
|
||||
})
|
||||
}
|
||||
|
||||
// error tests
|
||||
for _, testcase := range []struct {
|
||||
name string
|
||||
chownStr string
|
||||
idMapping *idtools.IDMappings
|
||||
descr string
|
||||
}{
|
||||
{
|
||||
name: "BadChownFlagFormat",
|
||||
chownStr: "bob:1:555",
|
||||
idMapping: unmapped,
|
||||
descr: "invalid chown string format: bob:1:555",
|
||||
},
|
||||
{
|
||||
name: "UserNoExist",
|
||||
chownStr: "bob",
|
||||
idMapping: unmapped,
|
||||
descr: "can't find uid for user bob: no such user: bob",
|
||||
},
|
||||
{
|
||||
name: "GroupNoExist",
|
||||
chownStr: "root:bob",
|
||||
idMapping: unmapped,
|
||||
descr: "can't find gid for group bob: no such group: bob",
|
||||
},
|
||||
} {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
_, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping)
|
||||
assert.EqualError(t, err, testcase.descr, "Expected error string doesn't match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,9 +10,9 @@ import (
|
||||
"github.com/docker/docker/pkg/system"
|
||||
)
|
||||
|
||||
// normaliseDest normalises the destination of a COPY/ADD command in a
|
||||
// normalizeDest normalizes the destination of a COPY/ADD command in a
|
||||
// platform semantically consistent way.
|
||||
func normaliseDest(workingDir, requested string) (string, error) {
|
||||
func normalizeDest(workingDir, requested string) (string, error) {
|
||||
dest := filepath.FromSlash(requested)
|
||||
endsInSlash := strings.HasSuffix(requested, string(os.PathSeparator))
|
||||
if !system.IsAbs(requested) {
|
||||
|
||||
@ -10,9 +10,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// normaliseDest normalises the destination of a COPY/ADD command in a
|
||||
// normalizeDest normalizes the destination of a COPY/ADD command in a
|
||||
// platform semantically consistent way.
|
||||
func normaliseDest(workingDir, requested string) (string, error) {
|
||||
func normalizeDest(workingDir, requested string) (string, error) {
|
||||
dest := filepath.FromSlash(requested)
|
||||
endsInSlash := strings.HasSuffix(dest, string(os.PathSeparator))
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormaliseDest(t *testing.T) {
|
||||
func TestNormalizeDest(t *testing.T) {
|
||||
tests := []struct{ current, requested, expected, etext string }{
|
||||
{``, `D:\`, ``, `Windows does not support destinations not on the system drive (C:)`},
|
||||
{``, `e:/`, ``, `Windows does not support destinations not on the system drive (C:)`},
|
||||
@ -40,7 +40,7 @@ func TestNormaliseDest(t *testing.T) {
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
msg := fmt.Sprintf("Input: %s, %s", testcase.current, testcase.requested)
|
||||
actual, err := normaliseDest(testcase.current, testcase.requested)
|
||||
actual, err := normalizeDest(testcase.current, testcase.requested)
|
||||
if testcase.etext == "" {
|
||||
if !assert.NoError(t, err, msg) {
|
||||
continue
|
||||
|
||||
@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.")
|
||||
errDockerfileNotStringArray = errors.New("when using JSON array syntax, arrays must be comprised of strings only")
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
75
components/engine/builder/remotecontext/errors.go
Normal file
75
components/engine/builder/remotecontext/errors.go
Normal file
@ -0,0 +1,75 @@
|
||||
package remotecontext
|
||||
|
||||
type notFoundError string
|
||||
|
||||
func (e notFoundError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (notFoundError) NotFound() {}
|
||||
|
||||
type requestError string
|
||||
|
||||
func (e requestError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e requestError) InvalidParameter() {}
|
||||
|
||||
type unauthorizedError string
|
||||
|
||||
func (e unauthorizedError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (unauthorizedError) Unauthorized() {}
|
||||
|
||||
type forbiddenError string
|
||||
|
||||
func (e forbiddenError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (forbiddenError) Forbidden() {}
|
||||
|
||||
type dnsError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e dnsError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e dnsError) NotFound() {}
|
||||
|
||||
func (e dnsError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type systemError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e systemError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e systemError) SystemError() {}
|
||||
|
||||
func (e systemError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type unknownError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e unknownError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (unknownError) Unknown() {}
|
||||
|
||||
func (e unknownError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/docker/docker/builder"
|
||||
@ -68,20 +70,37 @@ func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.
|
||||
|
||||
// GetWithStatusError does an http.Get() and returns an error if the
|
||||
// status code is 4xx or 5xx.
|
||||
func GetWithStatusError(url string) (resp *http.Response, err error) {
|
||||
if resp, err = http.Get(url); err != nil {
|
||||
return nil, err
|
||||
func GetWithStatusError(address string) (resp *http.Response, err error) {
|
||||
if resp, err = http.Get(address); err != nil {
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout {
|
||||
return nil, dnsError{err}
|
||||
}
|
||||
}
|
||||
return nil, systemError{err}
|
||||
}
|
||||
if resp.StatusCode < 400 {
|
||||
return resp, nil
|
||||
}
|
||||
msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status)
|
||||
msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, msg+": error reading body")
|
||||
return nil, errors.Wrap(systemError{err}, msg+": error reading body")
|
||||
}
|
||||
return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body))
|
||||
|
||||
msg += ": " + string(bytes.TrimSpace(body))
|
||||
switch resp.StatusCode {
|
||||
case http.StatusNotFound:
|
||||
return nil, notFoundError(msg)
|
||||
case http.StatusBadRequest:
|
||||
return nil, requestError(msg)
|
||||
case http.StatusUnauthorized:
|
||||
return nil, unauthorizedError(msg)
|
||||
case http.StatusForbidden:
|
||||
return nil, forbiddenError(msg)
|
||||
}
|
||||
return nil, unknownError{errors.New(msg)}
|
||||
}
|
||||
|
||||
// inspectResponse looks into the http response data at r to determine whether its
|
||||
|
||||
@ -10,8 +10,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -253,7 +253,7 @@ func TestGetWithStatusError(t *testing.T) {
|
||||
if testcase.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := testutil.ReadBody(response.Body)
|
||||
body, err := readBody(response.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), testcase.expectedBody)
|
||||
} else {
|
||||
@ -261,3 +261,8 @@ func TestGetWithStatusError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readBody(b io.ReadCloser) ([]byte, error) {
|
||||
defer b.Close()
|
||||
return ioutil.ReadAll(b)
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -219,9 +220,9 @@ func (cli *Client) getAPIPath(p string, query url.Values) string {
|
||||
var apiPath string
|
||||
if cli.version != "" {
|
||||
v := strings.TrimPrefix(cli.version, "v")
|
||||
apiPath = cli.basePath + "/v" + v + p
|
||||
apiPath = path.Join(cli.basePath, "/v"+v+p)
|
||||
} else {
|
||||
apiPath = cli.basePath + p
|
||||
apiPath = path.Join(cli.basePath, p)
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
|
||||
@ -9,6 +9,14 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
// transportFunc allows us to inject a mock transport for testing. We define it
|
||||
// here so we can detect the tlsconfig and return nil for only this type.
|
||||
type transportFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return tf(req)
|
||||
}
|
||||
|
||||
func newMockClient(doer func(*http.Request) (*http.Response, error)) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transportFunc(doer),
|
||||
|
||||
@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -21,6 +22,9 @@ func (cli *Client) ImageRemove(ctx context.Context, imageID string, options type
|
||||
|
||||
resp, err := cli.delete(ctx, "/images/"+imageID, query, nil)
|
||||
if err != nil {
|
||||
if resp.statusCode == http.StatusNotFound {
|
||||
return nil, imageNotFoundError{imageID}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,17 @@ func TestImageRemoveError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRemoveImageNotFound(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
|
||||
}
|
||||
|
||||
_, err := client.ImageRemove(context.Background(), "unknown", types.ImageRemoveOptions{})
|
||||
if err == nil || !IsErrNotFound(err) {
|
||||
t.Fatalf("expected an imageNotFoundError error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRemove(t *testing.T) {
|
||||
expectedURL := "/images/image_id"
|
||||
removeCases := []struct {
|
||||
|
||||
@ -2,18 +2,17 @@ package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestImageSearchAnyError(t *testing.T) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -8,7 +10,7 @@ import (
|
||||
// Ping pings the server and returns the value of the "Docker-Experimental", "OS-Type" & "API-Version" headers
|
||||
func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
|
||||
var ping types.Ping
|
||||
req, err := cli.buildRequest("GET", cli.basePath+"/_ping", nil, nil)
|
||||
req, err := cli.buildRequest("GET", path.Join(cli.basePath, "/_ping"), nil, nil)
|
||||
if err != nil {
|
||||
return ping, err
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
@ -5,14 +5,6 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// transportFunc allows us to inject a mock transport for testing. We define it
|
||||
// here so we can detect the tlsconfig and return nil for only this type.
|
||||
type transportFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return tf(req)
|
||||
}
|
||||
|
||||
// resolveTLSConfig attempts to resolve the TLS configuration from the
|
||||
// RoundTripper.
|
||||
func resolveTLSConfig(transport http.RoundTripper) *tls.Config {
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@ -19,7 +20,7 @@ const (
|
||||
func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
||||
var maxConcurrentDownloads, maxConcurrentUploads int
|
||||
|
||||
conf.ServiceOptions.InstallCliFlags(flags)
|
||||
installRegistryServiceFlags(&conf.ServiceOptions, flags)
|
||||
|
||||
flags.Var(opts.NewNamedListOptsRef("storage-opts", &conf.GraphOptions, nil), "storage-opt", "Storage driver options")
|
||||
flags.Var(opts.NewNamedListOptsRef("authorization-plugins", &conf.AuthorizationPlugins, nil), "authorization-plugin", "Authorization plugins to load")
|
||||
@ -75,3 +76,17 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
||||
conf.MaxConcurrentDownloads = &maxConcurrentDownloads
|
||||
conf.MaxConcurrentUploads = &maxConcurrentUploads
|
||||
}
|
||||
|
||||
func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) {
|
||||
ana := opts.NewNamedListOptsRef("allow-nondistributable-artifacts", &options.AllowNondistributableArtifacts, registry.ValidateIndexName)
|
||||
mirrors := opts.NewNamedListOptsRef("registry-mirrors", &options.Mirrors, registry.ValidateMirror)
|
||||
insecureRegistries := opts.NewNamedListOptsRef("insecure-registries", &options.InsecureRegistries, registry.ValidateIndexName)
|
||||
|
||||
flags.Var(ana, "allow-nondistributable-artifacts", "Allow push of nondistributable artifacts to registry")
|
||||
flags.Var(mirrors, "registry-mirror", "Preferred Docker registry mirror")
|
||||
flags.Var(insecureRegistries, "insecure-registry", "Enable insecure registry communication")
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
flags.BoolVar(&options.V2Only, "disable-legacy-registry", true, "Disable contacting legacy registries")
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/daemon/config"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
|
||||
@ -453,7 +453,7 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
|
||||
c, err := config.MergeDaemonConfigurations(conf, flags, opts.configFile)
|
||||
if err != nil {
|
||||
if flags.Changed("config-file") || !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("unable to configure the Docker daemon with file %s: %v\n", opts.configFile, err)
|
||||
return nil, fmt.Errorf("unable to configure the Docker daemon with file %s: %v", opts.configFile, err)
|
||||
}
|
||||
}
|
||||
// the merged configuration can be nil if the config file didn't exist.
|
||||
|
||||
@ -5,27 +5,14 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/libcontainerd"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const defaultDaemonConfigFile = ""
|
||||
|
||||
// currentUserIsOwner checks whether the current user is the owner of the given
|
||||
// file.
|
||||
func currentUserIsOwner(f string) bool {
|
||||
if fileInfo, err := system.Stat(f); err == nil && fileInfo != nil {
|
||||
if int(fileInfo.UID()) == os.Getuid() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// setDefaultUmask sets the umask to 0022 to avoid problems
|
||||
// caused by custom umask
|
||||
func setDefaultUmask() error {
|
||||
|
||||
@ -4,8 +4,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/tempfile"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -46,9 +46,9 @@ func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{"labels": ["l3=foo"]}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"labels": ["l3=foo"]}`))
|
||||
defer tempFile.Remove()
|
||||
configFile := tempFile.Name()
|
||||
configFile := tempFile.Path()
|
||||
|
||||
opts := defaultOptions(configFile)
|
||||
flags := opts.flags
|
||||
@ -62,10 +62,10 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{"tlsverify": true}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
@ -75,10 +75,10 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{"tlsverify": false}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
@ -88,10 +88,10 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
@ -101,10 +101,10 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{"log-level": "warn"}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -114,10 +114,10 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
|
||||
|
||||
func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
|
||||
content := `{"tlscacert": "/etc/certs/ca.pem", "log-driver": "syslog"}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -131,10 +131,10 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
|
||||
"registry-mirrors": ["https://mirrors.docker.com"],
|
||||
"insecure-registries": ["https://insecure.docker.com"]
|
||||
}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
|
||||
@ -9,17 +9,17 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/pkg/testutil/tempfile"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
|
||||
content := `{"log-opts": {"max-size": "1k"}}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts.Debug = true
|
||||
opts.LogLevel = "info"
|
||||
assert.NoError(t, opts.flags.Set("selinux-enabled", "true"))
|
||||
@ -37,10 +37,10 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
|
||||
|
||||
func TestLoadDaemonConfigWithNetwork(t *testing.T) {
|
||||
content := `{"bip": "127.0.0.2", "ip": "127.0.0.1"}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -54,10 +54,10 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
|
||||
"cluster-store-opts": {"kv.cacertfile": "/var/lib/docker/discovery_certs/ca.pem"},
|
||||
"log-opts": {"tag": "test"}
|
||||
}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -71,10 +71,10 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
|
||||
|
||||
func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
|
||||
content := `{ "userland-proxy": false }`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -90,10 +90,10 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
|
||||
tempFile := tempfile.NewTempFile(t, "config", `{}`)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
@ -103,10 +103,10 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
|
||||
|
||||
func TestLoadDaemonConfigWithLegacyRegistryOptions(t *testing.T) {
|
||||
content := `{"disable-legacy-registry": false}`
|
||||
tempFile := tempfile.NewTempFile(t, "config", content)
|
||||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Name())
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadedConfig)
|
||||
|
||||
@ -7,19 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/libcontainerd"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var defaultDaemonConfigFile = ""
|
||||
|
||||
// currentUserIsOwner checks whether the current user is the owner of the given
|
||||
// file.
|
||||
func currentUserIsOwner(f string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// setDefaultUmask doesn't do anything on windows
|
||||
func setDefaultUmask() error {
|
||||
return nil
|
||||
@ -61,8 +54,8 @@ func (cli *DaemonCli) setupConfigReloadTrap() {
|
||||
sa := windows.SecurityAttributes{
|
||||
Length: 0,
|
||||
}
|
||||
ev := "Global\\docker-daemon-config-" + fmt.Sprint(os.Getpid())
|
||||
if h, _ := system.CreateEvent(&sa, false, false, ev); h != 0 {
|
||||
ev, _ := windows.UTF16PtrFromString("Global\\docker-daemon-config-" + fmt.Sprint(os.Getpid()))
|
||||
if h, _ := windows.CreateEvent(&sa, 0, 0, ev); h != 0 {
|
||||
logrus.Debugf("Config reload - waiting signal at %s", ev)
|
||||
for {
|
||||
windows.WaitForSingleObject(h, windows.INFINITE)
|
||||
|
||||
@ -43,6 +43,7 @@ import (
|
||||
"github.com/docker/libnetwork/options"
|
||||
"github.com/docker/libnetwork/types"
|
||||
agentexec "github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -55,8 +56,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidEndpoint = fmt.Errorf("invalid endpoint while building port map info")
|
||||
errInvalidNetwork = fmt.Errorf("invalid network settings while building port map info")
|
||||
errInvalidEndpoint = errors.New("invalid endpoint while building port map info")
|
||||
errInvalidNetwork = errors.New("invalid network settings while building port map info")
|
||||
)
|
||||
|
||||
// Container holds the structure defining a container object.
|
||||
@ -260,12 +261,17 @@ func (container *Container) WriteHostConfig() (*containertypes.HostConfig, error
|
||||
|
||||
// SetupWorkingDirectory sets up the container's working directory as set in container.Config.WorkingDir
|
||||
func (container *Container) SetupWorkingDirectory(rootIDs idtools.IDPair) error {
|
||||
// TODO @jhowardmsft, @gupta-ak LCOW Support. This will need revisiting.
|
||||
// We will need to do remote filesystem operations here.
|
||||
if container.Platform != runtime.GOOS {
|
||||
return nil
|
||||
}
|
||||
|
||||
if container.Config.WorkingDir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
container.Config.WorkingDir = filepath.Clean(container.Config.WorkingDir)
|
||||
|
||||
pth, err := container.GetResourcePath(container.Config.WorkingDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -274,7 +280,7 @@ func (container *Container) SetupWorkingDirectory(rootIDs idtools.IDPair) error
|
||||
if err := idtools.MkdirAllAndChownNew(pth, 0755, rootIDs); err != nil {
|
||||
pthInfo, err2 := os.Stat(pth)
|
||||
if err2 == nil && pthInfo != nil && !pthInfo.IsDir() {
|
||||
return fmt.Errorf("Cannot mkdir: %s is not a directory", container.Config.WorkingDir)
|
||||
return errors.Errorf("Cannot mkdir: %s is not a directory", container.Config.WorkingDir)
|
||||
}
|
||||
|
||||
return err
|
||||
@ -357,7 +363,7 @@ func (container *Container) StartLogger() (logger.Logger, error) {
|
||||
cfg := container.HostConfig.LogConfig
|
||||
initDriver, err := logger.GetLogDriver(cfg.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get logging factory: %v", err)
|
||||
return nil, errors.Wrap(err, "failed to get logging factory")
|
||||
}
|
||||
info := logger.Info{
|
||||
Config: cfg.Config,
|
||||
@ -723,18 +729,18 @@ func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epC
|
||||
|
||||
for _, ips := range ipam.LinkLocalIPs {
|
||||
if linkip = net.ParseIP(ips); linkip == nil && ips != "" {
|
||||
return nil, fmt.Errorf("Invalid link-local IP address:%s", ipam.LinkLocalIPs)
|
||||
return nil, errors.Errorf("Invalid link-local IP address: %s", ipam.LinkLocalIPs)
|
||||
}
|
||||
ipList = append(ipList, linkip)
|
||||
|
||||
}
|
||||
|
||||
if ip = net.ParseIP(ipam.IPv4Address); ip == nil && ipam.IPv4Address != "" {
|
||||
return nil, fmt.Errorf("Invalid IPv4 address:%s)", ipam.IPv4Address)
|
||||
return nil, errors.Errorf("Invalid IPv4 address: %s)", ipam.IPv4Address)
|
||||
}
|
||||
|
||||
if ip6 = net.ParseIP(ipam.IPv6Address); ip6 == nil && ipam.IPv6Address != "" {
|
||||
return nil, fmt.Errorf("Invalid IPv6 address:%s)", ipam.IPv6Address)
|
||||
return nil, errors.Errorf("Invalid IPv6 address: %s)", ipam.IPv6Address)
|
||||
}
|
||||
|
||||
createOptions = append(createOptions,
|
||||
@ -838,7 +844,7 @@ func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epC
|
||||
portStart, portEnd, err = newP.Range()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err)
|
||||
return nil, errors.Wrapf(err, "Error parsing HostPort value (%s)", binding[i].HostPort)
|
||||
}
|
||||
pbCopy.HostPort = uint16(portStart)
|
||||
pbCopy.HostPortEnd = uint16(portEnd)
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -262,6 +261,14 @@ func (container *Container) ConfigMounts() []Mount {
|
||||
return mounts
|
||||
}
|
||||
|
||||
type conflictingUpdateOptions string
|
||||
|
||||
func (e conflictingUpdateOptions) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e conflictingUpdateOptions) Conflict() {}
|
||||
|
||||
// UpdateContainer updates configuration of a container. Callers must hold a Lock on the Container.
|
||||
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
||||
// update resources of container
|
||||
@ -273,16 +280,16 @@ func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfi
|
||||
// once NanoCPU is already set, updating CPUPeriod/CPUQuota will be blocked, and vice versa.
|
||||
// In the following we make sure the intended update (resources) does not conflict with the existing (cResource).
|
||||
if resources.NanoCPUs > 0 && cResources.CPUPeriod > 0 {
|
||||
return fmt.Errorf("Conflicting options: Nano CPUs cannot be updated as CPU Period has already been set")
|
||||
return conflictingUpdateOptions("Conflicting options: Nano CPUs cannot be updated as CPU Period has already been set")
|
||||
}
|
||||
if resources.NanoCPUs > 0 && cResources.CPUQuota > 0 {
|
||||
return fmt.Errorf("Conflicting options: Nano CPUs cannot be updated as CPU Quota has already been set")
|
||||
return conflictingUpdateOptions("Conflicting options: Nano CPUs cannot be updated as CPU Quota has already been set")
|
||||
}
|
||||
if resources.CPUPeriod > 0 && cResources.NanoCPUs > 0 {
|
||||
return fmt.Errorf("Conflicting options: CPU Period cannot be updated as NanoCPUs has already been set")
|
||||
return conflictingUpdateOptions("Conflicting options: CPU Period cannot be updated as NanoCPUs has already been set")
|
||||
}
|
||||
if resources.CPUQuota > 0 && cResources.NanoCPUs > 0 {
|
||||
return fmt.Errorf("Conflicting options: CPU Quota cannot be updated as NanoCPUs has already been set")
|
||||
return conflictingUpdateOptions("Conflicting options: CPU Quota cannot be updated as NanoCPUs has already been set")
|
||||
}
|
||||
|
||||
if resources.BlkioWeight != 0 {
|
||||
@ -310,7 +317,7 @@ func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfi
|
||||
// if memory limit smaller than already set memoryswap limit and doesn't
|
||||
// update the memoryswap limit, then error out.
|
||||
if resources.Memory > cResources.MemorySwap && resources.MemorySwap == 0 {
|
||||
return fmt.Errorf("Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time")
|
||||
return conflictingUpdateOptions("Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time")
|
||||
}
|
||||
cResources.Memory = resources.Memory
|
||||
}
|
||||
@ -327,7 +334,7 @@ func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfi
|
||||
// update HostConfig of container
|
||||
if hostConfig.RestartPolicy.Name != "" {
|
||||
if container.HostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return fmt.Errorf("Restart policy cannot be updated because AutoRemove is enabled for the container")
|
||||
return conflictingUpdateOptions("Restart policy cannot be updated because AutoRemove is enabled for the container")
|
||||
}
|
||||
container.HostConfig.RestartPolicy = hostConfig.RestartPolicy
|
||||
}
|
||||
|
||||
@ -28,16 +28,20 @@ func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res[0] == '/' || res[0] == '\\' {
|
||||
res = res[1:]
|
||||
}
|
||||
|
||||
// Make sure an online file-system operation is permitted.
|
||||
if err := daemon.isOnlineFSOperationPermitted(container); err != nil {
|
||||
return nil, err
|
||||
return nil, systemError{err}
|
||||
}
|
||||
|
||||
return daemon.containerCopy(container, res)
|
||||
data, err := daemon.containerCopy(container, res)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, containerFileNotFound{res, name}
|
||||
}
|
||||
return nil, systemError{err}
|
||||
}
|
||||
|
||||
// ContainerStatPath stats the filesystem resource at the specified path in the
|
||||
@ -50,10 +54,18 @@ func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.C
|
||||
|
||||
// Make sure an online file-system operation is permitted.
|
||||
if err := daemon.isOnlineFSOperationPermitted(container); err != nil {
|
||||
return nil, err
|
||||
return nil, systemError{err}
|
||||
}
|
||||
|
||||
return daemon.containerStatPath(container, path)
|
||||
stat, err = daemon.containerStatPath(container, path)
|
||||
if err == nil {
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, containerFileNotFound{path, name}
|
||||
}
|
||||
return nil, systemError{err}
|
||||
}
|
||||
|
||||
// ContainerArchivePath creates an archive of the filesystem resource at the
|
||||
@ -67,10 +79,18 @@ func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io
|
||||
|
||||
// Make sure an online file-system operation is permitted.
|
||||
if err := daemon.isOnlineFSOperationPermitted(container); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, systemError{err}
|
||||
}
|
||||
|
||||
return daemon.containerArchivePath(container, path)
|
||||
content, stat, err = daemon.containerArchivePath(container, path)
|
||||
if err == nil {
|
||||
return content, stat, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil, containerFileNotFound{path, name}
|
||||
}
|
||||
return nil, nil, systemError{err}
|
||||
}
|
||||
|
||||
// ContainerExtractToDir extracts the given archive to the specified location
|
||||
@ -87,10 +107,18 @@ func (daemon *Daemon) ContainerExtractToDir(name, path string, copyUIDGID, noOve
|
||||
|
||||
// Make sure an online file-system operation is permitted.
|
||||
if err := daemon.isOnlineFSOperationPermitted(container); err != nil {
|
||||
return err
|
||||
return systemError{err}
|
||||
}
|
||||
|
||||
return daemon.containerExtractToDir(container, path, copyUIDGID, noOverwriteDirNonDir, content)
|
||||
err = daemon.containerExtractToDir(container, path, copyUIDGID, noOverwriteDirNonDir, content)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return containerFileNotFound{path, name}
|
||||
}
|
||||
return systemError{err}
|
||||
}
|
||||
|
||||
// containerStatPath stats the filesystem resource at the specified path in this
|
||||
@ -297,6 +325,9 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path
|
||||
}
|
||||
|
||||
func (daemon *Daemon) containerCopy(container *container.Container, resource string) (rc io.ReadCloser, err error) {
|
||||
if resource[0] == '/' || resource[0] == '\\' {
|
||||
resource = resource[1:]
|
||||
}
|
||||
container.Lock()
|
||||
|
||||
defer func() {
|
||||
|
||||
@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/container"
|
||||
"github.com/docker/docker/container/stream"
|
||||
"github.com/docker/docker/daemon/logger"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -22,7 +22,7 @@ func (daemon *Daemon) ContainerAttach(prefixOrName string, c *backend.ContainerA
|
||||
if c.DetachKeys != "" {
|
||||
keys, err = term.ToBytes(c.DetachKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid detach keys (%s) provided", c.DetachKeys)
|
||||
return validationError{errors.Errorf("Invalid detach keys (%s) provided", c.DetachKeys)}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,12 +31,12 @@ func (daemon *Daemon) ContainerAttach(prefixOrName string, c *backend.ContainerA
|
||||
return err
|
||||
}
|
||||
if container.IsPaused() {
|
||||
err := fmt.Errorf("Container %s is paused, unpause the container before attach.", prefixOrName)
|
||||
return errors.NewRequestConflictError(err)
|
||||
err := fmt.Errorf("container %s is paused, unpause the container before attach", prefixOrName)
|
||||
return stateConflictError{err}
|
||||
}
|
||||
if container.IsRestarting() {
|
||||
err := fmt.Errorf("Container %s is restarting, wait until the container is running.", prefixOrName)
|
||||
return errors.NewRequestConflictError(err)
|
||||
err := fmt.Errorf("container %s is restarting, wait until the container is running", prefixOrName)
|
||||
return stateConflictError{err}
|
||||
}
|
||||
|
||||
cfg := stream.AttachConfig{
|
||||
@ -119,7 +119,7 @@ func (daemon *Daemon) containerAttach(c *container.Container, cfg *stream.Attach
|
||||
}
|
||||
cLog, ok := logDriver.(logger.LogReader)
|
||||
if !ok {
|
||||
return logger.ErrReadLogsNotSupported
|
||||
return logger.ErrReadLogsNotSupported{}
|
||||
}
|
||||
logs := cLog.ReadLogs(logger.ReadConfig{Tail: -1})
|
||||
defer logs.Close()
|
||||
|
||||
@ -72,21 +72,6 @@ const (
|
||||
contextPrefix = "com.docker.swarm"
|
||||
)
|
||||
|
||||
// errNoSwarm is returned on leaving a cluster that was never initialized
|
||||
var errNoSwarm = errors.New("This node is not part of a swarm")
|
||||
|
||||
// errSwarmExists is returned on initialize or join request for a cluster that has already been activated
|
||||
var errSwarmExists = errors.New("This node is already part of a swarm. Use \"docker swarm leave\" to leave this swarm and join another one.")
|
||||
|
||||
// errSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
|
||||
var errSwarmJoinTimeoutReached = errors.New("Timeout was reached before node was joined. The attempt to join the swarm will continue in the background. Use the \"docker info\" command to see the current swarm status of your node.")
|
||||
|
||||
// errSwarmLocked is returned if the swarm is encrypted and needs a key to unlock it.
|
||||
var errSwarmLocked = errors.New("Swarm is encrypted and needs to be unlocked before it can be used. Please use \"docker swarm unlock\" to unlock it.")
|
||||
|
||||
// errSwarmCertificatesExpired is returned if docker was not started for the whole validity period and they had no chance to renew automatically.
|
||||
var errSwarmCertificatesExpired = errors.New("Swarm certificates have expired. To replace them, leave the swarm and join again.")
|
||||
|
||||
// NetworkSubnetsProvider exposes functions for retrieving the subnets
|
||||
// of networks managed by Docker, so they can be filtered.
|
||||
type NetworkSubnetsProvider interface {
|
||||
@ -343,12 +328,12 @@ func (c *Cluster) errNoManager(st nodeState) error {
|
||||
if st.err == errSwarmCertificatesExpired {
|
||||
return errSwarmCertificatesExpired
|
||||
}
|
||||
return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
|
||||
return errors.WithStack(notAvailableError("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again."))
|
||||
}
|
||||
if st.swarmNode.Manager() != nil {
|
||||
return errors.New("This node is not a swarm manager. Manager is being prepared or has trouble connecting to the cluster.")
|
||||
return errors.WithStack(notAvailableError("This node is not a swarm manager. Manager is being prepared or has trouble connecting to the cluster."))
|
||||
}
|
||||
return errors.New("This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.")
|
||||
return errors.WithStack(notAvailableError("This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager."))
|
||||
}
|
||||
|
||||
// Cleanup stops active swarm node. This is run before daemon shutdown.
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/errdefs"
|
||||
enginetypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm/runtime"
|
||||
"github.com/docker/docker/plugin"
|
||||
@ -198,8 +199,7 @@ func (p *Controller) Wait(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
_, ok := errors.Cause(err).(plugin.ErrNotFound)
|
||||
return ok
|
||||
return errdefs.IsNotFound(err)
|
||||
}
|
||||
|
||||
// Shutdown is the shutdown phase from swarmkit
|
||||
|
||||
114
components/engine/daemon/cluster/errors.go
Normal file
114
components/engine/daemon/cluster/errors.go
Normal file
@ -0,0 +1,114 @@
|
||||
package cluster
|
||||
|
||||
const (
|
||||
// errNoSwarm is returned on leaving a cluster that was never initialized
|
||||
errNoSwarm notAvailableError = "This node is not part of a swarm"
|
||||
|
||||
// errSwarmExists is returned on initialize or join request for a cluster that has already been activated
|
||||
errSwarmExists notAvailableError = "This node is already part of a swarm. Use \"docker swarm leave\" to leave this swarm and join another one."
|
||||
|
||||
// errSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
|
||||
errSwarmJoinTimeoutReached notAvailableError = "Timeout was reached before node joined. The attempt to join the swarm will continue in the background. Use the \"docker info\" command to see the current swarm status of your node."
|
||||
|
||||
// errSwarmLocked is returned if the swarm is encrypted and needs a key to unlock it.
|
||||
errSwarmLocked notAvailableError = "Swarm is encrypted and needs to be unlocked before it can be used. Please use \"docker swarm unlock\" to unlock it."
|
||||
|
||||
// errSwarmCertificatesExpired is returned if docker was not started for the whole validity period and they had no chance to renew automatically.
|
||||
errSwarmCertificatesExpired notAvailableError = "Swarm certificates have expired. To replace them, leave the swarm and join again."
|
||||
)
|
||||
|
||||
type notFoundError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e notFoundError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e notFoundError) NotFound() {}
|
||||
|
||||
func (e notFoundError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type ambiguousResultsError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e ambiguousResultsError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e ambiguousResultsError) InvalidParameter() {}
|
||||
|
||||
func (e ambiguousResultsError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type convertError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e convertError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e convertError) InvalidParameter() {}
|
||||
|
||||
func (e convertError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type notAllowedError string
|
||||
|
||||
func (e notAllowedError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e notAllowedError) Forbidden() {}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type notAvailableError string
|
||||
|
||||
func (e notAvailableError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e notAvailableError) Unavailable() {}
|
||||
|
||||
type configError string
|
||||
|
||||
func (e configError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e configError) InvalidParameter() {}
|
||||
|
||||
type invalidUnlockKey struct{}
|
||||
|
||||
func (invalidUnlockKey) Error() string {
|
||||
return "swarm could not be unlocked: invalid key provided"
|
||||
}
|
||||
|
||||
func (invalidUnlockKey) Unauthorized() {}
|
||||
|
||||
type notLockedError struct{}
|
||||
|
||||
func (notLockedError) Error() string {
|
||||
return "swarm is not locked"
|
||||
}
|
||||
|
||||
func (notLockedError) Conflict() {}
|
||||
@ -34,7 +34,7 @@ type Backend interface {
|
||||
CreateManagedContainer(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error)
|
||||
ContainerStart(name string, hostConfig *container.HostConfig, checkpoint string, checkpointDir string) error
|
||||
ContainerStop(name string, seconds *int) error
|
||||
ContainerLogs(context.Context, string, *types.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
|
||||
ContainerLogs(context.Context, string, *types.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error)
|
||||
ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
|
||||
ActivateContainerServiceBinding(containerName string) error
|
||||
DeactivateContainerServiceBinding(containerName string) error
|
||||
|
||||
@ -451,7 +451,7 @@ func (c *containerAdapter) logs(ctx context.Context, options api.LogSubscription
|
||||
}
|
||||
}
|
||||
}
|
||||
msgs, err := c.backend.ContainerLogs(ctx, c.container.name(), apiOptions)
|
||||
msgs, _, err := c.backend.ContainerLogs(ctx, c.container.name(), apiOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ package cluster
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -15,7 +15,7 @@ func getSwarm(ctx context.Context, c swarmapi.ControlClient) (*swarmapi.Cluster,
|
||||
}
|
||||
|
||||
if len(rl.Clusters) == 0 {
|
||||
return nil, errors.NewRequestNotFoundError(errNoSwarm)
|
||||
return nil, errors.WithStack(errNoSwarm)
|
||||
}
|
||||
|
||||
// TODO: assume one cluster only
|
||||
@ -48,11 +48,11 @@ func getNode(ctx context.Context, c swarmapi.ControlClient, input string) (*swar
|
||||
|
||||
if len(rl.Nodes) == 0 {
|
||||
err := fmt.Errorf("node %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, notFoundError{err}
|
||||
}
|
||||
|
||||
if l := len(rl.Nodes); l > 1 {
|
||||
return nil, fmt.Errorf("node %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("node %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
return rl.Nodes[0], nil
|
||||
@ -84,11 +84,11 @@ func getService(ctx context.Context, c swarmapi.ControlClient, input string, ins
|
||||
|
||||
if len(rl.Services) == 0 {
|
||||
err := fmt.Errorf("service %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, notFoundError{err}
|
||||
}
|
||||
|
||||
if l := len(rl.Services); l > 1 {
|
||||
return nil, fmt.Errorf("service %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("service %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
if !insertDefaults {
|
||||
@ -128,11 +128,11 @@ func getTask(ctx context.Context, c swarmapi.ControlClient, input string) (*swar
|
||||
|
||||
if len(rl.Tasks) == 0 {
|
||||
err := fmt.Errorf("task %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, notFoundError{err}
|
||||
}
|
||||
|
||||
if l := len(rl.Tasks); l > 1 {
|
||||
return nil, fmt.Errorf("task %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("task %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
return rl.Tasks[0], nil
|
||||
@ -164,11 +164,11 @@ func getSecret(ctx context.Context, c swarmapi.ControlClient, input string) (*sw
|
||||
|
||||
if len(rl.Secrets) == 0 {
|
||||
err := fmt.Errorf("secret %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, notFoundError{err}
|
||||
}
|
||||
|
||||
if l := len(rl.Secrets); l > 1 {
|
||||
return nil, fmt.Errorf("secret %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("secret %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
return rl.Secrets[0], nil
|
||||
@ -200,11 +200,11 @@ func getConfig(ctx context.Context, c swarmapi.ControlClient, input string) (*sw
|
||||
|
||||
if len(rl.Configs) == 0 {
|
||||
err := fmt.Errorf("config %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, notFoundError{err}
|
||||
}
|
||||
|
||||
if l := len(rl.Configs); l > 1 {
|
||||
return nil, fmt.Errorf("config %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("config %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
return rl.Configs[0], nil
|
||||
@ -238,7 +238,7 @@ func getNetwork(ctx context.Context, c swarmapi.ControlClient, input string) (*s
|
||||
}
|
||||
|
||||
if l := len(rl.Networks); l > 1 {
|
||||
return nil, fmt.Errorf("network %s is ambiguous (%d matches found)", input, l)
|
||||
return nil, ambiguousResultsError{fmt.Errorf("network %s is ambiguous (%d matches found)", input, l)}
|
||||
}
|
||||
|
||||
return rl.Networks[0], nil
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoSuchInterface = errors.New("no such interface")
|
||||
errNoIP = errors.New("could not find the system's IP address")
|
||||
errMustSpecifyListenAddr = errors.New("must specify a listening address because the address to advertise is not recognized as a system address, and a system's IP address to use could not be uniquely identified")
|
||||
errBadNetworkIdentifier = errors.New("must specify a valid IP address or interface name")
|
||||
errBadListenAddr = errors.New("listen address must be an IP address or network interface (with optional port number)")
|
||||
errBadAdvertiseAddr = errors.New("advertise address must be a non-zero IP address or network interface (with optional port number)")
|
||||
errBadDataPathAddr = errors.New("data path address must be a non-zero IP address or network interface (without a port number)")
|
||||
errBadDefaultAdvertiseAddr = errors.New("default advertise address must be a non-zero IP address or network interface (without a port number)")
|
||||
const (
|
||||
errNoSuchInterface configError = "no such interface"
|
||||
errNoIP configError = "could not find the system's IP address"
|
||||
errMustSpecifyListenAddr configError = "must specify a listening address because the address to advertise is not recognized as a system address, and a system's IP address to use could not be uniquely identified"
|
||||
errBadNetworkIdentifier configError = "must specify a valid IP address or interface name"
|
||||
errBadListenAddr configError = "listen address must be an IP address or network interface (with optional port number)"
|
||||
errBadAdvertiseAddr configError = "advertise address must be a non-zero IP address or network interface (with optional port number)"
|
||||
errBadDataPathAddr configError = "data path address must be a non-zero IP address or network interface (without a port number)"
|
||||
errBadDefaultAdvertiseAddr configError = "default advertise address must be a non-zero IP address or network interface (without a port number)"
|
||||
)
|
||||
|
||||
func resolveListenAddr(specifiedAddr string) (string, string, error) {
|
||||
@ -125,13 +124,13 @@ func resolveInterfaceAddr(specifiedInterface string) (net.IP, error) {
|
||||
if ipAddr.IP.To4() != nil {
|
||||
// IPv4
|
||||
if interfaceAddr4 != nil {
|
||||
return nil, fmt.Errorf("interface %s has more than one IPv4 address (%s and %s)", specifiedInterface, interfaceAddr4, ipAddr.IP)
|
||||
return nil, configError(fmt.Sprintf("interface %s has more than one IPv4 address (%s and %s)", specifiedInterface, interfaceAddr4, ipAddr.IP))
|
||||
}
|
||||
interfaceAddr4 = ipAddr.IP
|
||||
} else {
|
||||
// IPv6
|
||||
if interfaceAddr6 != nil {
|
||||
return nil, fmt.Errorf("interface %s has more than one IPv6 address (%s and %s)", specifiedInterface, interfaceAddr6, ipAddr.IP)
|
||||
return nil, configError(fmt.Sprintf("interface %s has more than one IPv6 address (%s and %s)", specifiedInterface, interfaceAddr6, ipAddr.IP))
|
||||
}
|
||||
interfaceAddr6 = ipAddr.IP
|
||||
}
|
||||
@ -139,7 +138,7 @@ func resolveInterfaceAddr(specifiedInterface string) (net.IP, error) {
|
||||
}
|
||||
|
||||
if interfaceAddr4 == nil && interfaceAddr6 == nil {
|
||||
return nil, fmt.Errorf("interface %s has no usable IPv4 or IPv6 address", specifiedInterface)
|
||||
return nil, configError(fmt.Sprintf("interface %s has no usable IPv4 or IPv6 address", specifiedInterface))
|
||||
}
|
||||
|
||||
// In the case that there's exactly one IPv4 address
|
||||
@ -296,7 +295,7 @@ func listSystemIPs() []net.IP {
|
||||
|
||||
func errMultipleIPs(interfaceA, interfaceB string, addrA, addrB net.IP) error {
|
||||
if interfaceA == interfaceB {
|
||||
return fmt.Errorf("could not choose an IP address to advertise since this system has multiple addresses on interface %s (%s and %s)", interfaceA, addrA, addrB)
|
||||
return configError(fmt.Sprintf("could not choose an IP address to advertise since this system has multiple addresses on interface %s (%s and %s)", interfaceA, addrA, addrB))
|
||||
}
|
||||
return fmt.Errorf("could not choose an IP address to advertise since this system has multiple addresses on different interfaces (%s on %s and %s on %s)", addrA, interfaceA, addrB, interfaceB)
|
||||
return configError(fmt.Sprintf("could not choose an IP address to advertise since this system has multiple addresses on different interfaces (%s on %s and %s on %s)", addrA, interfaceA, addrB, interfaceB))
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package cluster
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
apitypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
@ -250,8 +249,8 @@ func (c *Cluster) DetachNetwork(target string, containerID string) error {
|
||||
// CreateNetwork creates a new cluster managed network.
|
||||
func (c *Cluster) CreateNetwork(s apitypes.NetworkCreateRequest) (string, error) {
|
||||
if runconfig.IsPreDefinedNetwork(s.Name) {
|
||||
err := fmt.Errorf("%s is a pre-defined network and cannot be created", s.Name)
|
||||
return "", apierrors.NewRequestForbiddenError(err)
|
||||
err := notAllowedError(fmt.Sprintf("%s is a pre-defined network and cannot be created", s.Name))
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
var resp *swarmapi.CreateNetworkResponse
|
||||
@ -299,14 +298,13 @@ func (c *Cluster) populateNetworkID(ctx context.Context, client swarmapi.Control
|
||||
// and use its id for the request.
|
||||
apiNetwork, err = getNetwork(ctx, client, ln.Name())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not find the corresponding predefined swarm network: %v", err)
|
||||
return apierrors.NewRequestNotFoundError(err)
|
||||
return errors.Wrap(notFoundError{err}, "could not find the corresponding predefined swarm network")
|
||||
}
|
||||
goto setid
|
||||
}
|
||||
if ln != nil && !ln.Info().Dynamic() {
|
||||
err = fmt.Errorf("The network %s cannot be used with services. Only networks scoped to the swarm can be used, such as those created with the overlay driver.", ln.Name())
|
||||
return apierrors.NewRequestForbiddenError(err)
|
||||
errMsg := fmt.Sprintf("The network %s cannot be used with services. Only networks scoped to the swarm can be used, such as those created with the overlay driver.", ln.Name())
|
||||
return errors.WithStack(notAllowedError(errMsg))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
apitypes "github.com/docker/docker/api/types"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/daemon/cluster/convert"
|
||||
@ -65,7 +64,7 @@ func (c *Cluster) UpdateNode(input string, version uint64, spec types.NodeSpec)
|
||||
return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
||||
nodeSpec, err := convert.NodeSpecToGRPC(spec)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return convertError{err}
|
||||
}
|
||||
|
||||
ctx, cancel := c.getRequestContext()
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
apitypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
@ -129,7 +128,7 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string, queryRe
|
||||
|
||||
serviceSpec, err := convert.ServiceSpecToGRPC(s)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return convertError{err}
|
||||
}
|
||||
|
||||
resp = &apitypes.ServiceCreateResponse{}
|
||||
@ -233,7 +232,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
|
||||
|
||||
serviceSpec, err := convert.ServiceSpecToGRPC(spec)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return convertError{err}
|
||||
}
|
||||
|
||||
currentService, err := getService(ctx, state.controlClient, serviceIDOrName, false)
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
apitypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
@ -41,7 +40,7 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
|
||||
}
|
||||
|
||||
if err := validateAndSanitizeInitRequest(&req); err != nil {
|
||||
return "", apierrors.NewBadRequestError(err)
|
||||
return "", validationError{err}
|
||||
}
|
||||
|
||||
listenHost, listenPort, err := resolveListenAddr(req.ListenAddr)
|
||||
@ -132,12 +131,12 @@ func (c *Cluster) Join(req types.JoinRequest) error {
|
||||
c.mu.Lock()
|
||||
if c.nr != nil {
|
||||
c.mu.Unlock()
|
||||
return errSwarmExists
|
||||
return errors.WithStack(errSwarmExists)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if err := validateAndSanitizeJoinRequest(&req); err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
listenHost, listenPort, err := resolveListenAddr(req.ListenAddr)
|
||||
@ -222,7 +221,7 @@ func (c *Cluster) Update(version uint64, spec types.Spec, flags types.UpdateFlag
|
||||
// will be used to swarmkit.
|
||||
clusterSpec, err := convert.SwarmSpecToGRPC(spec)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return convertError{err}
|
||||
}
|
||||
|
||||
_, err = state.controlClient.UpdateCluster(
|
||||
@ -284,7 +283,7 @@ func (c *Cluster) UnlockSwarm(req types.UnlockRequest) error {
|
||||
} else {
|
||||
// when manager is active, return an error of "not locked"
|
||||
c.mu.RUnlock()
|
||||
return errors.New("swarm is not locked")
|
||||
return notLockedError{}
|
||||
}
|
||||
|
||||
// only when swarm is locked, code running reaches here
|
||||
@ -293,7 +292,7 @@ func (c *Cluster) UnlockSwarm(req types.UnlockRequest) error {
|
||||
|
||||
key, err := encryption.ParseHumanReadableKey(req.UnlockKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
config := nr.config
|
||||
@ -312,9 +311,9 @@ func (c *Cluster) UnlockSwarm(req types.UnlockRequest) error {
|
||||
|
||||
if err := <-nr.Ready(); err != nil {
|
||||
if errors.Cause(err) == errSwarmLocked {
|
||||
return errors.New("swarm could not be unlocked: invalid key provided")
|
||||
return invalidUnlockKey{}
|
||||
}
|
||||
return fmt.Errorf("swarm component could not be started: %v", err)
|
||||
return errors.Errorf("swarm component could not be started: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -328,7 +327,7 @@ func (c *Cluster) Leave(force bool) error {
|
||||
nr := c.nr
|
||||
if nr == nil {
|
||||
c.mu.Unlock()
|
||||
return errNoSwarm
|
||||
return errors.WithStack(errNoSwarm)
|
||||
}
|
||||
|
||||
state := c.currentNodeState()
|
||||
@ -337,7 +336,7 @@ func (c *Cluster) Leave(force bool) error {
|
||||
|
||||
if errors.Cause(state.err) == errSwarmLocked && !force {
|
||||
// leave a locked swarm without --force is not allowed
|
||||
return errors.New("Swarm is encrypted and locked. Please unlock it first or use `--force` to ignore this message.")
|
||||
return errors.WithStack(notAvailableError("Swarm is encrypted and locked. Please unlock it first or use `--force` to ignore this message."))
|
||||
}
|
||||
|
||||
if state.IsManager() && !force {
|
||||
@ -348,7 +347,7 @@ func (c *Cluster) Leave(force bool) error {
|
||||
if active && removingManagerCausesLossOfQuorum(reachable, unreachable) {
|
||||
if isLastManager(reachable, unreachable) {
|
||||
msg += "Removing the last manager erases all current state of the swarm. Use `--force` to ignore this message. "
|
||||
return errors.New(msg)
|
||||
return errors.WithStack(notAvailableError(msg))
|
||||
}
|
||||
msg += fmt.Sprintf("Removing this node leaves %v managers out of %v. Without a Raft quorum your swarm will be inaccessible. ", reachable-1, reachable+unreachable)
|
||||
}
|
||||
@ -358,7 +357,7 @@ func (c *Cluster) Leave(force bool) error {
|
||||
}
|
||||
|
||||
msg += "The only way to restore a swarm that has lost consensus is to reinitialize it with `--force-new-cluster`. Use `--force` to suppress this message."
|
||||
return errors.New(msg)
|
||||
return errors.WithStack(notAvailableError(msg))
|
||||
}
|
||||
// release readers in here
|
||||
if err := nr.Stop(); err != nil {
|
||||
|
||||
@ -502,7 +502,7 @@ func Validate(config *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := opts.ParseGenericResources(config.NodeGenericResources); err != nil {
|
||||
if _, err := ParseGenericResources(config.NodeGenericResources); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Config defines the configuration of a docker daemon.
|
||||
// These are the configuration settings that you pass
|
||||
// to the docker daemon when you launch it with say: `docker -d -e lxc`
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/daemon/discovery"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/testutil/tempfile"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -29,7 +29,7 @@ func TestGetConflictFreeConfiguration(t *testing.T) {
|
||||
}
|
||||
}`))
|
||||
|
||||
file := tempfile.NewTempFile(t, "docker-config", configFileData)
|
||||
file := fs.NewFile(t, "docker-config", fs.WithContent(configFileData))
|
||||
defer file.Remove()
|
||||
|
||||
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
@ -38,7 +38,7 @@ func TestGetConflictFreeConfiguration(t *testing.T) {
|
||||
flags.Var(opts.NewNamedUlimitOpt("default-ulimits", nil), "default-ulimit", "")
|
||||
flags.Var(opts.NewNamedMapOpts("log-opts", nil, nil), "log-opt", "")
|
||||
|
||||
cc, err := getConflictFreeConfiguration(file.Name(), flags)
|
||||
cc, err := getConflictFreeConfiguration(file.Path(), flags)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, cc.Debug)
|
||||
@ -70,7 +70,7 @@ func TestDaemonConfigurationMerge(t *testing.T) {
|
||||
}
|
||||
}`))
|
||||
|
||||
file := tempfile.NewTempFile(t, "docker-config", configFileData)
|
||||
file := fs.NewFile(t, "docker-config", fs.WithContent(configFileData))
|
||||
defer file.Remove()
|
||||
|
||||
c := &Config{
|
||||
@ -90,7 +90,7 @@ func TestDaemonConfigurationMerge(t *testing.T) {
|
||||
flags.Var(opts.NewNamedUlimitOpt("default-ulimits", nil), "default-ulimit", "")
|
||||
flags.Var(opts.NewNamedMapOpts("log-opts", nil, nil), "log-opt", "")
|
||||
|
||||
cc, err := MergeDaemonConfigurations(c, flags, file.Name())
|
||||
cc, err := MergeDaemonConfigurations(c, flags, file.Path())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, cc.Debug)
|
||||
@ -120,7 +120,7 @@ func TestDaemonConfigurationMergeShmSize(t *testing.T) {
|
||||
"default-shm-size": "1g"
|
||||
}`))
|
||||
|
||||
file := tempfile.NewTempFile(t, "docker-config", data)
|
||||
file := fs.NewFile(t, "docker-config", fs.WithContent(data))
|
||||
defer file.Remove()
|
||||
|
||||
c := &Config{}
|
||||
@ -129,7 +129,7 @@ func TestDaemonConfigurationMergeShmSize(t *testing.T) {
|
||||
shmSize := opts.MemBytes(DefaultShmSize)
|
||||
flags.Var(&shmSize, "default-shm-size", "")
|
||||
|
||||
cc, err := MergeDaemonConfigurations(c, flags, file.Name())
|
||||
cc, err := MergeDaemonConfigurations(c, flags, file.Path())
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedValue := 1 * 1024 * 1024 * 1024
|
||||
|
||||
22
components/engine/daemon/config/opts.go
Normal file
22
components/engine/daemon/config/opts.go
Normal file
@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/daemon/cluster/convert"
|
||||
"github.com/docker/swarmkit/api/genericresource"
|
||||
)
|
||||
|
||||
// ParseGenericResources parses and validates the specified string as a list of GenericResource
|
||||
func ParseGenericResources(value string) ([]swarm.GenericResource, error) {
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resources, err := genericresource.Parse(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := convert.GenericResourcesFromGRPC(resources)
|
||||
return obj, nil
|
||||
}
|
||||
@ -3,10 +3,12 @@ package daemon
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/errors"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/container"
|
||||
@ -19,6 +21,7 @@ import (
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/opencontainers/selinux/go-selinux/label"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// GetContainer looks for a container using the provided information, which could be
|
||||
@ -30,7 +33,7 @@ import (
|
||||
// If none of these searches succeed, an error is returned
|
||||
func (daemon *Daemon) GetContainer(prefixOrName string) (*container.Container, error) {
|
||||
if len(prefixOrName) == 0 {
|
||||
return nil, errors.NewBadRequestError(fmt.Errorf("No container name or ID supplied"))
|
||||
return nil, errors.WithStack(invalidIdentifier(prefixOrName))
|
||||
}
|
||||
|
||||
if containerByID := daemon.containers.Get(prefixOrName); containerByID != nil {
|
||||
@ -48,10 +51,9 @@ func (daemon *Daemon) GetContainer(prefixOrName string) (*container.Container, e
|
||||
if indexError != nil {
|
||||
// When truncindex defines an error type, use that instead
|
||||
if indexError == truncindex.ErrNotExist {
|
||||
err := fmt.Errorf("No such container: %s", prefixOrName)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
return nil, containerNotFound(prefixOrName)
|
||||
}
|
||||
return nil, indexError
|
||||
return nil, systemError{indexError}
|
||||
}
|
||||
return daemon.containers.Get(containerID), nil
|
||||
}
|
||||
@ -136,7 +138,7 @@ func (daemon *Daemon) newContainer(name string, platform string, config *contain
|
||||
if config.Hostname == "" {
|
||||
config.Hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, systemError{err}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -227,13 +229,24 @@ func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *
|
||||
|
||||
// verifyContainerSettings performs validation of the hostconfig and config
|
||||
// structures.
|
||||
func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) {
|
||||
|
||||
func (daemon *Daemon) verifyContainerSettings(platform string, hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) {
|
||||
// First perform verification of settings common across all platforms.
|
||||
if config != nil {
|
||||
if config.WorkingDir != "" {
|
||||
config.WorkingDir = filepath.FromSlash(config.WorkingDir) // Ensure in platform semantics
|
||||
if !system.IsAbs(config.WorkingDir) {
|
||||
wdInvalid := false
|
||||
if runtime.GOOS == platform {
|
||||
config.WorkingDir = filepath.FromSlash(config.WorkingDir) // Ensure in platform semantics
|
||||
if !system.IsAbs(config.WorkingDir) {
|
||||
wdInvalid = true
|
||||
}
|
||||
} else {
|
||||
// LCOW. Force Unix semantics
|
||||
config.WorkingDir = strings.Replace(config.WorkingDir, string(os.PathSeparator), "/", -1)
|
||||
if !path.IsAbs(config.WorkingDir) {
|
||||
wdInvalid = true
|
||||
}
|
||||
}
|
||||
if wdInvalid {
|
||||
return nil, fmt.Errorf("the working directory '%s' is invalid, it needs to be an absolute path", config.WorkingDir)
|
||||
}
|
||||
}
|
||||
@ -255,19 +268,19 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
|
||||
// Validate the healthcheck params of Config
|
||||
if config.Healthcheck != nil {
|
||||
if config.Healthcheck.Interval != 0 && config.Healthcheck.Interval < containertypes.MinimumDuration {
|
||||
return nil, fmt.Errorf("Interval in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
return nil, errors.Errorf("Interval in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
}
|
||||
|
||||
if config.Healthcheck.Timeout != 0 && config.Healthcheck.Timeout < containertypes.MinimumDuration {
|
||||
return nil, fmt.Errorf("Timeout in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
return nil, errors.Errorf("Timeout in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
}
|
||||
|
||||
if config.Healthcheck.Retries < 0 {
|
||||
return nil, fmt.Errorf("Retries in Healthcheck cannot be negative")
|
||||
return nil, errors.Errorf("Retries in Healthcheck cannot be negative")
|
||||
}
|
||||
|
||||
if config.Healthcheck.StartPeriod != 0 && config.Healthcheck.StartPeriod < containertypes.MinimumDuration {
|
||||
return nil, fmt.Errorf("StartPeriod in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
return nil, errors.Errorf("StartPeriod in Healthcheck cannot be less than %s", containertypes.MinimumDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -277,7 +290,7 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
|
||||
}
|
||||
|
||||
if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return nil, fmt.Errorf("can't create 'AutoRemove' container with restart policy")
|
||||
return nil, errors.Errorf("can't create 'AutoRemove' container with restart policy")
|
||||
}
|
||||
|
||||
for _, extraHost := range hostConfig.ExtraHosts {
|
||||
@ -289,12 +302,12 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
|
||||
for port := range hostConfig.PortBindings {
|
||||
_, portStr := nat.SplitProtoPort(string(port))
|
||||
if _, err := nat.ParsePort(portStr); err != nil {
|
||||
return nil, fmt.Errorf("invalid port specification: %q", portStr)
|
||||
return nil, errors.Errorf("invalid port specification: %q", portStr)
|
||||
}
|
||||
for _, pb := range hostConfig.PortBindings[port] {
|
||||
_, err := nat.NewPort(nat.SplitProtoPort(pb.HostPort))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port specification: %q", pb.HostPort)
|
||||
return nil, errors.Errorf("invalid port specification: %q", pb.HostPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,16 +317,16 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
|
||||
switch p.Name {
|
||||
case "always", "unless-stopped", "no":
|
||||
if p.MaximumRetryCount != 0 {
|
||||
return nil, fmt.Errorf("maximum retry count cannot be used with restart policy '%s'", p.Name)
|
||||
return nil, errors.Errorf("maximum retry count cannot be used with restart policy '%s'", p.Name)
|
||||
}
|
||||
case "on-failure":
|
||||
if p.MaximumRetryCount < 0 {
|
||||
return nil, fmt.Errorf("maximum retry count cannot be negative")
|
||||
return nil, errors.Errorf("maximum retry count cannot be negative")
|
||||
}
|
||||
case "":
|
||||
// do nothing
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid restart policy '%s'", p.Name)
|
||||
return nil, errors.Errorf("invalid restart policy '%s'", p.Name)
|
||||
}
|
||||
|
||||
// Now do platform-specific verification
|
||||
|
||||
@ -14,7 +14,7 @@ func (daemon *Daemon) saveApparmorConfig(container *container.Container) error {
|
||||
}
|
||||
|
||||
if err := parseSecurityOpt(container, container.HostConfig); err != nil {
|
||||
return err
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
if !container.HostConfig.Privileged {
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
derr "github.com/docker/docker/api/errors"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/container"
|
||||
@ -923,7 +922,7 @@ func (daemon *Daemon) getNetworkedContainer(containerID, connectedContainerID st
|
||||
}
|
||||
if !nc.IsRunning() {
|
||||
err := fmt.Errorf("cannot join network of a non running container: %s", connectedContainerID)
|
||||
return nil, derr.NewRequestConflictError(err)
|
||||
return nil, stateConflictError{err}
|
||||
}
|
||||
if nc.IsRestarting() {
|
||||
return nil, errContainerIsRestarting(connectedContainerID)
|
||||
|
||||
@ -91,7 +91,7 @@ func (daemon *Daemon) getPidContainer(container *container.Container) (*containe
|
||||
|
||||
func containerIsRunning(c *container.Container) error {
|
||||
if !c.IsRunning() {
|
||||
return errors.Errorf("container %s is not running", c.ID)
|
||||
return stateConflictError{errors.Errorf("container %s is not running", c.ID)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
apierrors "github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/types"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
@ -37,17 +36,27 @@ func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (conta
|
||||
func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
|
||||
start := time.Now()
|
||||
if params.Config == nil {
|
||||
return containertypes.ContainerCreateCreatedBody{}, fmt.Errorf("Config cannot be empty in order to create a container")
|
||||
return containertypes.ContainerCreateCreatedBody{}, validationError{errors.New("Config cannot be empty in order to create a container")}
|
||||
}
|
||||
|
||||
warnings, err := daemon.verifyContainerSettings(params.HostConfig, params.Config, false)
|
||||
if err != nil {
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
|
||||
// TODO: @jhowardmsft LCOW support - at a later point, can remove the hard-coding
|
||||
// to force the platform to be linux.
|
||||
// Default the platform if not supplied
|
||||
if params.Platform == "" {
|
||||
params.Platform = runtime.GOOS
|
||||
}
|
||||
if system.LCOWSupported() {
|
||||
params.Platform = "linux"
|
||||
}
|
||||
|
||||
err = daemon.verifyNetworkingConfig(params.NetworkingConfig)
|
||||
warnings, err := daemon.verifyContainerSettings(params.Platform, params.HostConfig, params.Config, false)
|
||||
if err != nil {
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, validationError{err}
|
||||
}
|
||||
|
||||
err = verifyNetworkingConfig(params.NetworkingConfig)
|
||||
if err != nil {
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, validationError{err}
|
||||
}
|
||||
|
||||
if params.HostConfig == nil {
|
||||
@ -55,12 +64,12 @@ func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, manage
|
||||
}
|
||||
err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares)
|
||||
if err != nil {
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, validationError{err}
|
||||
}
|
||||
|
||||
container, err := daemon.create(params, managed)
|
||||
if err != nil {
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, daemon.imageNotExistToErrcode(err)
|
||||
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
|
||||
}
|
||||
containerActions.WithValues("create").UpdateSince(start)
|
||||
|
||||
@ -76,16 +85,6 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
|
||||
err error
|
||||
)
|
||||
|
||||
// TODO: @jhowardmsft LCOW support - at a later point, can remove the hard-coding
|
||||
// to force the platform to be linux.
|
||||
// Default the platform if not supplied
|
||||
if params.Platform == "" {
|
||||
params.Platform = runtime.GOOS
|
||||
}
|
||||
if system.LCOWSupported() {
|
||||
params.Platform = "linux"
|
||||
}
|
||||
|
||||
if params.Config.Image != "" {
|
||||
img, err = daemon.GetImage(params.Config.Image)
|
||||
if err != nil {
|
||||
@ -113,11 +112,11 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
|
||||
}
|
||||
|
||||
if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
|
||||
if err := daemon.mergeAndVerifyLogConfig(¶ms.HostConfig.LogConfig); err != nil {
|
||||
return nil, err
|
||||
return nil, validationError{err}
|
||||
}
|
||||
|
||||
if container, err = daemon.newContainer(params.Name, params.Platform, params.Config, params.HostConfig, imgID, managed); err != nil {
|
||||
@ -137,9 +136,26 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
|
||||
|
||||
container.HostConfig.StorageOpt = params.HostConfig.StorageOpt
|
||||
|
||||
// Fixes: https://github.com/moby/moby/issues/34074 and
|
||||
// https://github.com/docker/for-win/issues/999.
|
||||
// Merge the daemon's storage options if they aren't already present. We only
|
||||
// do this on Windows as there's no effective sandbox size limit other than
|
||||
// physical on Linux.
|
||||
if runtime.GOOS == "windows" {
|
||||
if container.HostConfig.StorageOpt == nil {
|
||||
container.HostConfig.StorageOpt = make(map[string]string)
|
||||
}
|
||||
for _, v := range daemon.configStore.GraphOptions {
|
||||
opt := strings.SplitN(v, "=", 2)
|
||||
if _, ok := container.HostConfig.StorageOpt[opt[0]]; !ok {
|
||||
container.HostConfig.StorageOpt[opt[0]] = opt[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set RWLayer for container after mount labels have been set
|
||||
if err := daemon.setRWLayer(container); err != nil {
|
||||
return nil, err
|
||||
return nil, systemError{err}
|
||||
}
|
||||
|
||||
rootIDs := daemon.idMappings.RootPair()
|
||||
@ -295,7 +311,7 @@ func (daemon *Daemon) mergeAndVerifyConfig(config *containertypes.Config, img *i
|
||||
|
||||
// Checks if the client set configurations for more than one network while creating a container
|
||||
// Also checks if the IPAMConfig is valid
|
||||
func (daemon *Daemon) verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
|
||||
func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
|
||||
if nwConfig == nil || len(nwConfig.EndpointsConfig) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -303,14 +319,14 @@ func (daemon *Daemon) verifyNetworkingConfig(nwConfig *networktypes.NetworkingCo
|
||||
for _, v := range nwConfig.EndpointsConfig {
|
||||
if v != nil && v.IPAMConfig != nil {
|
||||
if v.IPAMConfig.IPv4Address != "" && net.ParseIP(v.IPAMConfig.IPv4Address).To4() == nil {
|
||||
return apierrors.NewBadRequestError(fmt.Errorf("invalid IPv4 address: %s", v.IPAMConfig.IPv4Address))
|
||||
return errors.Errorf("invalid IPv4 address: %s", v.IPAMConfig.IPv4Address)
|
||||
}
|
||||
if v.IPAMConfig.IPv6Address != "" {
|
||||
n := net.ParseIP(v.IPAMConfig.IPv6Address)
|
||||
// if the address is an invalid network address (ParseIP == nil) or if it is
|
||||
// an IPv4 address (To4() != nil), then it is an invalid IPv6 address
|
||||
if n == nil || n.To4() != nil {
|
||||
return apierrors.NewBadRequestError(fmt.Errorf("invalid IPv6 address: %s", v.IPAMConfig.IPv6Address))
|
||||
return errors.Errorf("invalid IPv6 address: %s", v.IPAMConfig.IPv6Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -321,6 +337,5 @@ func (daemon *Daemon) verifyNetworkingConfig(nwConfig *networktypes.NetworkingCo
|
||||
for k := range nwConfig.EndpointsConfig {
|
||||
l = append(l, k)
|
||||
}
|
||||
err := fmt.Errorf("Container cannot be connected to network endpoints: %s", strings.Join(l, ", "))
|
||||
return apierrors.NewBadRequestError(err)
|
||||
return errors.Errorf("Container cannot be connected to network endpoints: %s", strings.Join(l, ", "))
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/container"
|
||||
@ -11,9 +12,19 @@ import (
|
||||
|
||||
// createContainerPlatformSpecificSettings performs platform specific container create functionality
|
||||
func (daemon *Daemon) createContainerPlatformSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error {
|
||||
// Make sure the host config has the default daemon isolation if not specified by caller.
|
||||
if containertypes.Isolation.IsDefault(containertypes.Isolation(hostConfig.Isolation)) {
|
||||
hostConfig.Isolation = daemon.defaultIsolation
|
||||
|
||||
if container.Platform == runtime.GOOS {
|
||||
// Make sure the host config has the default daemon isolation if not specified by caller.
|
||||
if containertypes.Isolation.IsDefault(containertypes.Isolation(hostConfig.Isolation)) {
|
||||
hostConfig.Isolation = daemon.defaultIsolation
|
||||
}
|
||||
} else {
|
||||
// LCOW must be a Hyper-V container as you can't run a shared kernel when one
|
||||
// is a Windows kernel, the other is a Linux kernel.
|
||||
if containertypes.Isolation.IsProcess(containertypes.Isolation(hostConfig.Isolation)) {
|
||||
return fmt.Errorf("process isolation is invalid for Linux containers on Windows")
|
||||
}
|
||||
hostConfig.Isolation = "hyperv"
|
||||
}
|
||||
|
||||
for spec := range config.Volumes {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user