Add pushes API for exporting images to remote registries - #353
Add pushes API for exporting images to remote registries#353chruffins wants to merge 19 commits into
Conversation
3529276 to
143d09b
Compare
912ae22 to
f2ee13c
Compare
8eefe9c to
d90000d
Compare
90d7da1 to
9302c19
Compare
|
reviewed — solid, well-tested layer 3: clean handler wiring, thorough error mapping, good happy-path tests. the bugs, nits, and the interface/timing structural items are fixed in Structural / Maintainability (open)
Questions (open)
Notes
status: all review findings addressed except the listed deferrals |
9302c19 to
fa579f0
Compare
fa579f0 to
b244f44
Compare
b244f44 to
dbd4c99
Compare
-->
✱ stlc build✅ go code · compare
✅ typescript code · compare
Diagnostics: 💡 0 new / 5 total note
Build metadata
This comment is auto-generated by stlc and is kept up to date as you push. |
846fef3 to
7448f03
Compare
7448f03 to
ad44f2a
Compare
Orchestrates pushing cached images to remote registries: resolves a hypeman image to its cached digest, runs push jobs on a bounded queue with digest+target deduplication, persists job status to disk with FIFO recovery across restarts, and exposes in-flight digests so the OCI cache GC can keep required blobs alive mid-push.
Docker-aligned credential flow: the caller's registry login rides along with the push request instead of living on the server. Borrowed credentials are used only for that job, never persisted or logged, and a credentialed push interrupted by a restart fails with an explanation instead of retrying under different credentials. The manager's default provider remains the fallback when no credentials are supplied.
- Release the inflight registration only after the queue slot is freed, closing a window where a new push for the same key persisted metadata but was never started because the queue still held the slot. - CreatePush adopts a pending record already on disk for the same digest+target instead of duplicating it. - Recovery dedupes same-key records (oldest wins, rest marked failed) and removes records whose status cannot be persisted, so nothing is left permanently queued. - Terminal status writes retry once and drop the record on failure so disk state cannot diverge from the WaitForPush notification.
…rsist-or-fail terminals - Defer queue completion so a panicking job still releases its slot and runs the completion hook. - An orphaned pending record that used borrowed credentials is closed with the recovery policy and replaced by a fresh job instead of being re-executed under the default provider. - When a terminal status cannot be persisted even after retry, drop the record and report the job failed with the persistence problem so WaitForPush and GetPush agree, logging the actual push outcome.
failRecovered persisted the failed status without notifying, so a WaitForPush racing the close could subscribe before the write and then wait for a notification that never comes.
- Include the insecure flag in the dedup/recovery key: the same target pushed with different transport modes is distinct work. - Log loudly when startup recovery cannot list pending pushes instead of failing silently. - Contain panics in the push goroutine: record a failed terminal and notify waiters instead of leaving the job stuck as pushing. - A request that lends credentials supersedes a credential-less orphan instead of adopting it, so the push never runs under the wrong auth.
CreatePush now refuses to merge a request into an in-flight job whose credential presence differs (one borrowed, one not), returning ErrCredentialConflict instead of silently running under the wrong auth. The orphan-adoption path in CreatePush was unreachable: startup recovery adopts every pending record before any CreatePush can run, and the create lock covers write+registration, so the per-request PushesDir scan was dead O(N) disk I/O under the lock. Remove it along with findPendingPush. listAllPushes now warns on unreadable metadata instead of swallowing it.
Extract lib/queue, a minimal in-memory bounded queue with key dedup, a concurrency cap, and an optional completion hook, and use it from imagepush and images; delete their local queue implementations (pushQueue and images' BuildQueue). builds keeps its superset with serial keys. imagepush/manager_test.go: add a testManager fixture to collapse the repeated paths+cache+resolver+NewManager setup, and merge the not-ready/unknown/ invalid-target rejection tests into one table-driven test. Net: -23% PR size (2030 -> ~1930 insertions) with behavior preserved.
Add mustPushed and writePushes helpers to collapse the repeated wait-for-push + get + assert-pushed blocks (6 sites) and the recovery fixture metadata writes, without changing coverage.
- lib/queue: replace the variadic done hook with a plain nil-able param, document that a dedup'd enqueue does not run it, and note the under-lock launch is safe - treat an empty non-nil credential config as anonymous (credsPresent) - persist push metadata durably: fsync the temp file before rename - use a distinct (non-wrapped) error for empty-digest-on-ready, a corrupted-record state rather than not-ready - sort InProgressDigests for determinism Tests: assert pending QueuePosition via the manager read surface, verify WaitForPush on a superseded recovered job fails, and cover recovery when blobs were reclaimed by GC between crash and restart.
bd67d51 to
85330bd
Compare
85330bd to
1a95a28
Compare
1a95a28 to
2a4434d
Compare
POST /pushes creates a push job exporting a ready hypeman image from
the OCI cache to a remote registry; GET /pushes and GET /pushes/{id}
expose job state. Requests may lend registry credentials, which the
push manager borrows for that job only and never persists; without
them the server's own credentials resolve via the Docker keychain.
Routes use the existing image:read/image:write scopes, the push queue
concurrency is configurable, and in-flight push digests are composed
into the OCI cache GC roots.
- Pin x-enum-varnames on PushStatus so generated constants carry the PushStatus prefix like the other enums, avoiding bare Failed/Queued names in the oapi package. - Treat an empty credentials object the same as absent credentials so the server's default credential resolution stays in effect instead of pushing with an empty auth config.
Without a pushes resource, generated SDK clients omit the push API even though the server exposes it. Mirrors the builds resource mapping.
2a4434d to
a9a4500
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 13a1a28. Configure here.
| // clear retryable error rather than orchestrating this caller into | ||
| // a successor job that does not yet exist. A retry once the entry | ||
| // drops creates a fresh job. | ||
| return nil, fmt.Errorf("%w: push job %s is being finalized after a record write failure; retry", ErrNotFound, id) |
There was a problem hiding this comment.
Finalization error becomes 500
Medium Severity
After removing the inflight-release wait, CreatePush can return wrapped imagepush.ErrNotFound during the persist-failure teardown window and labels it retryable, but the handler only maps images.ErrNotFound to 404 and treats this as internal_error 500.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 13a1a28. Configure here.


Layer 3 of remote registry push support (stacked on #348, #350).
What
The HTTP surface for outbound pushes, docker-aligned credentials included.
API (openapi.yaml → generated)
POST /pushes→ 202 + Push job. Body:image(hypeman image, must beready),target(full remote ref), optionalinsecure, optionalcredentialsGET /pushes(newest first),GET /pushes/{id}queued → pushing → pushed/failedwith queue position, error, layers/bytes, timestampsCredentials — borrow, don't store
credentials(username/password/registry_token, mirroring docker config.json fields) map to anauthn.AuthConfigand are borrowed for that single push only — never persisted (asserted in layer 2) or loggedimage:write(POST) /image:read(GET) — no new scope plumbing, existing tokens workWiring
ProvidePushManager(resolver = image manager, concurrency fromlimits.max_concurrent_pushes, default 2)ApiService.PushManager+ wire regencompositeOCICacheRootsin main.go feeds the OCI cache GC both the registry's BuildKit cache tags and the push manager's in-flight digests (imagepush.ManagergainsLiveCacheManifestDigeststo satisfyocicachegc.RootsProvider)Tests
Handler-level with a fake push manager (hermetic, follows the images_test pattern): request + credentials mapping, nil-credentials fallback, full error-status mapping table, get-not-found, list empty/all with layers/bytes mapping.
Notes
lib/oapi/oapi.goregenerated viamake oapi-generate(pinned oapi-codegen v2.5.1); the embedded-spec blob diff includes the pre-existing compression drift any regen under the current Go toolchain produces on main toowire_gen.goregenerated with wire v0.6.0GET /pushes/{id}(builds-style events can be a follow-up)Verification
go build -tags containers_image_openpgp ./...— full tree buildslib/scopes,lib/providers,cmd/api/config, and all push/image packages passNote
Medium Risk
Touches registry auth (request-borrowed credentials), async export to external registries, and OCI cache GC roots; behavior is heavily tested but misconfiguration or GC interaction could affect blob retention during pushes.
Overview
Adds outbound image push as a first-class API: clients can start async jobs that export ready hypeman images from the local OCI cache to a remote registry reference, then poll status via
GET /pushesandGET /pushes/{id}.HTTP surface (
POST /pushes→ 202): acceptsimage,target, optionalinsecure, and optional borrowed registry credentials (username/password/registry token). Handlers map domain errors to 400/404/409; empty credential objects are treated like omitted so the server keychain is not masked. Layer/byte counts are only exposed when status ispushed.lib/imagepush: bounded queue, on-disk job metadata underdata/pushes, in-flight dedup by digest+target+insecure with credential fingerprint conflicts, restart recovery (credentialed jobs fail; anonymous jobs re-queue), andLiveCacheManifestDigestsso GC keeps blobs alive mid-push.Infrastructure:
limits.max_concurrent_pushes(default 2); image build concurrency moves fromimages.BuildQueueto sharedlib/queue; OCI cache GC roots are composed from the embedded registry and the push manager. Wire/DI injectsPushManager; scopes reuse existing image read/write.Reviewed by Cursor Bugbot for commit 13a1a28. Bugbot is set up for automated code reviews on this repo. Configure here.