docker

package
v0.0.0-...-89e6720 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: Apache-2.0 Imports: 31 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).

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.

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

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

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