Documentation
¶
Overview ¶
Package docker implements container.Backend against the Docker Engine SDK. Phase 4 slices ship the implementation:
- 4.1: image-mode Create + Destroy + Capabilities skeleton.
- 4.2: real Exec + CaptureFiles (stdcopy demux + ctx-cancel watcher).
- 4.3: compose-mode Create + Exec + Destroy (compose-go + compose/v2).
- 4.4: Snapshot + Restore (streaming gzip-tar via ContainerDiff / CopyFromContainer through state.Blobs; 3-segment SnapshotRef embeds image + Cmd + Entrypoint; Restore streams via io.Pipe).
See docs/superpowers/specs/2026-04-14-awf-phase4-design.md for the design.
Index ¶
- type Backend
- func (*Backend) Capabilities() container.Caps
- func (b *Backend) CaptureFiles(ctx context.Context, h container.Handle, paths []string) ([]container.CapturedFile, error)
- func (b *Backend) Close() error
- func (b *Backend) CopyTo(ctx context.Context, h container.Handle, files []container.InputFile) error
- func (b *Backend) Create(ctx context.Context, spec container.ContainerSpec) (container.Handle, error)
- func (b *Backend) Destroy(ctx context.Context, h container.Handle) error
- func (b *Backend) Exec(ctx context.Context, h container.Handle, cmd container.Cmd) (<-chan container.IOChunk, <-chan container.ExecResult, error)
- func (b *Backend) Restore(ctx context.Context, ref container.SnapshotRef, name string) (container.Handle, error)
- func (b *Backend) Snapshot(ctx context.Context, h container.Handle) (container.SnapshotRef, error)
- type ErrSnapshotTooLarge
- type Option
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Backend ¶
type Backend struct {
// contains filtered or unexported fields
}
Backend is the Docker Engine SDK implementation of container.Backend. Slice 4.1 ships the skeleton; 4.2 added real Exec + CaptureFiles; 4.3 adds compose-mode Create + Exec + Destroy; 4.4 adds Snapshot + Restore.
blobs is the content-addressed artifact store used by Snapshot/Restore (slice 4.4). Required at construction time.
snapshotMaxBlobBytes caps the gzip-compressed diff-tar blob size used by Snapshot (slice 4.4). Default snapshotDefaultMaxBlobBytes (256 MiB); override via WithSnapshotMaxBlobBytes. The Snapshot impl lands in Task 4; this commit ships the configurable cap.
func New ¶
New constructs a Docker Backend bound to a specific run.
- cli: the Docker Engine client.
- runID: per-run container-name prefix (Phase 4 design decision 9).
- blobs: content-addressed artifact store for Snapshot/Restore.
- opts: optional behavior overrides (today: WithSnapshotMaxBlobBytes).
All three required args are nil-checked. Slice 4.5's CLI run constructor will pass the same Blobs the dispatcher uses at commit time.
func (*Backend) Capabilities ¶
Capabilities advertises SnapshotFSCoW per Phase 4 design decision 4. The real Snapshot + Restore implementations live in snapshot.go (slice 4.4). RuntimeImage is true: Create honors a map's runtime-resolved image: by pulling the digest-pinned ref at dispatch and reporting it on Handle.ResolvedImageDigest (the PullIfAbsent path below).
func (*Backend) CaptureFiles ¶
func (b *Backend) CaptureFiles(ctx context.Context, h container.Handle, paths []string) ([]container.CapturedFile, error)
CaptureFiles reads each in-container path and returns one CapturedFile per path in the input order. Missing-path is a hard error (no partial returns — matches the fake / Phase 2 invariant).
Implementation: one CopyFromContainer call per path. The SDK returns a tar archive containing the file at path's basename; we iterate the entries, find the basename, read its bytes. (For directory sources, the archive contains the whole tree — slice 4.2 only captures regular files since output_files are spec'd as files, not directories; a future Backend extension could add directory capture.)
nil/empty paths is a no-op returning ([], nil) — matches the fake's "len-zero loop body" semantic.
func (*Backend) Close ¶
Close releases internal resources the Backend itself owns. Currently: the lazy composeCli's wrapped Docker client (constructed by command.NewDockerCli inside newComposeCli — separate from b.cli, which is owned by the caller of New() and must be closed by them).
Without this, image-mode Backends with NO compose use leak nothing (composeCli never initialized), but compose-mode Backends leak the composeCli's HTTP transport goroutines on test teardown — surfaced by goleak.VerifyTestMain in main_test.go.
Not part of the container.Backend interface (CLAUDE.md "seams as designed — no more"): docker is the only Backend with internal resources to release. Image-mode-only tests don't need to call Close; integ tests that use compose-mode AND production cli/backend.go's newBackend cleanup func both call it defensively.
Idempotent; safe to call on a Backend that never created compose-mode state.
func (*Backend) CopyTo ¶
func (b *Backend) CopyTo(ctx context.Context, h container.Handle, files []container.InputFile) error
CopyTo writes each InputFile via one CopyToContainer call rooted at "/". The tar is built in a goroutine and piped to the SDK in lockstep (the io.Pipe + CloseWithError pattern from Restore, snapshot.go). Entry names are the dest paths with the leading "/" stripped. moby auto-creates ancestor directories for each entry (go-archive createImpliedDirectories), so no TypeDir entries are emitted; "/" (the dstPath) always exists. Overwrites. len==0 is a no-op.
func (*Backend) Create ¶
func (b *Backend) Create(ctx context.Context, spec container.ContainerSpec) (container.Handle, error)
Create materialises a container from the digest-pinned image in spec.Image, starts it, waits for readiness (image healthcheck or immediate if none), and returns a Handle. spec.Image is required for image-mode; compose mode lands in slice 4.3.
Precondition: spec.Image must already be present in the local Docker image cache. Create does NOT pull — it calls cli.ContainerCreate which returns a "no such image" error if absent. Callers (slice 4.5's cli/run.go onward) are responsible for pre-pulling via client.ImagePull before invoking Create. The integ tests demonstrate the pattern via the pullImage helper.
Exception (P6a): when spec.PullIfAbsent is set — a map's runtime-resolved per-element image: — Create itself pulls the image first (it cannot have been pre-provisioned, since the ref is learned at dispatch). That ref MUST be digest-pinned (name@sha256:…) so the booted bytes are content-addressed and resume-reproducible; a mutable tag is rejected. The booted digest is reported on Handle.ResolvedImageDigest.
func (*Backend) Destroy ¶
Destroy force-removes the container associated with h. Returns an error if h was never Created or was already Destroyed (matches the fake / os.File.Close double-close convention).
func (*Backend) Exec ¶
func (b *Backend) Exec(ctx context.Context, h container.Handle, cmd container.Cmd) (<-chan container.IOChunk, <-chan container.ExecResult, error)
Exec runs cmd inside the container the handle references. Slice 5.3 streaming contract:
- chunks: emits each stdout/stderr frame live as stdcopy.StdCopy demuxes them; closes when both pipes drain.
- result: receives one ExecResult AFTER chunks closes (ExitCode, accumulated Stdout, Err). ExecResult.Err carries transport-class errors (ctx-cancel, stdcopy mid-stream failure, ContainerExecInspect failure) so callers learn about them without the err return swallowing them before the chunks drain.
ExecResult.AWFOutput is left nil — the dispatcher reads the AWF_OUTPUT tempfile via CaptureFiles per the Phase 4 design §B contract (slice 4.2 Design Q1). A future Backend may populate it; the dispatcher handles both.
Shell: cmd.Run is passed as a single string to `sh -c` (POSIX baseline per Design Q5). Authors needing bash-specific features ship bash in their image and write `bash -c '...'` as the inner script.
TTY: ExecOptions.Tty is left false (zero value); the response stream is multiplexed (stdout + stderr framed) and stdcopy.StdCopy demuxes them. Setting Tty=true would produce a raw stream (no framing) — not what slice 4.2 wants.
On ctx cancellation: a watcher goroutine closes the attach response, the reader goroutine exits, chunks closes, and the result carries ExecResult{Err: ctx.Err()}.
On non-nil error return: the "launch / transport" error class — the command couldn't be started at all. BOTH channels are nil; callers must check err before ranging or receiving.
func (*Backend) Restore ¶
func (b *Backend) Restore(ctx context.Context, ref container.SnapshotRef, name string) (container.Handle, error)
Restore re-materializes a container from a SnapshotRef. Streams the gzip-compressed diff through an io.Pipe writer goroutine into CopyToContainer (no intermediate plain-tar buffer; peak memory at this stage is the diff bytes already in RAM from Blobs.Get + ~74 KiB streaming buffers).
The embedded image is NOT auto-pulled; callers responsible for prior ImagePull (matches Backend.Create's image-mode behavior — slice 4.1 precedent). If the image is absent from the local cache, ContainerCreate errors with "no such image" and Restore propagates.
Per-delete Exec is O(N) sequential. A workspace with N deletes does N sequential rm -rf calls (~50-100ms each); a 1000-delete workspace takes ~50-100 seconds. Acceptable for slice 4.4 (Restore is rare); a future optimization could batch via xargs. If a delete-Exec fails, Restore aborts cleanly (force-removes the partially-restored container) — the engine resume re-calls Restore from scratch.
func (*Backend) Snapshot ¶
Snapshot captures the workspace state of an image-mode container as a gzip-compressed CoW diff (Phase 4 design decision 4 + slice 4.4 Design Q5/Q8):
- ContainerInspect — read effective Config.Image, Config.Cmd, Config.Entrypoint.
- ContainerDiff — list changed paths.
- Sort changes by Path (deterministic blob refs).
- For each ChangeAdd/ChangeModify: CopyFromContainer → PathStat dispatch (dir/symlink/regular) → stream body through dw.WriteRegular/WriteSymlink/WriteDir.
- For each ChangeDelete: accumulate into deletes slice.
- dw.WriteDeletes(deletes); dw.Close.
- b.blobs.Put(blob bytes) → blobRef.
- formatSnapshotRef(blobRef, image, cmdSpec) → SnapshotRef.
Compose-mode handles error explicitly.
type ErrSnapshotTooLarge ¶
ErrSnapshotTooLarge is returned by Snapshot when the gzip-compressed diff-tar exceeds the Backend's snapshotMaxBlobBytes cap (default 256 MiB; override via WithSnapshotMaxBlobBytes). The engine (slice 4.5+ wiring) propagates it as permanent_failure (Phase 4 design decision 11).
Path is the tar entry being written when the cap tripped. Size is the cumulative gzip output bytes at the moment the cap tripped (>= Limit). Limit is the Backend's configured cap.
func (*ErrSnapshotTooLarge) Error ¶
func (e *ErrSnapshotTooLarge) Error() string
func (*ErrSnapshotTooLarge) Is ¶
func (e *ErrSnapshotTooLarge) Is(target error) bool
Is reports the docker too-large error as the package-level container.ErrSnapshotTooLarge sentinel, so the engine can classify it as a permanent_failure via errors.Is without importing this concrete type (Phase-4 decision 11; keeps the classification behind the container seam).
type Option ¶
Option is a functional option for New. Idiomatic fallible-option pattern (gRPC, OTel, Cobra precedent).
func WithSnapshotMaxBlobBytes ¶
WithSnapshotMaxBlobBytes overrides the default Snapshot blob-size cap (256 MiB). The cap is on the GZIP-COMPRESSED tar bytes (the blob that goes through state.Blobs.Put), NOT on the underlying workspace mutation. Text-heavy workspaces compress 3-10×, so 256 MiB of compressed blob corresponds to roughly 1 GiB of typical text content.
Rejects n <= 0 (operator footgun: WithSnapshotMaxBlobBytes(0) would make every Snapshot fail; explicit rejection at New time surfaces the typo loudly rather than at the first Snapshot call).