Skip to content

Commit 360069f

Browse files
committed
Use Brandon's new conformance tests
- hack `go.mod` for `ociregistry` conformance fixes - update code for conformance fixes ("hell yeah") - correct blob `Range:` implementation - implement `Referrers` - more correct errors handling (esp. translating/annotating containerd errors to ociregistry errors) - `go fix ./...` - use a cache mount in the `Dockerfile` for faster rebuilds slash test/dev cycles
1 parent ee6efd9 commit 360069f

6 files changed

Lines changed: 131 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,40 +36,23 @@ jobs:
3636
- run: docker run --rm --network=container:containerd-registry gcr.io/go-containerregistry/crane catalog localhost:5000 | grep -E '^totally-fake-never-exists$'
3737
- run: docker run --rm --network=container:containerd-registry gcr.io/go-containerregistry/crane ls localhost:5000/totally-fake-never-exists | grep -E '^true-yoloci$'
3838

39+
# TODO hacking in https://github.com/opencontainers/distribution-spec/pull/588 for Brandon's test rewrite 👀
40+
# TODO https://github.com/sudo-bmitch/distribution-spec/tree/pr-conformance-v2
3941
- name: OCI conformance tests
4042
env:
41-
# https://github.com/opencontainers/distribution-spec/tree/HEAD/conformance#readme
42-
OCI_ROOT_URL: http://localhost:5000
43-
OCI_NAMESPACE: oci-conformance/repo
44-
OCI_CROSSMOUNT_NAMESPACE: conformance/mount
45-
OCI_HIDE_SKIPPED_WORKFLOWS: 0
46-
OCI_TEST_PULL: 1
47-
OCI_TEST_PUSH: 1
48-
OCI_TEST_CONTENT_DISCOVERY: 1
49-
OCI_TEST_CONTENT_MANAGEMENT: 1
43+
OCI_CONFIGURATION: oci-conformance.yml
5044
run: |
51-
git init distribution-spec
52-
cd distribution-spec
53-
git remote add origin https://github.com/opencontainers/distribution-spec.git
54-
git fetch origin 5e57cc0a07ea002e507a65d4757e823f133fcb52: # main
55-
git checkout FETCH_HEAD
56-
57-
git config user.name 'Hack'
58-
git config user.email 'The@Planet'
59-
60-
# https://github.com/opencontainers/distribution-spec/commit/eadcef7ba0055c6893e679e47bb54fb13374fa12
61-
git fetch origin eadcef7ba0055c6893e679e47bb54fb13374fa12:
62-
git merge FETCH_HEAD
63-
64-
cd conformance
65-
commit="$(git rev-parse HEAD)"
66-
CGO_ENABLED=0 go test -c -trimpath -o oci-conformance -ldflags="-X github.com/opencontainers/distribution-spec/conformance.Version=$commit"
67-
./oci-conformance
45+
git init dist-spec
46+
git -C dist-spec remote add origin https://github.com/opencontainers/distribution-spec.git
47+
git -C dist-spec fetch origin d4010d8a469b7c89a1d0938379654a39805f77f9: # main
48+
git -C dist-spec checkout FETCH_HEAD
49+
( cd dist-spec/conformance && CGO_ENABLED=0 go build -o conformance )
50+
./dist-spec/conformance/conformance
6851
- uses: actions/upload-artifact@v7
6952
with:
7053
name: oci-conformance
7154
archive: false
72-
path: distribution-spec/conformance/report.html
55+
path: results/report.html
7356
if-no-files-found: error
7457
if: always()
7558

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
FROM --platform=$BUILDPLATFORM golang:1.25 AS build
22

33
ENV CGO_ENABLED 0
4+
ENV GOCACHE /go/cache
45

56
WORKDIR /app
67
COPY go.mod go.sum ./
@@ -10,7 +11,8 @@ COPY . .
1011
ARG TARGETOS TARGETARCH TARGETVARIANT
1112
ENV GOOS=$TARGETOS GOARCH=$TARGETARCH VARIANT=$TARGETVARIANT
1213

