Merge pull request #501 from RichardScothern/release/2.0.1
Release/2.0.1
This commit is contained in:
commit
1341222284
9 changed files with 440 additions and 210 deletions
|
@ -1,125 +0,0 @@
|
|||
Docker-Registry Configuration
|
||||
=============================
|
||||
|
||||
This document describes the registry configuration model and how to specify a custom configuration with a configuration file and/or environment variables.
|
||||
|
||||
Semantic-ish Versioning
|
||||
-----------------------
|
||||
|
||||
The configuration file is designed with versioning in mind, such that most upgrades will not require a change in configuration files, and such that configuration files can be "upgraded" from one version to another.
|
||||
|
||||
The version is specified as a string of the form `MajorVersion.MinorVersion`, where MajorVersion and MinorVersion are both non-negative integer values. Much like [semantic versioning](http://semver.org/), minor version increases denote inherently backwards-compatible changes, such as the addition of optional fields, whereas major version increases denote a restructuring, such as renaming fields or adding required fields. Because of the explicit version definition in the configuration file, it should be possible to parse old configuration files and port them to the current configuration version, although this is not guaranteed for all future versions.
|
||||
|
||||
File Structure (as of Version 0.1)
|
||||
------------------------------------
|
||||
|
||||
The configuration structure is defined by the `Configuration` struct in `configuration.go`, and is best described by the following two examples:
|
||||
|
||||
```yaml
|
||||
version: 0.1
|
||||
loglevel: info
|
||||
storage:
|
||||
s3:
|
||||
region: us-east-1
|
||||
bucket: my-bucket
|
||||
rootdirectory: /registry
|
||||
encrypt: true
|
||||
secure: false
|
||||
accesskey: SAMPLEACCESSKEY
|
||||
secretkey: SUPERSECRET
|
||||
host: ~
|
||||
port: ~
|
||||
auth:
|
||||
silly:
|
||||
realm: test-realm
|
||||
service: my-service
|
||||
reporting:
|
||||
bugsnag:
|
||||
apikey: mybugsnagapikey
|
||||
releasestage: development
|
||||
newrelic:
|
||||
licensekey: mynewreliclicensekey
|
||||
name: docker-distribution
|
||||
http:
|
||||
addr: 0.0.0.0:5000
|
||||
secret: mytokensecret
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: 0.1
|
||||
loglevel: debug
|
||||
storage: inmemory
|
||||
```
|
||||
|
||||
### version
|
||||
The version is expected to remain a top-level field, as to allow for a consistent version check before parsing the remainder of the configuration file.
|
||||
|
||||
### loglevel
|
||||
This specifies the log level of the registry.
|
||||
|
||||
Supported values:
|
||||
* `error`
|
||||
* `warn`
|
||||
* `info`
|
||||
* `debug`
|
||||
|
||||
### storage
|
||||
This specifies the storage driver, and may be provided either as a string (only the driver type) or as a driver name with a parameters map, as seen in the first example above.
|
||||
|
||||
The parameters map will be passed into the factory constructor of the given storage driver type.
|
||||
|
||||
### auth
|
||||
This specifies the authorization method the registry will use, and is provided as an auth type with a parameters map.
|
||||
|
||||
The parameters map will be passed into the factory constructor of the given auth type.
|
||||
|
||||
### reporting
|
||||
This specifies metrics/error reporting systems which the registry will forward information about stats/errors to. There are currently two supported systems, which are documented below.
|
||||
|
||||
#### bugsnag
|
||||
Reports http errors and panics to [bugsnag](https://bugsnag.com).
|
||||
|
||||
##### apikey
|
||||
(Required for bugsnag use) Specifies the bugnsag API Key for authenticating to your account.
|
||||
|
||||
##### releasestage
|
||||
(Optional) Tracks the stage at which the registry is deployed. For example: "production", "staging", "development".
|
||||
|
||||
##### endpoint
|
||||
(Optional) Used for specifying an enterprise bugsnag endpoint other than https://bugsnag.com.
|
||||
|
||||
#### newrelic
|
||||
Reports heap, goroutine, and http stats to [NewRelic](https://newrelic.com).
|
||||
|
||||
##### licensekey
|
||||
(Required for newrelic use) Specifies the NewRelic License Key for authenticating to your account.
|
||||
|
||||
##### name
|
||||
(Optional) Specifies the component name that is displayed in the NewRelic panel.
|
||||
|
||||
### http
|
||||
This is used for HTTP transport-specific configuration options.
|
||||
|
||||
#### addr
|
||||
Specifies the bind address for the registry instance. Example: 0.0.0.0:5000
|
||||
|
||||
#### secret
|
||||
Specifies the secret key with which query-string HMAC tokens are generated.
|
||||
|
||||
### Notes
|
||||
|
||||
All keys in the configuration file **must** be provided as a string of lowercase letters and numbers only, and values must be string-like (booleans and numerical values are fine to parse as strings).
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
|
||||
To support the workflow of running a docker registry from a standard container without having to modify configuration files, the registry configuration also supports environment variables for overriding fields.
|
||||
|
||||
Any configuration field other than version can be replaced by providing an environment variable of the following form: `REGISTRY_<uppercase key>[_<uppercase key>]...`.
|
||||
|
||||
For example, to change the loglevel to `error`, one can provide `REGISTRY_LOGLEVEL=error`, and to change the s3 storage driver's region parameter to `us-west-1`, one can provide `REGISTRY_STORAGE_S3_LOGLEVEL=us-west-1`.
|
||||
|
||||
### Notes
|
||||
If an environment variable changes a map value into a string, such as replacing the storage driver type with `REGISTRY_STORAGE=filesystem`, then all sub-fields will be erased. As such, specifying the storage type in the environment will remove all parameters related to the old storage configuration.
|
||||
|
||||
By restricting all keys in the configuration file to lowercase letters and numbers, we can avoid any potential environment variable mapping ambiguity.
|
|
@ -44,8 +44,8 @@
|
|||
ErrorLog ${APACHE_LOG_DIR}/registry_error_ssl_log
|
||||
CustomLog ${APACHE_LOG_DIR}/registry_access_ssl_log combined env=!dontlog
|
||||
|
||||
Header set Host "registry.example.com"
|
||||
Header set "Docker-Distribution-Api-Version" "registry/2.0"
|
||||
Header always set "Docker-Distribution-Api-Version" "registry/2.0"
|
||||
Header onsuccess set "Docker-Distribution-Api-Version" "registry/2.0"
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
|
||||
ProxyRequests off
|
||||
|
|
|
@ -163,6 +163,12 @@ REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry/test
|
|||
This variable overrides the `/tmp/registry` value to the `/tmp/registry/test`
|
||||
directory.
|
||||
|
||||
>**Note**: If an environment variable changes a map value into a string, such
|
||||
>as replacing the storage driver type with `REGISTRY_STORAGE=filesystem`, then
|
||||
>all sub-fields will be erased. As such, specifying the storage type in the
|
||||
>environment will remove all parameters related to the old storage
|
||||
>configuration.
|
||||
|
||||
|
||||
## version
|
||||
|
||||
|
|
200
docs/spec/api.md
200
docs/spec/api.md
|
@ -117,12 +117,24 @@ specification to correspond with the versions enumerated here.
|
|||
|
||||
<dl>
|
||||
<dt>2.0.1</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>Added capability of doing streaming upload to PATCH blob upload.</li>
|
||||
<li>Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.</li>
|
||||
<li>Removed `416 Requested Range Not Satisfiable` response status from PUT blob upload.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>2.0.0</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>Added support for immutable manifest references in manifest endpoints.</li>
|
||||
<li>Deleting a manifest by tag has been deprecated.</li>
|
||||
<li>Specified `Docker-Content-Digest` header for appropriate entities.</li>
|
||||
<li>Added error code for unsupported operations.</li>
|
||||
<li>Added capability of doing streaming upload to PATCH blob upload.</li>
|
||||
<li>Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.</li>
|
||||
<li>Removed 416 return code from PUT blob upload.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
|
@ -224,6 +236,11 @@ If `404 Not Found` response status, or other unexpected status, is returned,
|
|||
the client should proceed with the assumption that the registry does not
|
||||
implement V2 of the API.
|
||||
|
||||
When a `200 OK` or `401 Unauthorized` response is returned, the
|
||||
"Docker-Distribution-API-Version" header should be set to "registry/2.0".
|
||||
Clients may require this header value to determine if the endpoint serves this
|
||||
API. When this header is omitted, clients may fallback to an older API version.
|
||||
|
||||
### Pulling An Image
|
||||
|
||||
An "image" is a combination of a JSON manifest and individual layer files. The
|
||||
|
@ -2175,6 +2192,158 @@ The error codes that may be included in the response body are enumerated below:
|
|||
Upload a chunk of data for the specified upload.
|
||||
|
||||
|
||||
##### Stream upload
|
||||
|
||||
```
|
||||
PATCH /v2/<name>/blobs/uploads/<uuid>
|
||||
Host: <registry host>
|
||||
Authorization: <scheme> <token>
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<binary data>
|
||||
```
|
||||
|
||||
Upload a stream of data to upload without completing the upload.
|
||||
|
||||
|
||||
The following parameters should be specified on the request:
|
||||
|
||||
|Name|Kind|Description|
|
||||
|----|----|-----------|
|
||||
|`Host`|header|Standard HTTP Host Header. Should be set to the registry host.|
|
||||
|`Authorization`|header|An RFC7235 compliant authorization header.|
|
||||
|`name`|path|Name of the target repository.|
|
||||
|`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.|
|
||||
|
||||
|
||||
|
||||
|
||||
###### On Success: Data Accepted
|
||||
|
||||
```
|
||||
204 No Content
|
||||
Location: /v2/<name>/blobs/uploads/<uuid>
|
||||
Range: 0-<offset>
|
||||
Content-Length: 0
|
||||
Docker-Upload-UUID: <uuid>
|
||||
```
|
||||
|
||||
The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.
|
||||
|
||||
The following headers will be returned with the response:
|
||||
|
||||
|Name|Description|
|
||||
|----|-----------|
|
||||
|`Location`|The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.|
|
||||
|`Range`|Range indicating the current progress of the upload.|
|
||||
|`Content-Length`|The `Content-Length` header must be zero and the body must be empty.|
|
||||
|`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.|
|
||||
|
||||
|
||||
|
||||
|
||||
###### On Failure: Bad Request
|
||||
|
||||
```
|
||||
400 Bad Request
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"errors:" [
|
||||
{
|
||||
"code": <error code>,
|
||||
"message": "<error message>",
|
||||
"detail": ...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
There was an error processing the upload and it must be restarted.
|
||||
|
||||
|
||||
|
||||
The error codes that may be included in the response body are enumerated below:
|
||||
|
||||
|Code|Message|Description|
|
||||
-------|----|------|------------
|
||||
| `DIGEST_INVALID` | provided digest did not match uploaded content | When a blob is uploaded, the registry will check that the content matches the digest provided by the client. The error may include a detail structure with the key "digest", including the invalid digest string. This error may also be returned when a manifest includes an invalid layer digest. |
|
||||
| `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. |
|
||||
| `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. |
|
||||
|
||||
|
||||
|
||||
###### On Failure: Unauthorized
|
||||
|
||||
```
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: <scheme> realm="<realm>", ..."
|
||||
Content-Length: <length>
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"errors:" [
|
||||
{
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "access to the requested resource is not authorized",
|
||||
"detail": ...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The client does not have access to push to the repository.
|
||||
|
||||
The following headers will be returned on the response:
|
||||
|
||||
|Name|Description|
|
||||
|----|-----------|
|
||||
|`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.|
|
||||
|`Content-Length`|Length of the JSON error response body.|
|
||||
|
||||
|
||||
|
||||
The error codes that may be included in the response body are enumerated below:
|
||||
|
||||
|Code|Message|Description|
|
||||
-------|----|------|------------
|
||||
| `UNAUTHORIZED` | access to the requested resource is not authorized | The access controller denied access for the operation on a resource. Often this will be accompanied by a 401 Unauthorized response status. |
|
||||
|
||||
|
||||
|
||||
###### On Failure: Not Found
|
||||
|
||||
```
|
||||
404 Not Found
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"errors:" [
|
||||
{
|
||||
"code": <error code>,
|
||||
"message": "<error message>",
|
||||
"detail": ...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The upload is unknown to the registry. The upload must be restarted.
|
||||
|
||||
|
||||
|
||||
The error codes that may be included in the response body are enumerated below:
|
||||
|
||||
|Code|Message|Description|
|
||||
-------|----|------|------------
|
||||
| `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. |
|
||||
|
||||
|
||||
|
||||
##### Chunked upload
|
||||
|
||||
```
|
||||
PATCH /v2/<name>/blobs/uploads/<uuid>
|
||||
|
@ -2187,7 +2356,7 @@ Content-Type: application/octet-stream
|
|||
<binary chunk>
|
||||
```
|
||||
|
||||
Upload a chunk of data to specified upload without completing the upload.
|
||||
Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.
|
||||
|
||||
|
||||
The following parameters should be specified on the request:
|
||||
|
@ -2350,14 +2519,13 @@ Complete the upload specified by `uuid`, optionally appending the body as the fi
|
|||
PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
|
||||
Host: <registry host>
|
||||
Authorization: <scheme> <token>
|
||||
Content-Range: <start of range>-<end of range, inclusive>
|
||||
Content-Length: <length of chunk>
|
||||
Content-Length: <length of data>
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<binary chunk>
|
||||
<binary data>
|
||||
```
|
||||
|
||||
Complete the upload, providing the _final_ chunk of data, if necessary. This method may take a body with all the data. If the `Content-Range` header is specified, it may include the final chunk. A request without a body will just complete the upload with previously uploaded content.
|
||||
Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.
|
||||
|
||||
|
||||
The following parameters should be specified on the request:
|
||||
|
@ -2366,8 +2534,7 @@ The following parameters should be specified on the request:
|
|||
|----|----|-----------|
|
||||
|`Host`|header|Standard HTTP Host Header. Should be set to the registry host.|
|
||||
|`Authorization`|header|An RFC7235 compliant authorization header.|
|
||||
|`Content-Range`|header|Range of bytes identifying the block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header. May be omitted if no data is provided.|
|
||||
|`Content-Length`|header|Length of the chunk being uploaded, corresponding to the length of the request body. May be zero if no data is provided.|
|
||||
|`Content-Length`|header|Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.|
|
||||
|`name`|path|Name of the target repository.|
|
||||
|`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.|
|
||||
|`digest`|query|Digest of uploaded blob.|
|
||||
|
@ -2500,25 +2667,6 @@ The error codes that may be included in the response body are enumerated below:
|
|||
|
||||
|
||||
|
||||
###### On Failure: Requested Range Not Satisfiable
|
||||
|
||||
```
|
||||
416 Requested Range Not Satisfiable
|
||||
Location: /v2/<name>/blobs/uploads/<uuid>
|
||||
Range: 0-<offset>
|
||||
```
|
||||
|
||||
The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. The contents of the `Range` header may be used to resolve the condition.
|
||||
|
||||
The following headers will be returned on the response:
|
||||
|
||||
|Name|Description|
|
||||
|----|-----------|
|
||||
|`Location`|The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.|
|
||||
|`Range`|Range indicating the current progress of the upload.|
|
||||
|
||||
|
||||
|
||||
|
||||
#### DELETE Blob Upload
|
||||
|
||||
|
|
|
@ -117,12 +117,24 @@ specification to correspond with the versions enumerated here.
|
|||
|
||||
<dl>
|
||||
<dt>2.0.1</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>Added capability of doing streaming upload to PATCH blob upload.</li>
|
||||
<li>Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.</li>
|
||||
<li>Removed `416 Requested Range Not Satisfiable` response status from PUT blob upload.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>2.0.0</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>Added support for immutable manifest references in manifest endpoints.</li>
|
||||
<li>Deleting a manifest by tag has been deprecated.</li>
|
||||
<li>Specified `Docker-Content-Digest` header for appropriate entities.</li>
|
||||
<li>Added error code for unsupported operations.</li>
|
||||
<li>Added capability of doing streaming upload to PATCH blob upload.</li>
|
||||
<li>Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.</li>
|
||||
<li>Removed 416 return code from PUT blob upload.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
|
@ -224,6 +236,11 @@ If `404 Not Found` response status, or other unexpected status, is returned,
|
|||
the client should proceed with the assumption that the registry does not
|
||||
implement V2 of the API.
|
||||
|
||||
When a `200 OK` or `401 Unauthorized` response is returned, the
|
||||
"Docker-Distribution-API-Version" header should be set to "registry/2.0".
|
||||
Clients may require this header value to determine if the endpoint serves this
|
||||
API. When this header is omitted, clients may fallback to an older API version.
|
||||
|
||||
### Pulling An Image
|
||||
|
||||
An "image" is a combination of a JSON manifest and individual layer files. The
|
||||
|
|
26
docs/spec/implementations.md
Normal file
26
docs/spec/implementations.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Distribution API Implementations
|
||||
|
||||
This is a list of known implementations of the Distribution API spec.
|
||||
|
||||
## [Docker Distribution Registry](https://github.com/docker/distribution)
|
||||
|
||||
Docker distribution is the reference implementation of the distribution API
|
||||
specification. It aims to fully implement the entire specification.
|
||||
|
||||
### Releases
|
||||
#### 2.0.1 (_in development_)
|
||||
Implements API 2.0.1
|
||||
|
||||
_Known Issues_
|
||||
- No resumable push support
|
||||
- Content ranges ignored
|
||||
- Blob upload status will always return a starting range of 0
|
||||
|
||||
#### 2.0.0
|
||||
Implements API 2.0.0
|
||||
|
||||
_Known Issues_
|
||||
- No resumable push support
|
||||
- No PATCH implementation for blob upload
|
||||
- Content ranges ignored
|
||||
|
|
@ -1055,7 +1055,74 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Description: "Upload a chunk of data for the specified upload.",
|
||||
Requests: []RequestDescriptor{
|
||||
{
|
||||
Description: "Upload a chunk of data to specified upload without completing the upload.",
|
||||
Name: "Stream upload",
|
||||
Description: "Upload a stream of data to upload without completing the upload.",
|
||||
PathParameters: []ParameterDescriptor{
|
||||
nameParameterDescriptor,
|
||||
uuidParameterDescriptor,
|
||||
},
|
||||
Headers: []ParameterDescriptor{
|
||||
hostHeader,
|
||||
authHeader,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/octet-stream",
|
||||
Format: "<binary data>",
|
||||
},
|
||||
Successes: []ResponseDescriptor{
|
||||
{
|
||||
Name: "Data Accepted",
|
||||
Description: "The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.",
|
||||
StatusCode: http.StatusNoContent,
|
||||
Headers: []ParameterDescriptor{
|
||||
{
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "/v2/<name>/blobs/uploads/<uuid>",
|
||||
Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
|
||||
},
|
||||
{
|
||||
Name: "Range",
|
||||
Type: "header",
|
||||
Format: "0-<offset>",
|
||||
Description: "Range indicating the current progress of the upload.",
|
||||
},
|
||||
contentLengthZeroHeader,
|
||||
dockerUploadUUIDHeader,
|
||||
},
|
||||
},
|
||||
},
|
||||
Failures: []ResponseDescriptor{
|
||||
{
|
||||
Description: "There was an error processing the upload and it must be restarted.",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ErrorCodes: []ErrorCode{
|
||||
ErrorCodeDigestInvalid,
|
||||
ErrorCodeNameInvalid,
|
||||
ErrorCodeBlobUploadInvalid,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
unauthorizedResponsePush,
|
||||
{
|
||||
Description: "The upload is unknown to the registry. The upload must be restarted.",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ErrorCodes: []ErrorCode{
|
||||
ErrorCodeBlobUploadUnknown,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Chunked upload",
|
||||
Description: "Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.",
|
||||
PathParameters: []ParameterDescriptor{
|
||||
nameParameterDescriptor,
|
||||
uuidParameterDescriptor,
|
||||
|
@ -1143,26 +1210,15 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.",
|
||||
Requests: []RequestDescriptor{
|
||||
{
|
||||
// TODO(stevvooe): Break this down into three separate requests:
|
||||
// 1. Complete an upload where all data has already been sent.
|
||||
// 2. Complete an upload where the entire body is in the PUT.
|
||||
// 3. Complete an upload where the final, partial chunk is the body.
|
||||
|
||||
Description: "Complete the upload, providing the _final_ chunk of data, if necessary. This method may take a body with all the data. If the `Content-Range` header is specified, it may include the final chunk. A request without a body will just complete the upload with previously uploaded content.",
|
||||
Description: "Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.",
|
||||
Headers: []ParameterDescriptor{
|
||||
hostHeader,
|
||||
authHeader,
|
||||
{
|
||||
Name: "Content-Range",
|
||||
Type: "header",
|
||||
Format: "<start of range>-<end of range, inclusive>",
|
||||
Description: "Range of bytes identifying the block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header. May be omitted if no data is provided.",
|
||||
},
|
||||
{
|
||||
Name: "Content-Length",
|
||||
Type: "integer",
|
||||
Format: "<length of chunk>",
|
||||
Description: "Length of the chunk being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
|
||||
Format: "<length of data>",
|
||||
Description: "Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
|
||||
},
|
||||
},
|
||||
PathParameters: []ParameterDescriptor{
|
||||
|
@ -1181,7 +1237,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/octet-stream",
|
||||
Format: "<binary chunk>",
|
||||
Format: "<binary data>",
|
||||
},
|
||||
Successes: []ResponseDescriptor{
|
||||
{
|
||||
|
@ -1232,24 +1288,6 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. The contents of the `Range` header may be used to resolve the condition.",
|
||||
StatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
Headers: []ParameterDescriptor{
|
||||
{
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "/v2/<name>/blobs/uploads/<uuid>",
|
||||
Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
|
||||
},
|
||||
{
|
||||
Name: "Range",
|
||||
Type: "header",
|
||||
Format: "0-<offset>",
|
||||
Description: "Range indicating the current progress of the upload.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -209,6 +209,13 @@ func TestLayerAPI(t *testing.T) {
|
|||
uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName)
|
||||
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
|
||||
|
||||
// ------------------------------------------
|
||||
// Now, push just a chunk
|
||||
layerFile.Seek(0, 0)
|
||||
|
||||
uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName)
|
||||
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
|
||||
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
|
||||
// ------------------------
|
||||
// Use a head request to see if the layer exists.
|
||||
resp, err = http.Head(layerURL)
|
||||
|
@ -616,6 +623,75 @@ func pushLayer(t *testing.T, ub *v2.URLBuilder, name string, dgst digest.Digest,
|
|||
return resp.Header.Get("Location")
|
||||
}
|
||||
|
||||
func finishUpload(t *testing.T, ub *v2.URLBuilder, name string, uploadURLBase string, dgst digest.Digest) string {
|
||||
resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated)
|
||||
|
||||
expectedLayerURL, err := ub.BuildBlobURL(name, dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("error building expected layer url: %v", err)
|
||||
}
|
||||
|
||||
checkHeaders(t, resp, http.Header{
|
||||
"Location": []string{expectedLayerURL},
|
||||
"Content-Length": []string{"0"},
|
||||
"Docker-Content-Digest": []string{dgst.String()},
|
||||
})
|
||||
|
||||
return resp.Header.Get("Location")
|
||||
}
|
||||
|
||||
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) {
|
||||
u, err := url.Parse(uploadURLBase)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
||||
}
|
||||
|
||||
u.RawQuery = url.Values{
|
||||
"_state": u.Query()["_state"],
|
||||
}.Encode()
|
||||
|
||||
uploadURL := u.String()
|
||||
|
||||
digester := digest.NewCanonicalDigester()
|
||||
|
||||
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
return resp, digester.Digest(), err
|
||||
}
|
||||
|
||||
func pushChunk(t *testing.T, ub *v2.URLBuilder, name string, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) {
|
||||
resp, dgst, err := doPushChunk(t, uploadURLBase, body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
checkResponse(t, "putting chunk", resp, http.StatusAccepted)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error generating sha256 digest of body")
|
||||
}
|
||||
|
||||
checkHeaders(t, resp, http.Header{
|
||||
"Range": []string{fmt.Sprintf("0-%d", length-1)},
|
||||
"Content-Length": []string{"0"},
|
||||
})
|
||||
|
||||
return resp.Header.Get("Location"), dgst
|
||||
}
|
||||
|
||||
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
|
||||
if resp.StatusCode != expectedStatus {
|
||||
t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus)
|
||||
|
|
|
@ -23,11 +23,10 @@ func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
|
|||
}
|
||||
|
||||
handler := http.Handler(handlers.MethodHandler{
|
||||
"POST": http.HandlerFunc(luh.StartLayerUpload),
|
||||
"GET": http.HandlerFunc(luh.GetUploadStatus),
|
||||
"HEAD": http.HandlerFunc(luh.GetUploadStatus),
|
||||
// TODO(stevvooe): Must implement patch support.
|
||||
// "PATCH": http.HandlerFunc(luh.PutLayerChunk),
|
||||
"POST": http.HandlerFunc(luh.StartLayerUpload),
|
||||
"GET": http.HandlerFunc(luh.GetUploadStatus),
|
||||
"HEAD": http.HandlerFunc(luh.GetUploadStatus),
|
||||
"PATCH": http.HandlerFunc(luh.PatchLayerData),
|
||||
"PUT": http.HandlerFunc(luh.PutLayerUploadComplete),
|
||||
"DELETE": http.HandlerFunc(luh.CancelLayerUpload),
|
||||
})
|
||||
|
@ -133,7 +132,7 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R
|
|||
luh.Upload = upload
|
||||
defer luh.Upload.Close()
|
||||
|
||||
if err := luh.layerUploadResponse(w, r); err != nil {
|
||||
if err := luh.layerUploadResponse(w, r, true); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
||||
return
|
||||
|
@ -151,7 +150,10 @@ func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
if err := luh.layerUploadResponse(w, r); err != nil {
|
||||
// TODO(dmcgowan): Set last argument to false in layerUploadResponse when
|
||||
// resumable upload is supported. This will enable returning a non-zero
|
||||
// range for clients to begin uploading at an offset.
|
||||
if err := luh.layerUploadResponse(w, r, true); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
||||
return
|
||||
|
@ -161,11 +163,45 @@ func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Re
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// PutLayerUploadComplete takes the final request of a layer upload. The final
|
||||
// chunk may include all the layer data, the final chunk of layer data or no
|
||||
// layer data. Any data provided is received and verified. If successful, the
|
||||
// layer is linked into the blob store and 201 Created is returned with the
|
||||
// canonical url of the layer.
|
||||
// PatchLayerData writes data to an upload.
|
||||
func (luh *layerUploadHandler) PatchLayerData(w http.ResponseWriter, r *http.Request) {
|
||||
if luh.Upload == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if ct != "" && ct != "application/octet-stream" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
// TODO(dmcgowan): encode error
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): support Content-Range header to seek and write range
|
||||
|
||||
// Copy the data
|
||||
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
|
||||
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := luh.layerUploadResponse(w, r, false); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||
luh.Errors.Push(v2.ErrorCodeUnknown, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// PutLayerUploadComplete takes the final request of a layer upload. The
|
||||
// request may include all the layer data or no layer data. Any data
|
||||
// provided is received and verified. If successful, the layer is linked
|
||||
// into the blob store and 201 Created is returned with the canonical
|
||||
// url of the layer.
|
||||
func (luh *layerUploadHandler) PutLayerUploadComplete(w http.ResponseWriter, r *http.Request) {
|
||||
if luh.Upload == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
@ -190,14 +226,11 @@ func (luh *layerUploadHandler) PutLayerUploadComplete(w http.ResponseWriter, r *
|
|||
return
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Check the incoming range header here, per the
|
||||
// specification. LayerUpload should be seeked (sought?) to that position.
|
||||
|
||||
// TODO(stevvooe): Consider checking the error on this copy.
|
||||
// Theoretically, problems should be detected during verification but we
|
||||
// may miss a root cause.
|
||||
|
||||
// Read in the final chunk, if any.
|
||||
// Read in the data, if any.
|
||||
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
|
||||
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -260,13 +293,19 @@ func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.
|
|||
|
||||
// layerUploadResponse provides a standard request for uploading layers and
|
||||
// chunk responses. This sets the correct headers but the response status is
|
||||
// left to the caller.
|
||||
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error {
|
||||
// left to the caller. The fresh argument is used to ensure that new layer
|
||||
// uploads always start at a 0 offset. This allows disabling resumable push
|
||||
// by always returning a 0 offset on check status.
|
||||
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
|
||||
|
||||
offset, err := luh.Upload.Seek(0, os.SEEK_CUR)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(luh).Errorf("unable get current offset of layer upload: %v", err)
|
||||
return err
|
||||
var offset int64
|
||||
if !fresh {
|
||||
var err error
|
||||
offset, err = luh.Upload.Seek(0, os.SEEK_CUR)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(luh).Errorf("unable get current offset of layer upload: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Need a better way to manage the upload state automatically.
|
||||
|
@ -291,10 +330,15 @@ func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *htt
|
|||
return err
|
||||
}
|
||||
|
||||
endRange := offset
|
||||
if endRange > 0 {
|
||||
endRange = endRange - 1
|
||||
}
|
||||
|
||||
w.Header().Set("Docker-Upload-UUID", luh.UUID)
|
||||
w.Header().Set("Location", uploadURL)
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.Header().Set("Range", fmt.Sprintf("0-%d", luh.State.Offset))
|
||||
w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue