[PR #11] [MERGED] Implement image save for Portainer backup #13

Closed
opened 2026-10-15 12:51:17 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/LXC2Docker/pull/11
Author: @JBailes
Created: 4/17/2026
Status: ✅ Merged
Merged: 4/17/2026
Merged by: @JBailes

Base: docker-lxc-daemon ← Head: aimee/session/portainer-image-save


📝 Commits (3)

  • 2e14270 Implement image save for Portainer backup
  • 302e640 Implement image load as the counterpart of image save
  • 70795cf Add multi-image /images/get?names= bulk save

📊 Changes

2 files changed (+588 additions, -0 deletions)

View changed files

➕ internal/api/image_save.go (+585 -0)
📝 internal/api/router.go (+3 -0)

📄 Description

Summary

  • Add GET /images/{name}/get returning a proper Docker-save-format tarball so Portainer's "Download image" button and docker image save work end-to-end against our daemon.

Why

Users had no way to back images out of the daemon: the endpoint was 404'ing, and the recent runs of PR #10 surfaced this as the next blocker for Portainer's backup UX. A clean save also enables round-trips into a real Docker daemon for offline inspection.

What changed

  • layer.tar is produced by invoking host tar against the rootfs:
    • directory-backed templates use their rootfs path directly
    • ZFS-backed templates read straight from <mountpoint>/.zfs/snapshot/tmpl/, so no clone/mount dance is needed
  • The layer is hashed with sha256 so the outer manifest's diff_ids entry matches the byte payload — a hard requirement for docker load
  • A synthesised image config JSON materialises the OCI metadata already kept on ImageRecord (Entrypoint/Cmd/Env/ExposedPorts/User/StopSignal/Healthcheck/Labels/WorkingDir) plus rootfs.diff_ids
  • manifest.json and a legacy v1 repositories file wrap the bundle so older docker CLIs can still load it

Validation

  • Built on a transient daemon socket on root@192.168.1.253 against the running store
  • Saved portainer/portainer-ce:latest → ~180 MB tar with the expected structure:
    • manifest.json lists Config=<sha>.json, Layers=["layer.tar"], RepoTags=["portainer/portainer-ce:latest"]
    • the synthesised config round-trips Entrypoint=["/portainer"], ExposedPorts={8000,9000,9443}/tcp, WorkingDir=/
    • layer.tar contents begin with ./, ./tmp/, ./metadata.json, ./README.md, ./public/ — the real portainer rootfs
    • sha256(layer.tar) == config.rootfs.diff_ids[0] (critical for docker-load acceptance)
  • A second save against nginx:alpine produced the expected 63 MB tar with matching sha

Notes

  • Multi-image GET /images/get?names=a,b is not implemented yet; single-image save via the named endpoint is the one Portainer uses for its "Download image" button.
  • Build + vet pass; no tests in tree.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/LXC2Docker/pull/11 **Author:** [@JBailes](https://github.com/JBailes) **Created:** 4/17/2026 **Status:** ✅ Merged **Merged:** 4/17/2026 **Merged by:** [@JBailes](https://github.com/JBailes) **Base:** `docker-lxc-daemon` ← **Head:** `aimee/session/portainer-image-save` --- ### 📝 Commits (3) - [`2e14270`](https://github.com/games-on-whales/LXC2Docker/commit/2e14270d22956f8c58f994026518341d0babf2ed) Implement image save for Portainer backup - [`302e640`](https://github.com/games-on-whales/LXC2Docker/commit/302e64032a12fa2416ba21f021c03d1a27d0c35f) Implement image load as the counterpart of image save - [`70795cf`](https://github.com/games-on-whales/LXC2Docker/commit/70795cfbdec4da059f3d7bff2bf1f513c612eeae) Add multi-image /images/get?names= bulk save ### 📊 Changes **2 files changed** (+588 additions, -0 deletions) <details> <summary>View changed files</summary> ➕ `internal/api/image_save.go` (+585 -0) 📝 `internal/api/router.go` (+3 -0) </details> ### 📄 Description ## Summary - Add `GET /images/{name}/get` returning a proper Docker-save-format tarball so Portainer's "Download image" button and `docker image save` work end-to-end against our daemon. ## Why Users had no way to back images out of the daemon: the endpoint was 404'ing, and the recent runs of PR #10 surfaced this as the next blocker for Portainer's backup UX. A clean save also enables round-trips into a real Docker daemon for offline inspection. ## What changed - `layer.tar` is produced by invoking host `tar` against the rootfs: - directory-backed templates use their rootfs path directly - ZFS-backed templates read straight from `<mountpoint>/.zfs/snapshot/tmpl/`, so no clone/mount dance is needed - The layer is hashed with sha256 so the outer manifest's `diff_ids` entry matches the byte payload — a hard requirement for `docker load` - A synthesised image config JSON materialises the OCI metadata already kept on `ImageRecord` (Entrypoint/Cmd/Env/ExposedPorts/User/StopSignal/Healthcheck/Labels/WorkingDir) plus `rootfs.diff_ids` - `manifest.json` and a legacy v1 `repositories` file wrap the bundle so older docker CLIs can still load it ## Validation - Built on a transient daemon socket on `root@192.168.1.253` against the running store - Saved `portainer/portainer-ce:latest` → ~180 MB tar with the expected structure: - `manifest.json` lists `Config=<sha>.json`, `Layers=["layer.tar"]`, `RepoTags=["portainer/portainer-ce:latest"]` - the synthesised config round-trips `Entrypoint=["/portainer"]`, `ExposedPorts={8000,9000,9443}/tcp`, `WorkingDir=/` - `layer.tar` contents begin with `./`, `./tmp/`, `./metadata.json`, `./README.md`, `./public/` — the real portainer rootfs - `sha256(layer.tar)` == `config.rootfs.diff_ids[0]` (critical for docker-load acceptance) - A second save against `nginx:alpine` produced the expected 63 MB tar with matching sha ## Notes - Multi-image `GET /images/get?names=a,b` is not implemented yet; single-image save via the named endpoint is the one Portainer uses for its "Download image" button. - Build + vet pass; no tests in tree. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-15 12:51:17 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/LXC2Docker#13
No description provided.