Skip to content

Commit

Permalink
Add OCI-Chunk-Min-Bytes header
Browse files Browse the repository at this point in the history
Signed-off-by: Brandon Mitchell <git@bmitch.net>
  • Loading branch information
sudo-bmitch committed Mar 15, 2023
1 parent f8afb4b commit 5a465f1
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 13 deletions.
31 changes: 28 additions & 3 deletions conformance/02_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net/http"

"strconv"

"github.com/bloodorangeio/reggie"
g "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -157,6 +159,16 @@ var test02Push = func() {
location := resp.Header().Get("Location")
Expect(location).ToNot(BeEmpty())

// rebuild chunked blob if min size is above our chunk size
minSizeStr := resp.Header().Get("OCI-Chunk-Min-Bytes")
if minSizeStr != "" {
minSize, err := strconv.Atoi(minSizeStr)
Expect(err).To(BeNil())
if minSize > len(testBlobBChunk1) {
setupChunkedBlob(minSize*2 - 2)
}
}

req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()).
SetHeader("Content-Type", "application/octet-stream").
SetHeader("Content-Length", testBlobBChunk2Length).
Expand Down Expand Up @@ -187,18 +199,31 @@ var test02Push = func() {
lastResponse = resp
})

g.Specify("PUT request with final chunk should return 201", func() {
g.Specify("PATCH request with second chunk should return 202", func() {
SkipIfDisabled(push)
req := client.NewRequest(reggie.PUT, lastResponse.GetRelativeLocation()).
req := client.NewRequest(reggie.PATCH, lastResponse.GetRelativeLocation()).
SetHeader("Content-Length", testBlobBChunk2Length).
SetHeader("Content-Range", testBlobBChunk2Range).
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", testBlobBDigest).
SetBody(testBlobBChunk2)
resp, err := client.Do(req)
Expect(err).To(BeNil())
location := resp.Header().Get("Location")
Expect(location).ToNot(BeEmpty())
Expect(resp.StatusCode()).To(Equal(http.StatusAccepted))
lastResponse = resp
})

g.Specify("PUT request with digest should return 201", func() {
SkipIfDisabled(push)
req := client.NewRequest(reggie.PUT, lastResponse.GetRelativeLocation()).
SetHeader("Content-Length", "0").
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", testBlobBDigest)
resp, err := client.Do(req)
Expect(err).To(BeNil())
location := resp.Header().Get("Location")
Expect(location).ToNot(BeEmpty())
Expect(resp.StatusCode()).To(Equal(http.StatusCreated))
})
})
Expand Down
41 changes: 31 additions & 10 deletions conformance/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"log"
"math/big"
mathrand "math/rand"
"os"
"strconv"

Expand Down Expand Up @@ -133,12 +134,14 @@ var (
automaticCrossmountEnabled bool
configs []TestBlob
manifests []TestBlob
seed int64
Version = "unknown"
)

func init() {
var err error

seed = g.GinkgoRandomSeed()
hostname := os.Getenv(envVarRootURL)
namespace := os.Getenv(envVarNamespace)
username := os.Getenv(envVarUsername)
Expand Down Expand Up @@ -274,18 +277,12 @@ func init() {
nonexistentManifest = ".INVALID_MANIFEST_NAME"
invalidManifestContent = []byte("blablabla")

testBlobA = []byte("NBA Jam on my NBA toast")
dig, blob := randomBlob(42, seed+1)
testBlobA = blob
testBlobALength = strconv.Itoa(len(testBlobA))
testBlobADigest = godigest.FromBytes(testBlobA).String()
testBlobADigest = dig.String()

testBlobB = []byte("Hello, how are you today?")
testBlobBDigest = godigest.FromBytes(testBlobB).String()
testBlobBChunk1 = testBlobB[:3]
testBlobBChunk1Length = strconv.Itoa(len(testBlobBChunk1))
testBlobBChunk1Range = fmt.Sprintf("0-%d", len(testBlobBChunk1)-1)
testBlobBChunk2 = testBlobB[3:]
testBlobBChunk2Length = strconv.Itoa(len(testBlobBChunk2))
testBlobBChunk2Range = fmt.Sprintf("%d-%d", len(testBlobBChunk1), len(testBlobB)-1)
setupChunkedBlob(42)

dummyDigest = godigest.FromString("hello world").String()

Expand Down Expand Up @@ -404,3 +401,27 @@ func randomString(n int) string {
}
return string(ret)
}

// randomBlob outputs a reproducible random blob (based on the seed) for testing
func randomBlob(size int, seed int64) (godigest.Digest, []byte) {
r := mathrand.New(mathrand.NewSource(seed))
b := make([]byte, size)
if n, err := r.Read(b); err != nil {
panic(err)
} else if n != size {
panic("unable to read enough bytes")
}
return godigest.FromBytes(b), b
}

func setupChunkedBlob(size int) {
dig, blob := randomBlob(size, seed+2)
testBlobB = blob
testBlobBDigest = dig.String()
testBlobBChunk1 = testBlobB[:size/2+1]
testBlobBChunk1Length = strconv.Itoa(len(testBlobBChunk1))
testBlobBChunk1Range = fmt.Sprintf("0-%d", len(testBlobBChunk1)-1)
testBlobBChunk2 = testBlobB[size/2+1:]
testBlobBChunk2Length = strconv.Itoa(len(testBlobBChunk2))
testBlobBChunk2Range = fmt.Sprintf("%d-%d", len(testBlobBChunk1), len(testBlobB)-1)
}
7 changes: 7 additions & 0 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ The process remains unchanged for chunked upload, except that the post request M
Content-Length: 0
```

If the registry has a minimum chunk size, the response SHOULD include the following header, where `<size>` is the size in bytes:

```
OCI-Chunk-Min-Bytes: <size>
```

Please reference the above section for restrictions on the `<location>`.

---
Expand All @@ -347,6 +353,7 @@ It MUST match the following regular expression:
```

The `<length>` is the content-length, in bytes, of the current chunk.
If the registry provides a `OCI-Chunk-Min-Bytes` header, the size of each chunk, except for the final chunk, SHOULD be greater or equal to that value.

Each successful chunk upload MUST have a `202 Accepted` response code, and MUST have the following header:

Expand Down

0 comments on commit 5a465f1

Please sign in to comment.