Every 500 response in the OCI package silently discarded the actual error, making production debugging impossible. Add slog.Error before each 500 response with the error and relevant context (repo, digest, tag, uuid). Add slog.Info for state-mutating successes (manifest push, blob upload complete, deletions). Logger is injected into the OCI Handler via constructor, falling back to slog.Default() if nil. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
267 lines
8.0 KiB
Go
267 lines
8.0 KiB
Go
package oci
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func makeManifest(configDigest string, layerDigests []string) []byte {
|
|
layers := ""
|
|
for i, d := range layerDigests {
|
|
if i > 0 {
|
|
layers += ","
|
|
}
|
|
layers += fmt.Sprintf(`{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":%q,"size":1000}`, d)
|
|
}
|
|
return []byte(fmt.Sprintf(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":%q,"size":100},"layers":[%s]}`, configDigest, layers))
|
|
}
|
|
|
|
func manifestDigest(content []byte) string {
|
|
sum := sha256.Sum256(content)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func TestManifestPushByTag(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
fdb.addGlobalBlob("sha256:config1")
|
|
fdb.addGlobalBlob("sha256:layer1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
content := makeManifest("sha256:config1", []string{"sha256:layer1"})
|
|
digest := manifestDigest(content)
|
|
|
|
req := authedRequest("PUT", "/v2/myrepo/manifests/latest", nil)
|
|
req.Body = http.NoBody
|
|
// Re-create with proper body.
|
|
req = authedPushRequest("PUT", "/v2/myrepo/manifests/latest", content)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("status: got %d, want %d, body: %s", rr.Code, http.StatusCreated, rr.Body.String())
|
|
}
|
|
|
|
dcd := rr.Header().Get("Docker-Content-Digest")
|
|
if dcd != digest {
|
|
t.Fatalf("Docker-Content-Digest: got %q, want %q", dcd, digest)
|
|
}
|
|
|
|
loc := rr.Header().Get("Location")
|
|
if loc == "" {
|
|
t.Fatal("Location header missing")
|
|
}
|
|
|
|
ct := rr.Header().Get("Content-Type")
|
|
if ct != "application/vnd.oci.image.manifest.v1+json" {
|
|
t.Fatalf("Content-Type: got %q", ct)
|
|
}
|
|
|
|
// Verify manifest was stored.
|
|
if len(fdb.pushed) != 1 {
|
|
t.Fatalf("pushed count: got %d, want 1", len(fdb.pushed))
|
|
}
|
|
if fdb.pushed[0].Tag != "latest" {
|
|
t.Fatalf("pushed tag: got %q, want %q", fdb.pushed[0].Tag, "latest")
|
|
}
|
|
}
|
|
|
|
func TestManifestPushByDigest(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
fdb.addGlobalBlob("sha256:config1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
content := makeManifest("sha256:config1", nil)
|
|
digest := manifestDigest(content)
|
|
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/"+digest, content)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("status: got %d, body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Verify no tag was set.
|
|
if len(fdb.pushed) != 1 {
|
|
t.Fatalf("pushed count: got %d", len(fdb.pushed))
|
|
}
|
|
if fdb.pushed[0].Tag != "" {
|
|
t.Fatalf("pushed tag: got %q, want empty", fdb.pushed[0].Tag)
|
|
}
|
|
}
|
|
|
|
func TestManifestPushDigestMismatch(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
wrongDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/"+wrongDigest, content)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "DIGEST_INVALID" {
|
|
t.Fatalf("error code: got %+v, want DIGEST_INVALID", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestPushMissingBlob(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
// Config blob exists but layer blob does not.
|
|
fdb.addGlobalBlob("sha256:config1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
content := makeManifest("sha256:config1", []string{"sha256:missing_layer"})
|
|
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/latest", content)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d, want %d, body: %s", rr.Code, http.StatusBadRequest, rr.Body.String())
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "MANIFEST_BLOB_UNKNOWN" {
|
|
t.Fatalf("error code: got %+v, want MANIFEST_BLOB_UNKNOWN", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestPushMalformed(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/latest", []byte("not valid json"))
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "MANIFEST_INVALID" {
|
|
t.Fatalf("error code: got %+v, want MANIFEST_INVALID", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestPushEmpty(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/latest", nil)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestManifestPushUpdatesTag(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
fdb.addGlobalBlob("sha256:config1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
// First push with tag "latest".
|
|
content1 := makeManifest("sha256:config1", nil)
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/latest", content1)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("first push status: got %d", rr.Code)
|
|
}
|
|
firstDigest := rr.Header().Get("Docker-Content-Digest")
|
|
|
|
// Second push with same tag — different content.
|
|
fdb.addGlobalBlob("sha256:config2")
|
|
content2 := makeManifest("sha256:config2", nil)
|
|
req = authedPushRequest("PUT", "/v2/myrepo/manifests/latest", content2)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr = httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("second push status: got %d", rr.Code)
|
|
}
|
|
secondDigest := rr.Header().Get("Docker-Content-Digest")
|
|
|
|
if firstDigest == secondDigest {
|
|
t.Fatal("two pushes should produce different digests")
|
|
}
|
|
|
|
// Verify that the tag was atomically moved.
|
|
if len(fdb.pushed) != 2 {
|
|
t.Fatalf("pushed count: got %d, want 2", len(fdb.pushed))
|
|
}
|
|
}
|
|
|
|
func TestManifestPushRepushIdempotent(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
fdb.addGlobalBlob("sha256:config1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil, nil)
|
|
router := testRouter(h)
|
|
|
|
content := makeManifest("sha256:config1", nil)
|
|
|
|
// Push the same manifest twice.
|
|
for i := range 2 {
|
|
req := authedPushRequest("PUT", "/v2/myrepo/manifests/latest", content)
|
|
req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("push %d status: got %d", i+1, rr.Code)
|
|
}
|
|
}
|
|
}
|