Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Plan C: .zfs image format (zfs send streams) - #7

Merged
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format
Apr 29, 2026
Merged

Plan C: .zfs image format (zfs send streams)#7
sodre merged 6 commits into
zenroot/mainfrom
feature/zfs-c-zfs-format

Conversation

@sodre

Copy link
Copy Markdown
Member

Summary

Adds .zfs (zfs send stream) as a second image format alongside .sqsh. enroot create foo.zfs materializes the stream into the template cache via zfs receive, then clones to the user's container. enroot export --format=zfs NAME produces a .zfs file from a clone's @pristine snapshot.

Implements Plan C. Builds on Plans A (#1), B (#5).

What changed

  • src/storage_zfs.sh — two new helpers (all zfs::*):
    • zfs::ensure_template_from_stream STREAM SHA — recv-side sibling of zfs::ensure_template. Same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses zfs receive instead of unsquashfs. Auto-creates the templates parent dataset (zfs receive doesn't auto-create parents). The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed.
    • zfs::send_stream NAME FILENAME — exports the clone's origin @pristine snapshot to a file. For non-clones (containers created some other way), takes a fresh snapshot, sends, then destroys it.
  • src/runtime.sh (runtime::create) — two-axis dispatch (extension first, backend second). *.zfsruntime::_create_zfs_from_stream (requires ENROOT_STORAGE_BACKEND=zfs; hard error otherwise). Other extensions → existing .sqsh path with backend dispatch from Plan A.
  • src/runtime.sh (runtime::export) — refactored into _export_sqsh (today's mksquashfs byte-for-byte) and _export_zfs (new). Top-level dispatches on the format argument; default is sqsh.
  • enroot.in (enroot::export) — added --format / --format= CLI flag; updated usage block.
  • doc/zfs.md + CLAUDE.md — status notes flipped to "Plans A, B, C, E, F implemented".

Why no magic-byte sniffing

The dispatcher decides what a file is purely by extension (*.zfs vs everything-else). No magic-byte sniffing. If a user feeds a .zfs file to a non-ZFS host they get a clear ".zfs images require ENROOT_STORAGE_BACKEND=zfs" error rather than a confusing failure deep inside unsquashfs -s. Symmetric to how .sqsh is unambiguously squashfs.

Test Plan

Verified manually against a loopback ZFS pool on Linux 6.12.75 (aarch64), zfs-2.4.1:

  • Round-trip:create -n a alpine.sqshexport -f --format=zfs -o donor.zfs a → wipe template cache → create -n b donor.zfsstart b /bin/cat /etc/os-release prints alpine os-release.
  • Content equivalence: the /etc listing of a container created from alpine.sqsh matches the listing of one created from the round-tripped donor.zfs.
  • .sqsh regression:enroot export -f -o foo.sqsh NAME (no --format) still produces a valid squashfs.
  • .zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot create foo.zfs errors with ".zfs images require ENROOT_STORAGE_BACKEND=zfs".
  • --format=zfs hard error on dir backend:ENROOT_STORAGE_BACKEND=dir enroot export --format=zfs ... errors with "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs".
  • --format=invalid errors:enroot export --format=tar ... errors with "Invalid format: tar".
  • Plan B integration:ensure_template_from_stream calls zfs::sweep_templates first (cold/warm/pressure logic applies) and zfs::touch_template on extract and on cache-hit reuse — same lifecycle as the .sqsh path.

Known limitations

  • zfs send/receive produces a stream with the origin snapshot's name embedded in metadata (e.g. tank/enroot/sodre/.templates/<sha>@pristine). The sender's pool/dataset name is therefore visible in the stream header. For Plan D (zfs:// URI), the equivalent stdin/stdout helpers will need to strip this if cross-host privacy matters; for .zfs files passed between trusted operators, it's a non-issue.
  • The cache key for an imported .zfs is the sha256 of the file, not of the underlying snapshot's content. Two .zfs files produced from the same template at different times will differ byte-for-byte (different stream timestamps in the header) and cache as separate templates. ZFS block-level dedup on the templates dataset recovers most of this.
  • No incremental sends (zfs send -i) — full streams only. A future iteration could add --from=BASE to --format=zfs, but it requires the receiver to already have the base snapshot, which conflicts with the "drop a file on a host" simplicity of the current scheme.

sodre added 6 commits April 29, 2026 10:43
ensure_template_from_stream is the recv-side sibling of ensure_template:
same atomic .tmp lock pattern, sweep + touch integration with Plan B's
warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to
materialize the template. The received dataset's snapshot may not be
named @pristine (depends on what the sender sent), so we look it up
and rename if needed.
send_stream is the export-side helper: zfs send the clone's origin
@pristine snapshot to a file. For non-clones (containers created by
some other means), takes a fresh snapshot, sends, then destroys it.
zfs receive does not auto-create parents, so the templates parent is
created via 'zfs create -p' before the recv attempt.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
.zfs is a zfs send stream; only valid with the ZFS backend. .sqsh
keeps existing two-axis dispatch (extension first, then backend).
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
CLI gains a --format flag (default sqsh; zfs requires the ZFS
backend). runtime::export refactors into format-specific helpers:
_export_sqsh keeps the existing mksquashfs path byte-for-byte;
_export_zfs writes a zfs send stream of the clone's @pristine
snapshot via zfs::send_stream.
Behavior change: the function signature now takes an optional
third format argument from enroot::export. Old callers that
didn't pass a third arg still work — sqsh is the default.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Audit of the function refactors landed across Plans A and C found
seven upstream comments that were dropped during the function
splits. Restoring them brings the helpers back to byte-for-byte
parity with upstream's documentation.
In runtime::_create_dir (Plan A): restore
# Resolve the container rootfs path.
# Extract the container rootfs from the image.
In runtime::remove and the new _remove_dir / _remove_zfs (Plan A):
restore
# Resolve the container rootfs path.
# Remove the rootfs specified after asking for confirmation.
In runtime::_export_sqsh (Plan C): restore
# Resolve the container rootfs path.
# Generate an absolute filename if none was specified.
# Export a container image from the rootfs specified.
Plan E's runtime::start and Plan F's docker::load were verified to
have preserved their upstream comments in full.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Per the same principle that drove the Plan F refactor, keep ZFS
code together so the delta to existing files (here, src/runtime.sh)
stays minimal. The previous commit had two private runtime helpers
(runtime::_create_zfs_from_stream and runtime::_export_zfs) that
were thin wrappers over zfs:: calls; consolidating them into the
ZFS module and dispatching directly from runtime::create /
runtime::export is cleaner.
New helpers in src/storage_zfs.sh:
- zfs::create_from_stream IMAGE NAME
Counterpart of zfs::ensure_template + zfs::clone_container for
the .sqsh path; called from runtime::create when the input
image has a .zfs extension.
- zfs::export_to_file NAME FILENAME
Owns filename defaulting and the file-already-exists guard so
runtime::export's ZFS branch is a single dispatch call.
src/runtime.sh now calls the new helpers directly; the deleted
private wrappers were boilerplate.
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
@sodre
sodre merged commit 17cb2c9 into zenroot/mainApr 29, 2026
@sodresodre mentioned this pull request Apr 29, 2026
7 tasks
Sign up for freeto 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.

1 participant

@sodre