Skip to content

Add pull and inspect OCI capabilities - #302

Open
henrybear327 wants to merge 1 commit into
sysprog21:mainfrom
henrybear327:oci/store-pull-inspect
Open

Add pull and inspect OCI capabilities#302
henrybear327 wants to merge 1 commit into
sysprog21:mainfrom
henrybear327:oci/store-pull-inspect

Conversation

@henrybear327

@henrybear327 henrybear327 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

We will introduce the go toolchain in this PR, as we rely on the existing libraries to perform the job.


Summary by cubic

Adds elfuse-oci, a standalone Go CLI to pull and inspect OCI images into a spec-compliant local store, keeping elfuse OCI-agnostic. New behavior: pins images by reference and platform in a durable, lock-serialized store; default platform is linux/arm64, non-linux is rejected.

  • Store: OCI image-layout plus refs.json (ref→manifest digest keyed by os/arch[/variant]). Metadata writes are durable (temp+fsync+rename+fsync dir). An exclusive .lock flock serializes index.json and pin updates; corrupt refs.json/index.json fail closed. Index descriptors record platform. Blob writes and fsync happen before index/pin and heal missing/truncated blobs.
  • Pull: Uses github.com/google/go-containerregistry; supports --platform and --timeout (bounds the whole pull). Validates the pulled config matches the requested platform (single-manifest refs cannot silently mismatch). Progress prints to stderr. Credentials come from the default keychain, time-bounded with an actionable error that suggests DOCKER_CONFIG for anonymous pulls.
  • Inspect: Prints a human summary or --json the raw config blob (not re-marshaled). Write errors propagate. Includes help and version.
  • Build/CI: make all builds elfuse-oci only when GO is on PATH; make oci-test runs package tests. Adds a Linux CI job to vet and test the CLI. No impact to elfuse on Go-less hosts.
  • Defaults: Store is $ELFUSE_OCI_STORE or ~/.local/share/elfuse/oci.

Rollout

  • Install Go to build/test elfuse-oci (make oci-test). Existing elfuse workflows remain unchanged.
  • Use build/elfuse-oci pull [--store DIR] [--platform] [--timeout DUR] <ref> and build/elfuse-oci inspect [--store DIR] [--platform] [--json] <ref>. Set $ELFUSE_OCI_STORE to override the store location.

Written for commit 0f591a4. Summary will update on new commits.

Review in cubic

@henrybear327
henrybear327 requested a review from jserv August 16, 2026 20:49
@henrybear327 henrybear327 self-assigned this Aug 16, 2026
cubic-dev-ai[bot]

This comment was marked as resolved.

@henrybear327
henrybear327 force-pushed the oci/store-pull-inspect branch from 767c0cc to 29667f6 Compare August 16, 2026 21:39

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the store, lock, durability, platform, and build wiring paths end to end against the branch. go vet, gofmt -l, and go test -race ./cmd/elfuse-oci/ are all clean on go1.26.6.

The design decisions in the commit message mostly hold, with one cluster that does not: the crash-recovery story. Findings 1 and 2 are the same root cause seen from two ends, and I reproduced both against the real store rather than reasoning about them. The rest are independent.

Comments are inline.

Comment thread cmd/elfuse-oci/store.go
return "", err
}
err = s.withLock(func() error {
present, err := s.hasImageLocked(h)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Presence is decided from index.json alone, so a store damaged by a crash can never heal.

Remove one layer blob and re-pull: it fails with store: sync image blobs: open .../blobs/sha256/<hex>: no such file or directory, and it fails identically on every later pull, because appendImageLocked is skipped and the blob is never refetched. The ref is stuck permanently.

Zero-filling that same blob at its original size is worse: the re-pull succeeds and pins the corrupt content, since ggcr's size check in layout/write.go:216 only rewrites a blob whose size differs.

Make presence mean "descriptor in index.json and every blob it names present at the right size", so a damaged image falls back to appendImageLocked.

Comment thread cmd/elfuse-oci/store.go
if err != nil {
return err
}
if err := writeFileDurable(s.indexPath(), b, 0o644); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

index.json commits durably here, but the layer blobs it names are fsynced later, in addImage's syncImageBlobs call at line 308. WriteImage above leaves them rename-atomic and nothing more, so the index that names them commits first.

A crash in this window is exactly what produces the unrecoverable store described above. The ordering the design comment claims holds for the pin, but it is inverted for the index.

Move the blob fsync ahead of the index write: call syncImageBlobs at the end of appendImageLocked, before writeFileDurable(s.indexPath(), ...).

Comment thread cmd/elfuse-oci/store.go
if err != nil {
return "", err
}
err = s.withLock(func() error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

crane.Pull returns a lazy image, so the layer download happens inside this lock, at s.path.WriteImage on line 357. The exclusive store flock is held for the whole network transfer, not just the index and pin update the lock comment describes.

One slow or hung registry therefore blocks every other elfuse-oci process, including a first-run openStore bootstrap. There is no context or deadline on the pull either, so "slow" has no upper bound.

Blobs are content-addressed, so write them unlocked and take the lock only around hasImageLocked, the index write, and the pin. Pass crane.WithContext with a timeout while you are here.

Comment thread cmd/elfuse-oci/pull.go Outdated
Comment thread cmd/elfuse-oci/store.go Outdated
Comment thread Makefile

# Go unit tests for the OCI image CLI (offline).
.PHONY: oci-test
oci-test:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing runs this. oci-test is absent from check (mk/tests.mk:204) and from every workflow, and no job builds build/elfuse-oci, so the new Go package and its test suite are never compiled or exercised in CI. A regression here lands green.

Add oci-test to check behind HAVE_GO, or add a Linux job running go vet ./cmd/elfuse-oci/ && make oci-test. The package is pure Go, so a Linux runner covers it.

Comment thread go.mod Outdated
Comment thread cmd/elfuse-oci/flock.go Outdated
Comment thread cmd/elfuse-oci/store.go Outdated
@jserv jserv changed the title Add pull and inspect oci capabilities Add pull and inspect OCI capabilities Aug 17, 2026
@henrybear327
henrybear327 force-pushed the oci/store-pull-inspect branch from 29667f6 to af5e37d Compare August 19, 2026 16:49
elfuse-oci is a standalone Go binary that owns the OCI image pipeline;
elfuse itself stays a pure Linux syscall-to-Darwin runtime with no OCI
commands. This first slice is the acquisition half: an OCI image-layout
store plus the pull and inspect commands, built on go-containerregistry
($ELFUSE_OCI_STORE or ~/.local/share/elfuse/oci by default).

The store is a spec-shape image layout other tools can read, with a
refs.json pin table keyed by reference and platform, so one ref holds
its arm64 and amd64 variants side by side rather than the second pull
replacing the first; index descriptors carry their platform for the same
reason, and inspect takes --platform to pick one. A corrupt store fails
the command instead of being read as empty: a nil-object or flat-shape
refs.json is rejected, and digestFor returns a distinct errNotPulled so
callers can tell "not pulled" from "store broken".

An exclusive flock serializes the refs.json and index.json
read-modify-write cycles so concurrent pulls cannot lose pins or
descriptors. Blob writes stay outside it, since blobs are
content-addressed and crane's lazy layer download would otherwise hold
the lock across the whole network transfer, and they run whether or not
the index already names the image, so a blob a crash or an operator lost
is refetched instead of failing every later pull.

Store metadata writes are durable, not merely atomic: writeFileDurable
writes a temp sibling, fsyncs, renames, then fsyncs the parent
directory. Config, manifest, and index.json go through it rather than
the layout package, whose in-place write could leave a partial blob a
later size check accepts or a truncated index the store fails closed on;
layer blobs keep the layout package's size-checked temp-and-rename
write. The order is blobs, fsync, index, pin, so index.json never names
blobs the page cache could lose.

pull rejects a non-linux --platform OS at parsing, since a store entry
for another OS could never be consumed; the arch stays open for Rosetta.
crane.WithPlatform applies only to a manifest list, so pull also checks
the fetched config against the request and rejects a single-manifest ref
served for the wrong platform.

Credentials come from the ambient default keychain, wrapped to bound its
resolution: go-containerregistry drops the context around the
credential-helper exec, so a wedged helper would hang the pull silently.
timedKeychain caps the wait and names DOCKER_CONFIG as the way to force
an anonymous pull. --timeout bounds the whole transfer, unbounded by
default since a fixed bound would fail large images on slow links.

make all builds elfuse-oci when a Go toolchain is on PATH and skips it
with a notice otherwise; make oci-test runs the package tests, joins
make check behind the same toolchain gate, and runs with go vet in a
Linux CI job. go.mod declares the minimum language version rather than a
patch-exact toolchain, so a host Go at or above it needs no download.
docs/usage.md documents the store layout and the commands, and
docs/oci-design.md the model, the C/Go boundary, and the scope limits.
@henrybear327
henrybear327 force-pushed the oci/store-pull-inspect branch from af5e37d to 0f591a4 Compare August 20, 2026 14:11

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rebase latest main branch as #313 installs Git hooks and a CI gate to enforce commit‑message rules and staged checks.

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