13-
RUN set -eux; \
14+
RUN --mount=type=cache,target=$GOCACHE \
15+
set -eux; \
1416
case "$GOARCH" in \
1517
arm) export GOARM="${VARIANT#v}" ;; \
1618
amd64) export GOAMD64="$VARIANT" ;; \

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@ require (
6262
google.golang.org/grpc v1.79.3 // indirect
6363
google.golang.org/protobuf v1.36.11 // indirect
6464
)
65+
66+
// TODO temporary hacks -- hopefully can upstream conformance fixes eventually 🙈
67+
// https://github.com/cue-labs/oci/compare/main...tianon:cuelabs-oci:conformance
68+
replace cuelabs.dev/go/oci/ociregistry => github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20260320221224-66cf54feb22c

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2-
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I=
3-
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8=
42
cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY=
53
cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
64
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
@@ -138,6 +136,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
138136
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
139137
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
140138
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
139+
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20260320221224-66cf54feb22c h1:acchZJNdZ7GKYLOegXArB+8BlzUWjqbm+1SlLnmSPoI=
140+
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20260320221224-66cf54feb22c/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8=
141141
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
142142
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
143143
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=

main.go

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package main
22

33
import (
44
"bytes"
5+
"cmp"
56
"context"
67
"encoding/json"
78
"errors"
89
"io"
10+
"iter"
911
"log"
1012
"net/http"
1113
"os"
@@ -40,7 +42,7 @@ type containerdRegistry struct {
4042
client *containerd.Client
4143
}
4244

43-
func (r containerdRegistry) Repositories(ctx context.Context, startAfter string) ociregistry.Seq[string] {
45+
func (r containerdRegistry) Repositories(ctx context.Context, startAfter string) iter.Seq2[string, error] {
4446
is := r.client.ImageService()
4547

4648
images, err := is.List(ctx)
@@ -80,7 +82,7 @@ func (r containerdRegistry) Repositories(ctx context.Context, startAfter string)
8082
return ociregistry.SliceSeq[string](names)
8183
}
8284

83-
func (r containerdRegistry) Tags(ctx context.Context, repo string, startAfter string) ociregistry.Seq[string] {
85+
func (r containerdRegistry) Tags(ctx context.Context, repo string, startAfter string) iter.Seq2[string, error] {
8486
is := r.client.ImageService()
8587

8688
images, err := is.List(ctx, "name~="+strconv.Quote("^"+regexp.QuoteMeta(repo)+":"))
@@ -117,6 +119,62 @@ func (r containerdRegistry) Tags(ctx context.Context, repo string, startAfter st
117119
return ociregistry.SliceSeq[string](tags)
118120
}
119121

122+
func (r containerdRegistry) Referrers(ctx context.Context, repo string, digest ociregistry.Digest, artifactType string) iter.Seq2[ociregistry.Descriptor, error] {
123+
yieldBreak := errors.New("break " + string(digest))
124+
return func(yield func(ociregistry.Descriptor, error) bool) {
125+
cs := r.client.ContentStore()
126+
// TODO should this be a new "background" context?
127+
err := cs.Walk(ctx, func(info content.Info) error {
128+
// TODO we should really pull "artifactType" and "mediaType" up into labels so we don't have to fetch/parse the manifest here to get them -- maybe even annotations?
129+
desc := ociregistry.Descriptor{
130+
Digest: info.Digest,
131+
Size: info.Size,
132+
}
133+
ra, err := cs.ReaderAt(ctx, desc)
134+
if err != nil {
135+
if !yield(ociregistry.Descriptor{}, err) {
136+
return yieldBreak
137+
}
138+
return nil
139+
}
140+
// wrap in a LimitedReader here to make sure we don't read an enormous amount of valid but useless JSON that DoS's us
141+
reader := io.LimitReader(content.NewReader(ra), manifestSizeLimit)
142+
fields := struct {
143+
MediaType string `json:"mediaType"`
144+
ArtifactType string `json:"artifactType"`
145+
Config struct {
146+
// for the fallback ("If the artifactType is empty or missing in the image manifest, the value of artifactType MUST be set to the config descriptor mediaType value.")
147+
MediaType string `json:"mediaType"`
148+
} `json:"config"`
149+
Annotations map[string]string `json:"annotations"`
150+
}{}
151+
if err := json.NewDecoder(reader).Decode(&fields); err != nil {
152+
if !yield(ociregistry.Descriptor{}, err) {
153+
return yieldBreak
154+
}
155+
return nil
156+
}
157+
desc.MediaType = fields.MediaType
158+
desc.ArtifactType = cmp.Or(fields.ArtifactType, fields.Config.MediaType)
159+
desc.Annotations = fields.Annotations
160+
if artifactType != "" && desc.ArtifactType != artifactType {
161+
return nil
162+
}
163+
if !yield(desc, nil) {
164+
return yieldBreak
165+
}
166+
return nil
167+
}, `labels."containerd.io/gc.bref.content.subject"==`+strconv.Quote(string(digest)))
168+
if err == yieldBreak {
169+
return
170+
}
171+
if err != nil {
172+
yield(ociregistry.Descriptor{}, err)
173+
return
174+
}
175+
}
176+
}
177+
120178
type containerdBlobReader struct {
121179
client *containerd.Client
122180
ctx context.Context
@@ -130,7 +188,7 @@ func (br *containerdBlobReader) validate() error {
130188
info, err := br.client.ContentStore().Info(br.ctx, br.desc.Digest)
131189
if err != nil {
132190
if errdefs.IsNotFound(err) {
133-
return ociregistry.ErrBlobUnknown
191+
return errors.Join(err, ociregistry.ErrBlobUnknown)
134192
}
135193
return err
136194
}
@@ -223,7 +281,11 @@ func (r containerdRegistry) GetBlobRange(ctx context.Context, repo string, diges
223281
requestedSize := offset1 - offset0
224282
if offset1 < 0 || offset0+requestedSize > br.desc.Size {
225283
// "If offset1 is negative or exceeds the actual size of the blob, GetBlobRange will return all the data starting from offset0."
226-
return br, nil
284+
requestedSize = br.desc.Size - offset0
285+
}
286+
if offset0 < 0 { // TODO https://github.com/cue-labs/oci/issues/47
287+
offset0 = br.desc.Size - offset1
288+
requestedSize = offset1
227289
}
228290

229291
ra, err := br.ensureReaderAt()
@@ -245,7 +307,7 @@ func (r containerdRegistry) GetManifest(ctx context.Context, repo string, digest
245307
ra, err := r.client.ContentStore().ReaderAt(ctx, desc)
246308
if err != nil {
247309
if errdefs.IsNotFound(err) {
248-
return nil, ociregistry.ErrManifestUnknown
310+
return nil, errors.Join(err, ociregistry.ErrManifestUnknown)
249311
}
250312
return nil, err
251313
}
@@ -290,7 +352,7 @@ func (r containerdRegistry) GetTag(ctx context.Context, repo string, tagName str
290352
}
291353

292354
// TODO differentiate ErrNameUnknown (repo unknown) from ErrManifestUnknown ?
293-
return nil, ociregistry.ErrManifestUnknown
355+
return nil, errors.Join(err, ociregistry.ErrManifestUnknown)
294356
}
295357
return nil, err
296358
}
@@ -330,16 +392,34 @@ func (r containerdRegistry) ResolveTag(ctx context.Context, repo string, tagName
330392

331393
func (r containerdRegistry) DeleteBlob(ctx context.Context, repo string, digest ociregistry.Digest) error {
332394
// TODO should we stop this from removing things that are still tagged or children of tagged?
333-
return r.client.ContentStore().Delete(ctx, digest)
395+
if err := r.client.ContentStore().Delete(ctx, digest); err != nil {
396+
if errdefs.IsNotFound(err) {
397+
return errors.Join(err, ociregistry.ErrBlobUnknown)
398+
}
399+
return err
400+
}
401+
return nil
334402
}
335403

336404
func (r containerdRegistry) DeleteManifest(ctx context.Context, repo string, digest ociregistry.Digest) error {
337405
// TODO should we stop this from removing things that are still tagged or children of tagged?
338-
return r.client.ContentStore().Delete(ctx, digest)
406+
if err := r.client.ContentStore().Delete(ctx, digest); err != nil {
407+
if errdefs.IsNotFound(err) {
408+
return errors.Join(err, ociregistry.ErrManifestUnknown)
409+
}
410+
return err
411+
}
412+
return nil
339413
}
340414

341415
func (r containerdRegistry) DeleteTag(ctx context.Context, repo string, name string) error {
342-
return r.client.ImageService().Delete(ctx, repo+":"+name)
416+
if err := r.client.ImageService().Delete(ctx, repo+":"+name); err != nil {
417+
if errdefs.IsNotFound(err) {
418+
return errors.Join(err, ociregistry.ErrManifestUnknown)
419+
}
420+
return err
421+
}
422+
return nil
343423
}
344424

345425
func (r containerdRegistry) PushBlob(ctx context.Context, repo string, desc ociregistry.Descriptor, reader io.Reader) (ociregistry.Descriptor, error) {
@@ -367,6 +447,9 @@ func (r containerdRegistry) PushBlob(ctx context.Context, repo string, desc ocir
367447
if err := content.WriteBlob(ctx, cs, ingestRef, reader, desc); err != nil {
368448
_ = cs.Abort(ctx, ingestRef)
369449
_ = deleteLease(ctx)
450+
if errdefs.IsFailedPrecondition(err) {
451+
err = errors.Join(err, ociregistry.ErrDigestInvalid)
452+
}
370453
return ociregistry.Descriptor{}, err
371454
}
372455

@@ -425,6 +508,9 @@ func (bw *containerdBlobWriter) Commit(digest ociregistry.Digest) (ociregistry.D
425508
return ociregistry.Descriptor{}, err
426509
}
427510
if err := bw.Writer.Commit(bw.ctx, 0, digest); err != nil && !errdefs.IsAlreadyExists(err) {
511+
if errdefs.IsFailedPrecondition(err) {
512+
err = errors.Join(err, ociregistry.ErrDigestInvalid)
513+
}
428514
return ociregistry.Descriptor{}, err
429515
}
430516
return ociregistry.Descriptor{
@@ -529,7 +615,6 @@ func (r containerdRegistry) PushManifest(ctx context.Context, repo string, tag s
529615
"l": manifestChildren.Layers,
530616
} {
531617
for i, d := range list {
532-
d := d
533618
labelMappings[prefix+"."+strconv.Itoa(i)] = &d
534619
}
535620
}
@@ -555,6 +640,9 @@ func (r containerdRegistry) PushManifest(ctx context.Context, repo string, tag s
555640
return ociregistry.Descriptor{}, err
556641
}
557642
if err := content.WriteBlob(ctx, cs, ingestRef, bytes.NewReader(contents), desc, content.WithLabels(labels)); err != nil {
643+
if errdefs.IsFailedPrecondition(err) {
644+
err = errors.Join(err, ociregistry.ErrDigestInvalid)
645+
}
558646
return ociregistry.Descriptor{}, err
559647
}
560648

oci-conformance.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
tls: disabled
2+
apis:
3+
blobs:
4+
digestHeader: true
5+
manifests:
6+
digestHeader: true
7+
data:
8+
# TODO implement sha512 (requires containerd to support it first 😭)
9+
# $ ctr content fetch docker.io/tianon/test:sha512-blobs
10+
# docker.io/tianon/test:sha512-blobs: resolved |++++++++++++++++++++++++++++++++++++++|
11+
# manifest-sha256:3aeaae15b975d380c89b18aa9694dbb690af663575ac746a8a6db90015550295: downloading |--------------------------------------| 0.0 B/1.3 KiB
12+
# elapsed: 1.1 s total: 0.0 B (0.0 B/s)
13+
# ctr: failed commit on ref "layer-sha512:bae9ebc21672534b4a4014540b89e9b851ba90e091f3adb929a99ef575fe86ddef8584327630501f65636bc587c8b999cd4ca5f9200166abe15dc82f05f2ffca": commit failed: unexpected commit digest sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6, expected sha512:bae9ebc21672534b4a4014540b89e9b851ba90e091f3adb929a99ef575fe86ddef8584327630501f65636bc587c8b999cd4ca5f9200166abe15dc82f05f2ffca: failed precondition
14+
sha512: false

0 commit comments

Comments
 (0)