Skip to content

Commit 55ebd32

Browse files
authored
Add cross-repository mounting section to spec/test (#204)
* Add cross-repository mounting section to spec/test * New test in 02_push_test.go * Section added to spec.md detailing cross-repository mounting * Section added to conformance/README.md on new configuration options for cross-repository mounting Signed-off-by: Peter Engelbert <[email protected]> * Require cross-mounting Signed-off-by: Peter Engelbert <[email protected]> * Address PR comments Signed-off-by: Peter Engelbert <[email protected]>
1 parent 703802f commit 55ebd32

File tree

4 files changed

+94
-20
lines changed

4 files changed

+94
-20
lines changed

conformance/02_push_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,43 @@ var test02Push = func() {
195195
})
196196
})
197197

198+
g.Context("Cross-Repository Blob Mount", func() {
199+
g.Specify("POST request to mount another repository's blob should return 201 or 202", func() {
200+
SkipIfDisabled(push)
201+
req := client.NewRequest(reggie.POST, "/v2/<name>/blobs/uploads/",
202+
reggie.WithName(crossmountNamespace)).
203+
SetQueryParam("mount", testBlobADigest).
204+
SetQueryParam("from", client.Config.DefaultName)
205+
resp, err := client.Do(req)
206+
Expect(err).To(BeNil())
207+
Expect(resp.StatusCode()).To(SatisfyAny(
208+
Equal(http.StatusCreated),
209+
Equal(http.StatusAccepted),
210+
))
211+
Expect(resp.GetRelativeLocation()).To(ContainSubstring(crossmountNamespace))
212+
213+
lastResponse = resp
214+
})
215+
216+
g.Specify("GET request to test digest within cross-mount namespace should return 200", func() {
217+
SkipIfDisabled(push)
218+
RunOnlyIf(lastResponse.StatusCode() == http.StatusCreated)
219+
220+
req := client.NewRequest(reggie.GET, lastResponse.GetRelativeLocation())
221+
resp, err := client.Do(req)
222+
Expect(err).To(BeNil())
223+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
224+
})
225+
226+
g.Specify("Cross-mounting of nonexistent blob should yield session id", func() {
227+
SkipIfDisabled(push)
228+
RunOnlyIf(lastResponse.StatusCode() == http.StatusAccepted)
229+
230+
loc := lastResponse.GetRelativeLocation()
231+
Expect(loc).To(ContainSubstring("/blobs/uploads/"))
232+
})
233+
})
234+
198235
g.Context("Manifest Upload", func() {
199236
g.Specify("GET nonexistent manifest should return 404", func() {
200237
SkipIfDisabled(push)

conformance/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Next, set environment variables with your registry details:
1818
# Registry details
1919
export OCI_ROOT_URL="https://r.myreg.io"
2020
export OCI_NAMESPACE="myorg/myrepo"
21+
export OCI_CROSSMOUNT_NAMESPACE="myorg/other"
2122
export OCI_USERNAME="myuser"
2223
export OCI_PASSWORD="mypass"
2324
@@ -105,6 +106,15 @@ environment:
105106
OCI_SKIP_EMPTY_LAYER_PUSH_TEST=1
106107
```
107108

109+
The test suite will need access to a second namespace. This namespace is used to check support for cross-repository mounting
110+
of blobs, and may need to be configured on the server-side in advance. It is specified by setting the following in
111+
the environment:
112+
113+
```
114+
# The destination repository for cross-repository mounting:
115+
OCI_CROSSMOUNT_NAMESPACE="myorg/other"
116+
```
117+
108118
##### Content Discovery
109119

110120
The Content Discovery tests validate that the contents of a registry can be discovered.

conformance/setup.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ type (
2323
)
2424

2525
const (
26+
pull = 1 << iota
27+
push
28+
contentDiscovery
29+
contentManagement
30+
2631
BLOB_UNKNOWN = iota
2732
BLOB_UPLOAD_INVALID
2833
BLOB_UPLOAD_UNKNOWN
@@ -57,6 +62,7 @@ const (
5762
envVarHideSkippedWorkflows = "OCI_HIDE_SKIPPED_WORKFLOWS"
5863
envVarAuthScope = "OCI_AUTH_SCOPE"
5964
envVarDeleteManifestBeforeBlobs = "OCI_DELETE_MANIFEST_BEFORE_BLOBS"
65+
envVarCrossmountNamespace = "OCI_CROSSMOUNT_NAMESPACE"
6066

6167
emptyLayerTestTag = "emptylayer"
6268
testTagName = "tagtest0"
@@ -66,11 +72,6 @@ const (
6672
titleContentDiscovery = "Content Discovery"
6773
titleContentManagement = "Content Management"
6874

69-
pull = 1 << iota
70-
push
71-
contentDiscovery
72-
contentManagement
73-
7475
// layerBase64String is a base64 encoding of a simple tarball, obtained like this:
7576
// $ echo 'you bothered to find out what was in here. Congratulations!' > test.txt
7677
// $ tar czvf test.tar.gz test.txt
@@ -103,6 +104,7 @@ var (
103104
client *reggie.Client
104105
configBlobContent []byte
105106
configBlobContentLength string
107+
crossmountNamespace string
106108
dummyDigest string
107109
errorCodes []string
108110
invalidManifestContent []byte
@@ -111,8 +113,8 @@ var (
111113
layerBlobContentLength string
112114
manifestContent []byte
113115
manifestDigest string
114-
emptyLayerManifestContent []byte
115116
emptyLayerManifestDigest string
117+
emptyLayerManifestContent []byte
116118
nonexistentManifest string
117119
reportJUnitFilename string
118120
reportHTMLFilename string
@@ -136,6 +138,7 @@ func init() {
136138
username := os.Getenv(envVarUsername)
137139
password := os.Getenv(envVarPassword)
138140
authScope := os.Getenv(envVarAuthScope)
141+
crossmountNamespace = os.Getenv(envVarCrossmountNamespace)
139142

140143
debug, _ := strconv.ParseBool(os.Getenv(envVarDebug))
141144

spec.md

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,29 @@ Location: <blob-location>
304304
Here, `<blob-location>` is a pullable blob URL.
305305

306306

307+
##### Mounting a blob from another repository
308+
If a necessary blob exists already in another repository, it can be mounted into a different repository via a `POST`
309+
request in the following format:
310+
311+
`/v2/<name>/blobs/uploads/?mount=<digest>&from=<other_namespace>` <sup>[end-11](#endpoints)</sup>.
312+
313+
In this case, `<name>` is the namespace to which the blob will be mounted. `<digest>` is the digest of the blob to mount,
314+
and `<other_namespace>` is the namespace from which the blob should be mounted.
315+
316+
The response to a successful mount MUST be `201 Created`, and MUST contain the following header:
317+
```
318+
Location: <blob-location>
319+
```
320+
321+
The digest contained in the `Location` header MAY be different from that of the blob that was mounted. As such, a client
322+
SHOULD use the digest found in the path from this header and SHOULD NOT use the digest of the blob that was mounted.
323+
324+
The response to an unsuccessful mount MUST be `202 Accepted`, and be handled in the same way as a `POST` request to
325+
`/v2/<name>/blobs/uploads/`<sup>[end-4a](#endpoints)</sup>. That is, it MUST contain the following header, in the following format:
326+
```
327+
Location: /v2/<name>/blobs/uploads/<session-id>
328+
```
329+
307330
##### Pushing Manifests
308331

309332
To push a manifest, perform a `PUT` request to a path in the following format, and with the following headers
@@ -414,20 +437,21 @@ of this specification.
414437

415438
#### Endpoints
416439

417-
| ID | Method | API endpoint | Accepted Successful Response Codes | Accepted Failure Response Codes |
418-
| ------ | -------- | ------------------------------------------------------ | ---------------------------------- | ------------------------------- |
419-
| end-1 | `GET` | `/v2/` | `200` | `404`/`401` |
420-
| end-2 | `GET` | `/v2/<name>/blobs/<digest>` | `200` | `404` |
421-
| end-3 | `GET` | `/v2/<name>/manifests/<reference>` | `200` | `404` |
422-
| end-4a | `POST` | `/v2/<name>/blobs/uploads/` | `202` | `404` |
423-
| end-4b | `POST` | `/v2/<name>/blobs/uploads/?digest=<digest>` | `201`/`202` | `404`/`400` |
424-
| end-5 | `PATCH` | `/v2/<name>/blobs/uploads/<reference>` | `202` | `404`/`416` |
425-
| end-6 | `PUT` | `/v2/<name>/blobs/uploads/<reference>?digest=<digest>` | `201` | `404`/`400` |
426-
| end-7 | `PUT` | `/v2/<name>/manifests/<reference>` | `201` | `404` |
427-
| end-8a | `GET` | `/v2/<name>/tags/list` | `200` | `404` |
428-
| end-8b | `GET` | `/v2/<name>/tags/list?n=<integer>&last=<integer>` | `200` | `404` |
429-
| end-9 | `DELETE` | `/v2/<name>/manifests/<reference>` | `202` | `404`/`400`/`405` |
430-
| end-10 | `DELETE` | `/v2/<name>/blobs/<digest>` | `202` | `404`/`405` |
440+
| ID | Method | API endpoint | Accepted Successful Response Codes | Accepted Failure Response Codes |
441+
| ------ | -------- | ----------------------------------------------------------------- | ---------------------------------- | ------------------------------- |
442+
| end-1 | `GET` | `/v2/` | `200` | `404`/`401` |
443+
| end-2 | `GET` | `/v2/<name>/blobs/<digest>` | `200` | `404` |
444+
| end-3 | `GET` | `/v2/<name>/manifests/<reference>` | `200` | `404` |
445+
| end-4a | `POST` | `/v2/<name>/blobs/uploads/` | `202` | `404` |
446+
| end-4b | `POST` | `/v2/<name>/blobs/uploads/?digest=<digest>` | `201`/`202` | `404`/`400` |
447+
| end-5 | `PATCH` | `/v2/<name>/blobs/uploads/<reference>` | `202` | `404`/`416` |
448+
| end-6 | `PUT` | `/v2/<name>/blobs/uploads/<reference>?digest=<digest>` | `201` | `404`/`400` |
449+
| end-7 | `PUT` | `/v2/<name>/manifests/<reference>` | `201` | `404` |
450+
| end-8a | `GET` | `/v2/<name>/tags/list` | `200` | `404` |
451+
| end-8b | `GET` | `/v2/<name>/tags/list?n=<integer>&last=<integer>` | `200` | `404` |
452+
| end-9 | `DELETE` | `/v2/<name>/manifests/<reference>` | `202` | `404`/`400`/`405` |
453+
| end-10 | `DELETE` | `/v2/<name>/blobs/<digest>` | `202` | `404`/`405` |
454+
| end-11 | `POST` | `/v2/<name>/blobs/uploads/?mount=<digest>&from=<other_namespace>` | `201` | `404` |
431455

432456
#### Error Codes
433457

0 commit comments

Comments
 (0)