Add per-layer content-addressed erofs artifact exporter - #450
Conversation
ExportLayerArtifacts reads OCI layers from the shared OCI cache and converts each supported layer into a content-addressed erofs artifact under images/layers/<diff-id>/, keyed by the layer diff ID and verified against the config's rootfs.diff_ids during unpack. Artifacts install atomically; unsupported media types and layers that cannot unpack standalone are reported as skipped. The flattened image build path and VM boot are unchanged.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 131d5ee. Configure here.
| return "", true | ||
| default: | ||
| return fmt.Sprintf("unsupported layer media type %s", mediaType), false | ||
| } |
There was a problem hiding this comment.
Restricted layers abort export
Medium Severity
supportedLayerMediaType treats OCIRestrictedLayer as convertible, but ocicache Uncompressed only decompresses standard gzip/tar types. A nondistributable or Docker foreign layer therefore fails at open and aborts the whole export instead of being skipped as an unsupported media type.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 131d5ee. Configure here.
|
|
||
| if got := hex.EncodeToString(hasher.Sum(nil)); got != diffID.Hex { | ||
| return LayerArtifact{}, false, fmt.Errorf("diff ID mismatch: unpacked sha256:%s, config declares %s", got, diffID.String()) | ||
| } |
There was a problem hiding this comment.
Diff ID hash may be incomplete
Medium Severity
The diff ID is hashed only from bytes UnpackLayer reads via TeeReader. A tar parser stops at the end-of-archive marker and does not consume trailing uncompressed padding, so the digest can disagree with rootfs.diff_ids and abort export for valid layers.
Reviewed by Cursor Bugbot for commit 131d5ee. Configure here.
Layers carrying deletion semantics (.wh.* whiteouts and .wh..wh..opq opaque markers) are now skipped with an explicit reason instead of being converted: unpacking them standalone applies their whiteouts against an empty root and silently drops the deletions, so a raw tar-to-erofs conversion of such a layer would not compose. metadata.json now records the filesystem format options (erofs -z compression via the new ErofsCompression constant and sector alignment) so the artifact contract is complete for readers.


summary
Reusable per-layer artifact exporter on top of #449's shared content storage:
images.ExportLayerArtifacts(ctx, paths, imageDigest)reads an image's layers from the existing shared OCI cache (lib/ocicache) and converts each supported layer into a content-addressed EROFS artifact underimages/layers/<diff-id>/rootfs.diff_ids) — a layer shared by any number of images converts oncemetadata.jsoninstall atomically via the existinginstallAtomically(staged beside the final path, same filesystem)metadata.jsonrecords the filesystem format and options (erofs-zcompression via a newErofsCompressionconstant, sector alignment) so the artifact contract is complete for readersVerified against a real pull (
public.ecr.aws/docker/library/alpine, since Docker Hub rate-limits here): blobs land atsystem/oci-cache/blobs/sha256/<hex>, layer descriptors carry…layer.v1.tar+gzipwith a matchingdiffID— exactly the layout and media types the exporter consumes.Explicitly out of scope, per the parent effort:
buildImagestill produces one rootfs per image digest; nothing inimages/contentor tag references is touched (asserted in tests).ExportRootfs/mkfs.erofsis reused, not modified (only a behavior-neutral-zconstant extracted)explicit OCI whiteout / opaque-directory handling
A raw tar→EROFS conversion does not compose for layers that delete, so the exporter refuses them rather than silently mis-converting:
.wh.<name>) and opaque-directory markers (.wh..wh..opq) remove content that lives in earlier layers. umoci's standaloneUnpackLayerapplies those whiteouts against an empty root, which silently drops the deletions — so the exporter scans each layer's tar up front (firstWhiteoutMarker) and reports such layers inSkippedinstead of producing a misleading artifact.documented blockers (no invented types)
Where a correct implementation depends on decisions not made yet, the limitation is documented (doc comment on
ExportLayerArtifacts+lib/images/README.md) rather than papered over:imageMetadatagains layer fields once a composition consumer exists. Dependency note: there is currently no separate "manifest metadata" PR; the exporter derives everything it needs from the manifest + config blobs already in the OCI cache, so nothing is duplicated and there is nothing to wait on. If a manifest-metadata change lands later, this mapping is the one field to move.images/layers. Safe today — an artifact is self-contained once installed — but a retention policy is future work.mkfs.erofs; otherwise returnsErrLayerArtifactsUnsupportedfor callers to soft-skip.validation
mkfs.erofsandfsck.erofsinstalled in the test environment, so conversion and artifact-content assertions run for real (not skipped). All tests are synthetic in-memory images written into a temp OCI cache — no live registry required:fsck.erofs --extractand contents compared; asserts metadata recordsformat=erofs,compression=lz4,sector_size=4096; asserts nothing written underimages/contentReused, identical sizes, no.unpack-*scratch left, deleted metadata healsdiff ID mismatchabort with no artifact installed; deleted blob →layer blob missingabortErrLayerArtifactsUnsupportedwhenmkfs.erofsis absentFull existing suites (
./lib/images ./lib/paths ./lib/ocicache) pass, including PR #449's verification test set. The registry-dependent manager tests (TestCreateImage*,TestGetImage,TestDeleteImage*,TestLayerCaching, …) fail identically on the base branch in this environment: Docker Hub unauthenticated pull rate limits (TOOMANYREQUESTS), unrelated to this change.