Skip to content

Add local image tagging on shared content storage - #453

Open
chruffins wants to merge 11 commits into
mainfrom
hypeship/image-tag-api
Open

Add local image tagging on shared content storage#453
chruffins wants to merge 11 commits into
mainfrom
hypeship/image-tag-api

Conversation

@chruffins

@chruffins chruffins commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

summary

This PR adds server-side support for hypeman tag.

Adds POST /images/{name}/tag: create or update a local tag pointing at an image already present locally, without pulling or reconverting. Stacked on the content-addressed storage groundwork.

  • Source may be a tag or digest reference; target must carry a tag (digest targets are rejected).
  • Tagging requires the source to be ready (409 otherwise); missing sources return 404, invalid references 400.
  • Cross-repository targets promote the digest into shared content first, so every alias resolves to one rootfs copy and GC keeps content alive while any repository tag references it.
  • Tag symlinks are staged and renamed atomically, matching the existing last-pull-wins replacement semantics.
  • Regenerated lib/oapi/oapi.go via make oapi-generate (pinned oapi-codegen), added the route scope, and registered the endpoint in stainless.yaml for SDK generation.

flows

The existing image-creation path resolves a registry reference, then reuses a local image or queues a pull and conversion:

createImage(name):
  validate tags, platform, and image reference
  resolve the tag or digest against the remote registry
  lock image mutations

  if local metadata exists:
    if ready:
      create or update the local tag reference
      return the image
    if still building:
      record the tag as a pending claim
      return the pending image

  write pending metadata
  create the pending tag reference
  enqueue pull -> unpack -> convert -> finalize
  return the pending image

ImportLocalImage uses the same reuse-or-queue path with a digest supplied by the local OCI cache. Existing lookups resolve a tag symlink or digest, then read metadata; deletion removes references and garbage-collects unreferenced content.

The new tag path operates only on ready local content:

tagImage(source, target):
  parse both references; require a tagged target
  lock image mutations

  resolve a source tag to its local digest, or use the source digest
  read source metadata and require ready status

  if repositories differ:
    promote legacy content into shared content

  atomically create or replace the target tag
  update the target tag generation
  return the source metadata under the target name

It does not contact a registry, pull layers, or reconvert the rootfs. Same-repository tagging only installs the new reference; cross-repository tagging first makes the shared content available.

why

Separates mutable tag references from immutable digest content so many similar images (e.g. app deployments) can share storage and be retagged cheaply — the Docker-style tag-then-push flow.

validation

  • go test ./lib/images ./lib/scopes ./lib/builds ./cmd/api -count=1 (includes new unit tests: same-repo tag, digest source, cross-repo promotion + GC, not-ready/not-found/digest-target rejection, atomic tag replacement, handler error mapping, route URL decoding)
  • go vet ./... clean
  • Full image integration tests run against Docker Hub; instance VM tests require /dev/kvm access not available in this environment.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ab2c694. Configure here.

Comment thread lib/images/manager.go Outdated
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
-->

✱ stlc build

go code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

116 files generated at 7dfc85f (pushed)

go get github.com/kernel/hypeman-go-staging@7dfc85fbfe4a99a082aecd2a1f033a38055aa5a0
python code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

231 files generated at 8decfb1 (pushed)

typescript code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

138 files generated at f5428d0 (pushed)

Diagnostics: ❗ 0 new / 1 total error, 💡 0 new / 5 total note
LevelCodeMessageTargets
Build metadata
Buildbd_76Dxjpmn-ornate-snare
Timestamp2026-08-28T20:00:33.659Z
stlc8413509
Spec hashc73109a410fe
Config hash659c3687c3f0

This comment is auto-generated by stlc and is kept up to date as you push.
If you push new commits, re-run this workflow to update this comment.
Last updated: 2026-08-28 20:00:58 UTC

@chruffins
chruffins force-pushed the hypeship/image-tag-api branch from ab2c694 to d8b8cc3 Compare August 26, 2026 16:18
@chruffins chruffins changed the title Tag existing local images without pulling or converting Add local image tagging on shared content storage Aug 26, 2026
@chruffins
chruffins force-pushed the hypeship/image-tag-api branch 7 times, most recently from 0ff7892 to d537750 Compare August 26, 2026 19:47
@chruffins
chruffins force-pushed the hypeship/image-tag-api branch from d537750 to 32b6564 Compare August 26, 2026 22:22
@chruffins
chruffins force-pushed the hypeship/image-tag-api branch from 32b6564 to b7a0db5 Compare August 27, 2026 18:04
Base automatically changed from hypeship/content-addressed-image-storage to main August 27, 2026 19:46
POST /images/{name}/tag creates or updates a repository tag pointing at
an existing ready image without pulling or reconverting it. Sources may
be a tag or digest; targets must carry a tag. Cross-repository targets
promote the digest into shared content first so every alias resolves to
one rootfs copy, and deletion keeps content alive while any repository
tag references it.

The tag symlink is staged and renamed atomically, so retagging an
existing name never leaves a dangling reference.
Ported from the earlier tag draft: verify the generated tag route decodes
URL-encoded source names, and keep the builds test mock satisfying the
extended Manager interface.
Move the target tag generation bump to after the tag symlink is created
so a failed TagImage no longer invalidates pending pulls' claims on the
target tag. Use toImageFor for the tag response and GetImage. Extract
the tag feature from manager.go into tag.go and share reference
resolution between GetImage and readyTagImage via resolveRefMetadata.
Fix the digest-source test's dead assertion and add no-side-effect and
legacy-layout coverage.
@chruffins
chruffins marked this pull request as ready for review August 28, 2026 14:48
Replace the error-code switch helper with full-response equality and
collapse repeated tag-call assertion blocks into a shared helper.
@chruffins
chruffins requested a review from sjmiller609 August 28, 2026 15:06

@sjmiller609 sjmiller609 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

changes requested

  • lib/images/tag.go:37-40 — replacing a tag does not garbage-collect its previous digest or refresh disk usage. example:

    1. app:stable → sha256:old
    2. app:latest → sha256:new
    3. sha256:old has no other tags
    4. hypeman tag app:latest app:stable changes stable to sha256:new
    5. nothing references sha256:old, but its rootfs remains on disk indefinitely
  • openapi.yaml:2671-2701 — the authenticated endpoint omits a 401 response, unlike POST /images. add it so the contract and generated SDK response types cover authentication failures consistently.

- Log replaced-content cleanup failures instead of failing the call after
  the tag is live, and document the collection in TagImage's doc comment
- Wrap the existing-target resolve error for consistent 500 context
- Clarify the digest-target rejection message
- Explain why TagImage bumps tag generation after install, unlike
  updateExistingReference
- Skip restaging tag symlinks that already point at shared content during
  legacy promotion
- Extract shared image-seeding test helper into lib/images/testutil
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.

2 participants