Skip to content

Add Docker-style image tagging and decouple content / repositories - #425

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

Add Docker-style image tagging and decouple content / repositories#425
chruffins wants to merge 20 commits into
mainfrom
hypeship/image-tag

Conversation

@chruffins

@chruffins chruffins commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

summary

Kernel browser sessions and app deployments run as VM-backed instances. This PR adds Docker-style local image tagging and separates immutable bootable image content from mutable repository/tag references.

A ready Hypeman image can be retagged and pushed without pulling or reconverting it. One converted rootfs is stored per content digest, while repository tags remain lightweight references. Existing legacy image trees remain readable during the migration.

1. pre-existing flows + new flows

existing pull and lookup flow

createImage(name):
  resolve the registry reference to a manifest digest
  write pending metadata
  pull OCI layers and convert them to a bootable rootfs
  write the rootfs and ready metadata
  create a tag reference

getImage(name):
  resolve a tag to a digest, or use the supplied digest
  read metadata and verify the ready rootfs exists
  return the image

new local tagging flow

tagImage(source, target):
  validate source and target references
  require target to contain a tag
  resolve the source tag or digest
  require the source image to be ready

  if source and target use the same repository:
    create or replace the target tag reference
  else:
    promote legacy content into the shared digest layout when needed
    move legacy tags to shared content references
    create the target repository tag reference

  return the target image

Example:

hypeman tag alpine:latest registry.example.com/app:v1
hypeman push registry.example.com/app:v1

Tagging does not pull or reconvert the image. The companion CLI changes are in kernel/hypeman-cli#65.

2. changes to the data model

There is no database schema or migration.

The API adds:

POST /images/{name}/tag

request:
  { "target": "registry.example.com/app:v1" }

response:
  image

The endpoint returns explicit errors for invalid references, missing sources, and non-ready sources.

The logical ownership model is now:

content[digest]  -> one canonical rootfs and metadata record
repository/tag  -> a reference to content[digest]

The existing image metadata format remains compatible. Tags are represented by filesystem references rather than a new database table.

This matches the app platform’s existing deployment identity: deployments retain both an image reference and an image digest. The reference is a mutable lookup name; the digest identifies the immutable image used by the VM.

3. changes to filesystem layout

legacy layout

images/
└── <repository>/
    ├── <tag> -> <digest>
    └── <digest>/
        ├── metadata.json
        └── rootfs.erofs

On macOS, the rootfs uses rootfs.ext4.

content-addressed layout

images/
├── content/
│   └── <digest>/
│       ├── metadata.json
│       └── rootfs.erofs
└── repositories/
    └── <repository>/
        └── <tag> -> ../../content/<digest>

The content directory owns the converted rootfs. Repository/tag paths are references and do not contain another copy of the image.

Readers support both layouts:

  • ready legacy content remains authoritative while shared content is incomplete
  • ready shared content becomes canonical once promotion completes
  • metadata and rootfs are always resolved from the same layout
  • failed or orphaned content can be removed without deleting referenced content

4. why change the filesystem layout

protect the bootable browser artifact

Hypeman converts OCI layers into an EROFS or EXT4 disk that boots the browser VM. That converted disk is the expensive runtime artifact; names such as chrome:stable and deployment-specific tags are only references to it.

Multiple names should not create multiple bootable disks or change the bytes used to start a browser session.

keep app deployment identity immutable

The app platform stores an image digest for deployments and app versions. A deployment must be able to start, roll back, or be recreated from the same digest even after a mutable tag moves forward.

Content-addressed storage makes the digest the local content owner and keeps aliases from creating separate image copies.

support host-local caching

Browser sessions and app invocations may run on different hosts over their lifetimes. A host can receive or prewarm a digest, materialize whatever repository/tag reference it needs, and start the VM from the same cached rootfs.

The host does not need to preserve every repository name that previously referenced the image.

make cleanup and accounting correct

Deleting one tag must not delete content still needed by another tag, app version, browser session, or digest-only reference. Separating content from references lets cleanup retain a digest until its final reference disappears.

Hard-linked aliases can also be counted once by physical inode, so repository aliases do not inflate disk usage.

preserve compatibility and enable promotion

Existing legacy images are readable without an offline migration. Cross-repository tagging promotes legacy content by hard-linking the rootfs into the shared directory, writing shared metadata, atomically installing references, and removing the duplicate legacy tree.

The shared content boundary also provides a clear future home for digest-level prewarming, eviction, verification, replication, backup, and restore.

validation

  • targeted image, storage, disk-accounting, API, scope, and CLI tests pass
  • regression coverage includes legacy/content layout selection, cross-repository promotion, alias deletion, malformed metadata, hard-linked accounting, and atomic tag replacement
  • full integration tests require embedded runtime binaries, mkfs.erofs, registry access, and host virtualization support

Note

High Risk
Reworks image storage, lookup, deletion, and disk accounting for bootable rootfs artifacts. Layout selection and promotion bugs could delete or mix content, or mis-account host disk.

Overview
Adds Docker-style local image tagging so a ready image can be retagged (including across repositories) without pulling or reconverting it, via POST /images/{name}/tag (ImageWrite) and hypeman tag.

Splits on-disk layout into shared content (images/content/<digest>) and repository tags (images/repositories/...). Cross-repo tags promote legacy trees by hard-linking the rootfs, atomically installing tag symlinks, then dropping the duplicate digest dir. Readers still accept the old per-repo layout until promotion.

Deletes now drop tags in the requested repository only and keep shared content while other tags, digest-only refs, or in-flight pulls exist. Disk accounting counts hard-linked aliases once. Builds install the rootfs atomically; GetImage/DeleteImage use the requested reference name.

Reviewed by Cursor Bugbot for commit 6149b26. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread openapi.yaml
Comment thread lib/images/storage.go
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
-->

✱ stlc build

go code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

116 files generated at 887f6be (pushed)

go get github.com/kernel/hypeman-go-staging@887f6be1dfa18d5ca7f99003ae310ad20386a413
python code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

231 files generated at ef2ae23 (pushed)

typescript code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

138 files generated at 693edd5 (pushed)

Diagnostics: ❗ 0 new / 1 total error, 💡 0 new / 5 total note
LevelCodeMessageTargets
Build metadata
Buildbd_76BZrC5V-curved-dirt
Timestamp2026-08-21T18:37:47.259Z
stlc8413509
Spec hashc0136e7a9a1e
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-21 18:38:11 UTC

Comment thread lib/images/disk_usage.go
@chruffins
chruffins marked this pull request as ready for review August 19, 2026 15:26
@chruffins
chruffins requested a review from sjmiller609 August 19, 2026 15:26
@chruffins

Copy link
Copy Markdown
Contributor Author

companion PR here: kernel/hypeman-cli#65

@sjmiller609

Copy link
Copy Markdown
Collaborator

mentioned to assess this directory layout:

images/
├── docker.io/library/alpine/       # existing legacy layout, untouched
│   ├── latest -> <digest>
│   └── <digest>/
│       ├── metadata.json
│       └── rootfs.erofs
│
├── content/                        # new layout only
│   └── <digest>/
│       ├── metadata.json
│       └── rootfs.erofs
│
└── repositories/                   # new tags only
    └── example.com/app/
        └── v1 -> ../../../content/<digest>

and if the migration is worth it or if we should stick to existing for now. un-requesting review until pinged again.

@sjmiller609
sjmiller609 removed their request for review August 19, 2026 18:42
Comment thread lib/images/storage.go
Comment thread lib/images/storage.go Outdated
Comment thread lib/images/storage.go Outdated
Comment thread lib/images/storage.go Outdated
Comment thread lib/images/storage.go Outdated
@chruffins chruffins changed the title Add Docker-style image tagging Add Docker-style image tagging and decouple content / repositories Aug 20, 2026
Comment thread lib/images/manager.go

@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 6149b26. Configure here.

if len(refs) > 0 || contentPullInProgress(p, digestHex) || (preserveDigestOnly && contentIsDigestOnly(p, digestHex)) {
return nil
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy tree retained after digest delete

Medium Severity

removeDigestIfUnreferenced now returns early whenever shared content is still referenced, an in-flight pull exists, or digest-only content should be kept. That early return also skips removing the repository-local legacy digest directory. Previously the legacy tree was always removed first, and only content removal was gated. Deleting a digest that still has aliases in another repository therefore leaves an orphaned legacy tree on disk, which breaks the dual-layout cleanup path covered by TestDeleteDigestRemovesLegacyTreeWhenContentIsReferenced.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6149b26. Configure here.

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