Skip to content

feat(ecr): Batch 3 — OCI v2 Distribution, real docker push/pull#719

Merged
vieiralucas merged 3 commits intomainfrom
worktree-worktree-ecr-batch3
Apr 23, 2026
Merged

feat(ecr): Batch 3 — OCI v2 Distribution, real docker push/pull#719
vieiralucas merged 3 commits intomainfrom
worktree-worktree-ecr-batch3

Conversation

@vieiralucas
Copy link
Copy Markdown
Member

@vieiralucas vieiralucas commented Apr 23, 2026

Summary

Endpoints

Method Path Purpose
GET /v2/ API version probe (required by docker CLI login)
GET /v2/<name>/tags/list List image tags
HEAD/GET/DELETE /v2/<name>/blobs/<digest> Blob access
POST /v2/<name>/blobs/uploads/ Start upload (supports ?digest= single-POST)
PATCH /v2/<name>/blobs/uploads/<uuid> Append chunk
PUT /v2/<name>/blobs/uploads/<uuid>?digest= Finalize
DELETE /v2/<name>/blobs/uploads/<uuid> Cancel
HEAD/GET/PUT/DELETE /v2/<name>/manifests/<ref> Manifest access (tag or sha256)

Behaviour

  • Auth: Basic base64("AWS:<token>") tokens issued by GetAuthorizationToken are accepted; test* dev-bypass users too. Unauth returns 401 with WWW-Authenticate + Docker-Distribution-Api-Version headers.
  • Content-addressed blobs: PUT manifest computes sha256 of uploaded bytes; DELETE by tag only removes the image when no other tags reference the digest.
  • Batch 2 integration: OCI uploads use the same layer state machine. Push a blob via PATCH/PUT and the AWS SDK BatchCheckLayerAvailability / DescribeImages sees it, and vice versa.
  • Dispatcher: /v2/* pre-routes to ECR before the apigateway fallback, since Docker clients don't carry SigV4.

Test plan

  • cargo build --workspace
  • cargo fmt --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p fakecloud-e2e --test ecr_oci — 5 passing (full push/pull round-trip via raw reqwest)
  • cargo test -p fakecloud-conformance --test ecr — 22 passing
  • cargo run -p fakecloud-conformance --release -- run --services ecr — 22/58 ops, 100% pass
  • cargo run -p fakecloud-conformance --release -- check — no regressions
  • Real docker CLI smoke (manual; test suite already covers the wire protocol via reqwest)

Follow-up

Batch 4: lifecycle policy evaluation, scanning, registry ops, pull-through cache, replication, CloudFormation AWS::ECR::Repository + Lambda CodeUri image integration, website landing + marketing.


Summary by cubic

Adds OCI Distribution v2 endpoints and GetAuthorizationToken to ECR so real Docker clients can login, push, and pull against localhost:4566. Also fixes unauthenticated /v2/ to return a proper 401 challenge and prevents cross-repository upload cancellation.

  • New Features

    • Implements /v2/* for tags, blobs (HEAD/GET/DELETE, uploads POST/PATCH/PUT/DELETE), and manifests (HEAD/GET/PUT/DELETE).
    • Supports Basic auth via GetAuthorizationToken (AWS:<token>) and sets Docker-Distribution-Api-Version headers.
    • Computes manifest digests on PUT; tag deletes only remove unreferenced images; supports chunked uploads and single-POST ?digest=.
    • Pre-routes /v2/* to ECR in the core dispatcher (no SigV4). Adds e2e push/pull and a conformance test; ECR now at 22/58 ops; no regressions.
  • Bug Fixes

    • Returns 401 with WWW-Authenticate on /v2/ when Authorization is missing, matching Docker's login flow.
    • Rejects DELETE /v2/<name>/blobs/uploads/<uuid> when the upload belongs to a different repository (covered by new e2e test).

Written for commit e5dc9f8. Summary will update on new commits.

Ships the OCI Distribution v2 HTTP surface (`/v2/...`) and the
`GetAuthorizationToken` JSON op so `aws ecr get-login-password |
docker login localhost:4566 && docker push` now round-trips against
fakecloud. This is the differentiator Batch 2 set up — real
content-addressed blob and manifest storage + the HTTP layer Docker
speaks on top of it.

Endpoints:
- GET /v2/                                         API version probe
- GET /v2/<name>/tags/list                         list image tags
- HEAD/GET/DELETE /v2/<name>/blobs/<digest>        blob access
- POST   /v2/<name>/blobs/uploads/                 start blob upload
                                                   (?digest= for
                                                   single-POST)
- PATCH  /v2/<name>/blobs/uploads/<uuid>           chunked append
- PUT    /v2/<name>/blobs/uploads/<uuid>?digest=   finalize
- DELETE /v2/<name>/blobs/uploads/<uuid>           cancel
- HEAD/GET/PUT/DELETE /v2/<name>/manifests/<ref>   manifest access
                                                   (ref = tag or
                                                   sha256: digest)

Behaviour:
- Basic auth challenge returns 401 with `WWW-Authenticate` + the
  `Docker-Distribution-Api-Version: registry/2.0` header Docker
  expects. Tokens issued by `GetAuthorizationToken` are accepted
  (user = `AWS`); `test*` dev-bypass users also accepted.
- Every response carries `Docker-Distribution-Api-Version` and, where
  relevant, `Docker-Content-Digest`, `Docker-Upload-UUID`, `Location`,
  and `Range` headers matching the OCI spec.
- PUT manifest computes the sha256 of the uploaded bytes, stores the
  image by digest, and (if ref isn't a `sha256:` digest) adds a tag
  pointing at it. DELETE by tag removes only the tag when other tags
  still reference the digest.
- Uploads integrate with the Batch 2 layer state machine: PATCH
  appends, PUT validates `digest` and commits atomically. Retrying
  with a bad digest on PUT no longer drops upload state.
- GetAuthorizationToken returns `base64("AWS:<uuid>")` with a 12h
  expiry matching AWS's documented shape.
- Dispatcher pre-routes `/v2/*` to ECR before the apigateway
  fallback so Docker clients (which don't carry SigV4) land in the
  right handler.

Tests:
- 5 E2E tests exercise the full Docker push/pull flow over raw
  reqwest (API probe, unauthorized, chunked upload + manifest push +
  tag list + SDK crossover, get-authorization-token, tag-name push).
- 1 new conformance test for GetAuthorizationToken; 22/58 ECR ops
  now implemented at 100% (690/1789 variants).
- Baseline bumped to 60644 / 61743 (98.2%). No regressions on other
  services.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 0% with 572 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/fakecloud-ecr/src/oci.rs 0.00% 526 Missing ⚠️
crates/fakecloud-ecr/src/service.rs 0.00% 40 Missing ⚠️
crates/fakecloud-core/src/dispatch.rs 0.00% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/fakecloud-ecr/src/oci.rs">

<violation number="1" location="crates/fakecloud-ecr/src/oci.rs:533">
P2: `blob_upload_cancel` is missing the repository-name ownership check that `blob_upload_patch` and `blob_upload_finish` both enforce. A cancel request scoped to one repository can delete an upload belonging to a different repository.</violation>
</file>

<file name="crates/fakecloud-e2e/tests/ecr_oci.rs">

<violation number="1" location="crates/fakecloud-e2e/tests/ecr_oci.rs:47">
P3: Drop the Authorization header here so this test actually covers the unauthenticated `/v2/` case.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread crates/fakecloud-ecr/src/oci.rs Outdated
Comment thread crates/fakecloud-e2e/tests/ecr_oci.rs
- blob_upload_cancel now refuses to delete an upload scoped to a
  different repository. Previously a DELETE targeting `repo-b` could
  tear down an in-flight upload owned by `repo-a` because the handler
  only checked the upload id. Matches the ownership check the PATCH
  and PUT handlers already enforce. E2E test covers the cross-repo
  case.
- Renamed the negative-auth test to describe what it actually
  verifies (bad credentials -> 401), since it does send an
  Authorization header.
Missing Authorization on /v2/ endpoints now returns 401 with a
WWW-Authenticate header, matching the Docker CLI's two-phase login
contract (unauth GET /v2/ -> 401 challenge -> Basic retry). Prior
behaviour silently accepted missing credentials as a dev convenience,
which broke the challenge flow and also meant the negative tests
never actually exercised the unauth path.

Adds an E2E test that drops the Authorization header entirely.
@vieiralucas vieiralucas merged commit 6d7cf0e into main Apr 23, 2026
47 checks passed
@vieiralucas vieiralucas deleted the worktree-worktree-ecr-batch3 branch April 23, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant