docker

package
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

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

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

func New(cli *client.Client, runID string, blobs state.Blobs, opts ...Option) (*Backend, error)

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

func (*Backend) Capabilities() container.Caps

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). A relative path resolves against the container's WORKDIR (F8; resolveCapturePath) — an absolute path is used unchanged.

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

func (b *Backend) Close() error

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

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.

spec.Image need not be pre-cached: if ContainerCreate reports the image missing (cold cache), Create pulls it by its pinned digest (F27) and retries ContainerCreate exactly once — see the not-found handling below.

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

func (b *Backend) Destroy(ctx context.Context, h container.Handle) error

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) ReadFileAt added in v0.1.1

func (b *Backend) ReadFileAt(ctx context.Context, h container.Handle, filePath string) ([]byte, error)

ReadFileAt reads a single regular file from the running container at the given absolute path. Uses CopyFromContainer (which returns a tar stream) and extracts the single regular-file entry.

Missing path: the daemon surfaces this as an error from CopyFromContainer; we propagate it as a hard error. Container-fs confinement is enforced by the Docker daemon boundary (unlike native's os.Root).

func (*Backend) ReadTreeAt added in v0.1.1

func (b *Backend) ReadTreeAt(ctx context.Context, h container.Handle, dir string) ([]byte, error)

ReadTreeAt captures the subtree rooted at dir inside the container as a gzip-tar whose entries are paths relative to dir (no leading basename component). Matches the wire format produced by the Fake and native backends.

CopyFromContainer returns a plain-tar stream where all entries are prefixed with basename(dir)+"/"; rerootTarEntries strips that prefix before the archive is gzip-compressed for the caller.

The handle is validated first (mirrors file_at.go's lock-lookup order). The Docker daemon surfaces a missing dir as an error from CopyFromContainer.

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):

  1. ContainerInspect — read effective Config.Image, Config.Cmd, Config.Entrypoint.
  2. ContainerDiff — list changed paths.
  3. Sort changes by Path (deterministic blob refs).
  4. For each ChangeAdd/ChangeModify: CopyFromContainer → PathStat dispatch (dir/symlink/regular) → stream body through dw.WriteRegular/WriteSymlink/WriteDir.
  5. For each ChangeDelete: accumulate into deletes slice.
  6. dw.WriteDeletes(deletes); dw.Close.
  7. b.blobs.Put(blob bytes) → blobRef.
  8. formatSnapshotRef(blobRef, image, cmdSpec) → SnapshotRef.

Compose-mode handles error explicitly.

func (*Backend) WriteFileAt added in v0.1.1

func (b *Backend) WriteFileAt(ctx context.Context, h container.Handle, filePath string, content []byte) error

WriteFileAt writes content into the running container at the given absolute path, overwriting any existing file. Mirrors CopyTo in copy.go: builds a one-entry tar via an io.Pipe goroutine and calls CopyToContainer with the path rooted at "/". The Docker daemon's moby layer auto-creates any missing ancestor directories.

Container-fs confinement is enforced by the Docker daemon boundary (unlike native's os.Root).

func (*Backend) WriteTreeAt added in v0.1.1

func (b *Backend) WriteTreeAt(ctx context.Context, h container.Handle, dir string, tarGz []byte) error

WriteTreeAt extracts a gzip-tar (as produced by ReadTreeAt or BuildTreeTar) into dir inside the container, creating dir and its parents as needed. Existing files are overwritten. Entry paths in the archive must be relative to dir.

Uses CopyToContainer with "/" as dstPath (the WriteFileAt + CopyTo convention): entry names are prefixed with strings.TrimPrefix(dir, "/")+"/", causing the Docker daemon to place them under dir. The moby layer auto-creates implied ancestor directories (go-archive.createImpliedDirectories).

The handle is validated BEFORE any tar decoding (mirrors file_at.go's lock-lookup-proceed order). Caps: decompressed bytes ≤ container.TreeTarMaxBytes; entry count ≤ container.TreeTarMaxEntries (enforced inside rerootTarEntries).

type ErrSnapshotTooLarge

type ErrSnapshotTooLarge struct {
	Path  string
	Size  int64
	Limit int64
}

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

type Option func(*Backend) error

Option is a functional option for New. Idiomatic fallible-option pattern (gRPC, OTel, Cobra precedent).

func WithSnapshotMaxBlobBytes

func WithSnapshotMaxBlobBytes(n int64) Option

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL