Documentation
¶
Overview ¶
Package docker wraps the Docker Engine API. Every call goes through the SDK's HTTP-over-socket client, never a shelled-out `docker` CLI command, per the project's rules for node communication and orchestration. This file defines the narrow interface reconcile controllers depend on, so tests can fake it without a daemon.
Index ¶
- type BindMount
- type Client
- func (c *Client) BridgeGatewayIP(ctx context.Context) (string, error)
- func (c *Client) Close() error
- func (c *Client) Create(ctx context.Context, spec ContainerSpec) (string, error)
- func (c *Client) DiskUsage(ctx context.Context) (DiskUsage, error)
- func (c *Client) EnsureNetwork(ctx context.Context, name string) (string, error)
- func (c *Client) EnsureVolume(ctx context.Context, name string) error
- func (c *Client) Events(ctx context.Context) (<-chan Event, <-chan error)
- func (c *Client) Exec(ctx context.Context, containerID string, cmd []string) (io.ReadCloser, error)
- func (c *Client) ExecTTY(ctx context.Context, containerID string, opts ExecTTYOptions) (ExecSession, error)
- func (c *Client) ExecWithInput(ctx context.Context, containerID string, cmd []string, stdin io.Reader) (io.ReadCloser, error)
- func (c *Client) InspectByName(ctx context.Context, name string) (*ContainerState, error)
- func (c *Client) ListByPrefix(ctx context.Context, prefix string) ([]ContainerState, error)
- func (c *Client) ListImages(ctx context.Context, repo string) ([]ImageInfo, error)
- func (c *Client) ListNetworksByPrefix(ctx context.Context, prefix string) ([]NetworkInfo, error)
- func (c *Client) Logs(ctx context.Context, containerID string, follow bool, since time.Time) (<-chan LogLine, <-chan error)
- func (c *Client) Ping(ctx context.Context) error
- func (c *Client) Prune(ctx context.Context, keep []string) PruneResult
- func (c *Client) PruneAnonymousVolumes(ctx context.Context) (PruneVolumesResult, error)
- func (c *Client) PruneBuildCache(ctx context.Context) (PruneBuildCacheResult, error)
- func (c *Client) PruneContainers(ctx context.Context, keep []string) (PruneContainersResult, error)
- func (c *Client) PruneDanglingImages(ctx context.Context) (PruneImagesResult, error)
- func (c *Client) Remove(ctx context.Context, id string, force bool) error
- func (c *Client) RemoveNetwork(ctx context.Context, name string) error
- func (c *Client) Start(ctx context.Context, id string) error
- func (c *Client) Stats(ctx context.Context, containerID string) (ContainerStats, error)
- func (c *Client) Stop(ctx context.Context, id string, timeout time.Duration) error
- func (c *Client) TestRegistryAuth(ctx context.Context, host, username, password string) error
- func (c *Client) UpdateResources(ctx context.Context, id string, resources Resources) error
- type ContainerSpec
- type ContainerState
- type ContainerStats
- type DiskUsage
- type Event
- type EventAction
- type ExecExitError
- type ExecSession
- type ExecTTYOptions
- type ImageInfo
- type LogLine
- type NetworkAttachment
- type NetworkInfo
- type PortBinding
- type PruneBuildCacheResult
- type PruneContainersResult
- type PruneImagesResult
- type PruneResult
- type PruneVolumesResult
- type RegistryAuth
- type Resources
- type Runtime
- type TTYRuntime
- type TTYSize
- type VolumeMount
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BindMount ¶
BindMount attaches a real host directory to a path inside a container, HostPath a real filesystem path on whichever node the container runs on, not a Docker volume name (VolumeMount's own doc comment above). internal/reconcile/application is the only caller today, translating store.ServiceBindMount into this at container- create time; internal/api gates persisting a non-empty BindMounts to AbilityRoot callers and rejects the deny-listed host paths internal/compose's own validateBindMountHostPath defines, both upstream of this package, which trusts HostPath the same way it already trusts VolumeMount.Name.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the real Runtime implementation, talking to the Docker Engine API over the local socket. It never shells out to the `docker` CLI, per the project's Docker Engine API only rule.
func NewClient ¶
NewClient builds a Client from the standard Docker environment (DOCKER_HOST, DOCKER_CERT_PATH, etc.), negotiating API version against whatever daemon is actually running rather than pinning one.
func (*Client) BridgeGatewayIP ¶
BridgeGatewayIP returns the gateway IP of Docker's default "bridge" network: the address from which a container attached to that network (the default whenever Create's NetworkingConfig is nil, which is every container this Client creates) can reach the host. Used by cmd/levelrail/mesh.go's containerDNSAddr to compute a container-reachable address for the mesh DNS server, since no WireGuard mesh IP is bound to anything yet.
func (*Client) DiskUsage ¶
DiskUsage reports Docker's own storage accounting via GET /system/df, aggregated into the four categories the General settings page shows. See the DiskUsage type's own doc comment for why this is a different number from the control plane's own DataDir usage.
func (*Client) EnsureNetwork ¶
EnsureNetwork implements Runtime.
func (*Client) EnsureVolume ¶
EnsureVolume implements Runtime. Docker's VolumeCreate is itself idempotent by name (creating a volume that already exists returns the existing volume, not an error), so this is a thin wrapper, not a check-then-create with its own race window.
func (*Client) Exec ¶
Exec implements Runtime. It creates an exec configuration on containerID, attaches to it to actually run cmd, and returns a ReadCloser streaming cmd's stdout back to the caller.
Docker's exec attach stream, with no tty (never requested here), is always multiplexed per pkg/stdcopy's frame format even though this call only asks for one logical output: stdout is copied straight through to the pipe this method returns, stderr is captured separately into a capped buffer purely so a non-zero exit has something useful to explain itself with. ContainerExecAttach itself has no notion of exit status, only ContainerExecInspect does, so exit code is checked only after the stream ends, in the background goroutine that drives the copy.
func (*Client) ExecTTY ¶
func (c *Client) ExecTTY(ctx context.Context, containerID string, opts ExecTTYOptions) (ExecSession, error)
ExecTTY implements TTYRuntime. Unlike Exec, the attach stream is raw rather than stdcopy-multiplexed: Docker merges stdout and stderr onto the PTY exactly as a real terminal does, so there is nothing to demultiplex and the bytes pass straight through in both directions.
func (*Client) ExecWithInput ¶
func (c *Client) ExecWithInput(ctx context.Context, containerID string, cmd []string, stdin io.Reader) (io.ReadCloser, error)
ExecWithInput implements Runtime. It is execCreateAttach plus a stdin side: everything Exec does (see Exec's own doc comment for the stdout/stderr/exit-code handling, unchanged here, both methods share streamExecOutput) plus a second goroutine that copies stdin into the exec session's write side and then closes it, the same "close the write half once the reader is exhausted" signal a real pipe or terminal gives a process reading until EOF.
The stdin-copy goroutine and streamExecOutput's own stdout-copy goroutine run concurrently, not sequentially: a command that starts producing output before this process has finished writing every byte of stdin (true of psql on a large dump, which echoes NOTICE/COPY progress lines as it goes) would otherwise deadlock, stdin blocked on a full kernel pipe buffer nobody is draining because this method hadn't gotten to the read side yet.
func (*Client) InspectByName ¶
InspectByName implements Runtime.
func (*Client) ListByPrefix ¶
ListByPrefix implements Runtime.
func (*Client) ListImages ¶
ListImages implements Runtime.
func (*Client) ListNetworksByPrefix ¶
ListNetworksByPrefix implements Runtime.
func (*Client) Logs ¶
func (c *Client) Logs(ctx context.Context, containerID string, follow bool, since time.Time) (<-chan LogLine, <-chan error)
Logs streams a container's log output, demultiplexed into a channel of individually stream-tagged, timestamped lines. Reads directly from the Docker Engine API's log endpoint, per the observability design rule that logs never come from the json-file driver's on-disk files, matching Events' own channel-based streaming shape rather than a blocking read loop.
Docker multiplexes stdout and stderr into a single byte stream with an 8-byte frame header per chunk whenever a container wasn't created with a TTY, which is every container this codebase creates (ContainerSpec has no TTY field, so container.Config.Tty is always Docker's zero value, false). stdcopy.StdCopy is the SDK's own demultiplexer for that framing, used here rather than hand-parsing the header format.
follow keeps the stream open and delivering new lines as the container produces them; without it, the stream ends once currently-buffered output is drained. since, if non-zero, asks Docker to only return lines at or after that time (a resume point for a caller that already ingested everything before it); the zero value returns everything Docker still has buffered.
The returned channels close together once the underlying log stream ends, for any reason: the container stopped, ctx was cancelled, or a read error occurred. A read error is reported on the error channel before both channels close; ctx cancellation and a clean stream end report no error, matching Events' own convention.
func (*Client) Ping ¶
Ping is a thin liveness check against the Docker daemon, wrapping the underlying SDK client's own Ping. Deliberately a method on the concrete *Client, not a new Runtime method: Runtime has 6+ fake implementations across internal/reconcile's controllers and internal/agent's test files, plus the real client here and the agent-side remote transport, so adding a method there would ripple into all of them for the sake of one read-only health signal that GET /api/v1/system/status needs. internal/api.DockerPinger is the narrow interface that actually consumes this, satisfied structurally by *Client alone.
func (*Client) Prune ¶
func (c *Client) Prune(ctx context.Context, keep []string) PruneResult
Prune runs every cleanup stage this package supports, in order: stopped containers not in keep, dangling images, anonymous unused volumes, then unused build cache. Each stage is independent; a failure in one does not stop the others, and every stage's own failure (if any) is collected into Errors rather than aborting the call, so a caller always gets back whatever the other stages did manage to clean up.
keep is every container name the caller currently considers desired (every application replica's target container name plus every database's container name); see PruneContainers' own doc comment for exactly why this cannot be computed inside this package.
func (*Client) PruneAnonymousVolumes ¶
func (c *Client) PruneAnonymousVolumes(ctx context.Context) (PruneVolumesResult, error)
PruneAnonymousVolumes removes only anonymous volumes (Docker's own randomly-generated 64-character hex name, assigned when a container mounts a volume without one, e.g. a Dockerfile's own VOLUME instruction with no matching app.yaml volume declaration) that are not currently attached to any container, running or stopped.
This project's own EnsureVolume always creates a caller-named volume; internal/reconcile/database's dataVolumeName ("db-<name>-data") is the only volume name this project's reconciler itself ever produces. So restricting to the anonymous-name pattern is not merely mirroring the Docker CLI's own conservative default (`docker volume prune` without --all only removes anonymous volumes as of Engine API >= 1.42): it is a second, independent, daemon-version-proof guarantee that this method can never remove a database's data volume, because no name this project creates can ever match the anonymous pattern. Named volumes, even ones that look unused right now, are always left alone; a named volume can look briefly unattached mid-replace during a database engine version bump (internal/reconcile/database.Controller.replaceContainer: stop, remove, then create, strictly sequential) without actually being abandoned.
"Not currently attached to any container" is computed from a fresh ContainerList across every container, running or stopped, rather than trusting the Engine API's own dangling-volume filter, whose exact semantics (does it count a stopped container's mounts as still-attached?) are not something this method's safety should depend on being right.
func (*Client) PruneBuildCache ¶
func (c *Client) PruneBuildCache(ctx context.Context) (PruneBuildCacheResult, error)
PruneBuildCache removes unused BuildKit cache records from the daemon's embedded BuildKit instance, the same one internal/build's own Client drives (internal/build/client.go's own doc comment: BuildKit runs inside dockerd, reachable only via this same *dockerclient.Client's own /grpc hijack, so this daemon's build-cache accounting is this project's real build cache, not an unrelated one). All: false, matching `docker builder prune`'s own default, removes only cache not currently InUse: a cache record still backing an in-flight build is left alone rather than this call racing a build that happens to be running at the same moment an operator clicks "clean up now."
func (*Client) PruneContainers ¶
PruneContainers stops and removes every non-running container on this daemon whose name is not in keep, using a direct list-then-remove sequence (each already-stopped container removed individually) rather than the Docker Engine API's own bulk POST /containers/prune (ContainersPrune in the SDK). See this file's own package doc comment for why: that bulk endpoint cannot exclude specific containers by name, and this project's reconciler can consider a stopped container still desired (about to be restarted in place). keep must include every container name the caller currently considers live, e.g. every application replica's target container name plus every database's container name, or this can race a reconcile pass that's about to restart one of them.
Only containers in Docker's "exited" or "created" state are candidates, matching the scope Docker's own bulk container prune targets. "paused" and "restarting" are transient, mid-lifecycle states, never a stable "this is garbage" signal, so they're left alone entirely; "dead", a rare failure state of Docker's own container teardown, is left for an operator to investigate rather than silently swept up.
Since every candidate is already confirmed non-running by the status filter, removal never needs Force: true (that flag exists to stop a running container before removing it, not relevant here) and never sets RemoveVolumes: cascading volume deletion through container removal would bypass PruneAnonymousVolumes' own, more careful safety checks; volume cleanup is that method's job alone.
One narrow, self-healing race remains, worth naming rather than treating as impossible: internal/reconcile/application's createAndStart calls Create then Start as two separate steps, so a container briefly exists in Docker's own "created" (not yet running) state between them. The candidate filter above (status: exited or created) matches that window on purpose, since a container that never got past Create after a genuine failure is real cruft this should still remove. But if the caller's own keep set was computed before that exact container's name existed (a deploy landing in the same instant a manual prune call is already mid-flight), this could remove it. Level-triggered reconcile recovers on its own next pass (one wasted create, a logged error, no data loss), the same tolerance this codebase already extends to other narrow eventual-consistency windows (see internal/docker/client_live_test.go's waitForStopped), so this is accepted rather than solved with, e.g., a lock this package has no good way to hold across a caller-supplied keep computation anyway.
func (*Client) PruneDanglingImages ¶
func (c *Client) PruneDanglingImages(ctx context.Context) (PruneImagesResult, error)
PruneDanglingImages removes only dangling images: untagged and not referenced by any container, Docker's own conservative default for `docker image prune` (no -a flag). The "dangling" filter is set explicitly here rather than relying on ImagesPrune's behavior with no filter at all, so this scope is guaranteed regardless of daemon version.
This deliberately never touches a tagged image, even one currently unreferenced by any container: this project's own rollback design keeps the previous N images pinned by tag so garbage collection cannot orphan a rollback target, and a tagged image sitting unused right now is exactly what that design depends on staying in place. The more aggressive "everything unused, tagged or not" cleanup `docker image prune -a` offers has no equivalent here, on purpose.
func (*Client) RemoveNetwork ¶
RemoveNetwork implements Runtime.
func (*Client) Stats ¶
Stats fetches one resource-usage snapshot for the container with this ID, via Docker's one-shot stats endpoint (a single accurate sample, not a subscription): correct for a periodic 15s-resolution collector, unlike the streaming variant which is built for a live-updating `docker stats`-style display instead.
func (*Client) TestRegistryAuth ¶
TestRegistryAuth asks the daemon to authenticate against host with username/password, the daemon's own `docker login`-equivalent check, without pulling anything. Same narrow-interface-on-the-concrete-type reasoning as Ping above: internal/api.RegistryAuthTester is the consumer boundary, satisfied structurally by *Client alone.
func (*Client) UpdateResources ¶
UpdateResources implements Runtime, via the Engine API's own ContainerUpdate call. This adjusts the container's cgroup limits in place; it never touches image, env, ports, mounts, or any other part of the container's configuration.
type ContainerSpec ¶
type ContainerSpec struct {
Name string
Image string
Ports []PortBinding
Env map[string]string
Resources *Resources
// Volumes are named Docker volumes to mount at create time. A
// database controller is the first caller; ordinary
// application containers leave this nil.
Volumes []VolumeMount
// BindMounts are real host directories to mount at create time
// (BindMount's own doc comment). Nil for every service before this
// field existed and for the ordinary stateless service today.
BindMounts []BindMount
// DNS lists nameserver IPs Docker writes into the container's
// /etc/resolv.conf, ahead of whatever the daemon would otherwise
// configure. Empty/nil is byte-identical to today: only a caller
// that resolved a real, container-reachable mesh DNS address
// (cmd/levelrail/mesh.go's containerDNSAddr) ever sets this, and
// only when the mesh is actually enabled and running.
DNS []string
// Labels are applied to the container verbatim via the Engine API's
// own label mechanism (container.Config.Labels), operator-supplied
// custom Docker labels (internal/spec.Service.Labels, an escape
// hatch for external tooling that keys off container labels). This
// package stays spec-agnostic (same reasoning as Image being a
// plain string, not something spec-aware, this struct's own doc
// comment above): it trusts Labels arrived here already validated,
// the same way it already trusts Env and every other field. See
// internal/spec.ValidateLabels for what's rejected before a caller
// ever builds a ContainerSpec with this set.
Labels map[string]string
// Network attaches the container to a non-default Docker network at
// create time, with Alias as the name sibling containers on that
// network can reach it by (Docker's embedded per-network DNS
// resolves it). Nil means Docker's default "bridge" network,
// unchanged from every container this codebase created before this
// field existed: that network has no embedded DNS, so this is what
// makes multi-service apps able to reach each other by name at all.
Network *NetworkAttachment
// RegistryAuth authenticates the image pull (Create's own ensureImage
// step) against a private registry. Nil means an unauthenticated
// pull, unchanged from every container this codebase created before
// this field existed.
RegistryAuth *RegistryAuth
// Command overrides the image's own default CMD. Nil means the
// image's own default, unchanged from every container this codebase
// created before this field existed. First caller:
// internal/reconcile/cloudflaretunnel, whose cloudflared image needs
// an explicit "tunnel run" argument rather than relying on the
// image's bare entrypoint.
Command []string
// Entrypoint overrides the image's own default ENTRYPOINT. Nil means
// the image's own default, unchanged from every container this
// codebase created before this field existed. First caller:
// internal/reconcile/registry, which needs to write an htpasswd file
// from injected env before handing off to the registry image's real
// entrypoint.
Entrypoint []string
}
ContainerSpec is desired state for a container a controller wants to exist.
No restart policy field, deliberately: the reconciler, not Docker, is meant to be the sole authority on "should this container be running." A Docker-native restart policy running alongside a reconciler that also restarts dead containers is two independent systems racing to make the same decision, exactly the kind of drift the reconciler's level-triggered design exists to avoid. Every container Levelrail creates gets Docker's "no" restart policy; staying running is the reconciler's job, proven live in nginxdemo (Phase 0).
No health check field either: the app spec's readiness/liveness probes are modeled on Kubernetes' prober pattern (the controller calls out to the container, not the container reporting its own health via Docker's HEALTHCHECK state machine), the same design choice ADR 002's Consequences section already commits to over Coolify's confirmed weaker alternative (health check disabled by default, gated entirely on Docker's own HEALTHCHECK). The prober itself belongs in the application controller (Phase 1), not here; this package only needs to make a container reachable, via Ports above.
type ContainerState ¶
type ContainerState struct {
ID string
Name string
Image string
Running bool
// Ports reflects live port bindings. Docker only actually binds a
// container's published ports once it's running, so this is empty
// for a created-but-not-yet-started container, not an error.
Ports []PortBinding
}
ContainerState is the observed state of a single container, trimmed to the fields a controller actually needs to decide what to do next.
type ContainerStats ¶
type ContainerStats struct {
// CPUPercent is 0-100 per core, i.e. a container fully using 2 cores
// on an otherwise idle host reports 200.0, matching `docker stats`'
// own convention, computed the same way (delta of cpu_usage over
// delta of system_cpu_usage, times online CPU count).
CPUPercent float64
// MemoryUsageBytes excludes page cache when the cgroup reports it
// separately (Stats above), matching `docker stats`' "used" figure
// rather than the raw cgroup usage counter, which double-counts
// reclaimable cache as "used."
MemoryUsageBytes uint64
MemoryLimitBytes uint64
// NetworkRxBytes/TxBytes are summed across every interface Docker
// reports for this container; a single container's total network
// usage is what the per-app metrics requirement asks for, not a
// per-interface breakdown.
NetworkRxBytes uint64
NetworkTxBytes uint64
// DiskReadBytes/WriteBytes are summed across every block device
// Docker's blkio accounting reports for this container's cgroup.
DiskReadBytes uint64
DiskWriteBytes uint64
}
ContainerStats is one point-in-time resource usage snapshot, trimmed and computed down to what the metrics store actually needs (CPU/memory/disk IO/network IO), same shape of simplification ContainerState already applies to Docker's raw container summary.
type DiskUsage ¶
type DiskUsage struct {
ImagesTotalBytes int64
// ImagesReclaimableBytes follows Docker's own `docker system df`
// methodology (unused = zero containers reference the image,
// tagged or not), so an operator sees the same number the docker
// CLI would show them directly on this host. It is deliberately a
// bigger number than what PruneDanglingImages actually frees: that
// call only ever removes the dangling (untagged) subset, on
// purpose, see PruneDanglingImages' own doc comment for why a
// tagged-but-currently-unused image is a real rollback target this
// project has an explicit design rule to protect.
ImagesReclaimableBytes int64
ContainersTotalBytes int64
// ContainersReclaimableBytes sums the writable-layer size (SizeRw,
// not SizeRootFs, which would double-count shared image layers) of
// every non-running container, matching what PruneContainers is
// eligible to remove before its own additional "currently desired"
// exclusion is applied.
ContainersReclaimableBytes int64
VolumesTotalBytes int64
// VolumesReclaimableBytes sums every volume Docker itself reports
// as unreferenced by any container (RefCount == 0), a bigger set
// than PruneAnonymousVolumes actually touches: that call only
// removes the anonymous subset, never a named volume regardless of
// use, because a volume that looks unused can be one mid-recreate
// during a database engine version bump, see
// PruneAnonymousVolumes' own doc comment.
VolumesReclaimableBytes int64
BuildCacheTotalBytes int64
// BuildCacheReclaimableBytes sums every build cache record not
// currently InUse, matching what PruneBuildCache removes.
BuildCacheReclaimableBytes int64
}
DiskUsage is Docker's own storage accounting: space claimed by images, containers, volumes, and BuildKit's build cache (BuildKit runs embedded inside this daemon, see internal/build/client.go's own doc comment, so this daemon's build-cache accounting is the real number for this project's own builds too, not an unrelated cache).
This is a materially different number from GET /api/v1/system/status's existing DataDirTotalBytes/DataDirFreeBytes fields: those measure the control plane's own APP_DATA_DIR (app source checkouts, telemetry storage, the SQLite file), a plain host filesystem path with no relationship to where Docker itself stores image layers, container writable layers, or named volumes (typically under Docker's own data-root, e.g. /var/lib/docker). An operator whose data dir looks healthy can still be moments from a full disk because of accumulated Docker state, which is exactly the gap this reports.
type Event ¶
type Event struct {
Action EventAction
ContainerName string
Time time.Time
}
Event is a trimmed container lifecycle event from the Docker event stream. Reconcilers key off ContainerName, not ID, since a container's ID changes across recreates but the name a controller manages does not.
type EventAction ¶
type EventAction string
EventAction mirrors the subset of Docker container lifecycle events a reconciler cares about.
const ( EventStart EventAction = "start" EventDie EventAction = "die" EventStop EventAction = "stop" )
The lifecycle actions a reconciler can receive from Events.
type ExecExitError ¶
ExecExitError is the trailing error Exec's and ExecWithInput's returned io.ReadCloser produce, once their stream ends, when cmd exited non-zero inside the container: see Runtime.Exec's own doc comment for why an exit code is folded into the error a later Read returns rather than carried as a second out-of-band field. Every caller before this type existed (internal/backup's Dumper and Restorer) only ever needed "err != nil means the dump/restore failed," so ExitCode and Stderr lived only inside the error's formatted message, unrecoverable except by parsing text. This type keeps that exact message (Error() reproduces streamExecOutput's original format string byte for byte, so nothing that already checks err != nil or matches on message substrings, e.g. client_live_test.go's "exited 7" check, changes behavior) while making ExitCode and Stderr recoverable via errors.As, for a caller like internal/api's exec endpoint that has to report a real exit code and stderr back to an HTTP client instead of only "the command failed."
func (*ExecExitError) Error ¶
func (e *ExecExitError) Error() string
type ExecSession ¶
ExecSession is one PTY-backed exec running inside a container: Read yields the terminal's output (stdout and stderr merged, as a real terminal merges them), Write feeds its input, Resize tells the process its window changed, and Close ends the session.
Read's trailing error carries the exec's own exit status the same way Runtime.Exec's does: io.EOF for a clean exit, *ExecExitError for a non-zero one. Close is best effort at stopping the remote process: the Engine API has no "kill this exec," so closing the PTY is the only lever, which ends a shell but cannot end a process that ignores its terminal hanging up.
type ExecTTYOptions ¶
ExecTTYOptions configures one interactive exec. Cmd is required; Env carries the terminal's own environment (TERM above all, without which most full-screen programs refuse to draw).
type ImageInfo ¶
ImageInfo is one tagged image available locally, used to discover rollback candidates, per the rule that the previous N images stay pinned so garbage collection cannot orphan a rollback target.
type LogLine ¶
type LogLine struct {
// Stream is "stdout" or "stderr".
Stream string
// Timestamp is Docker's own recorded time for this line, parsed from
// the log stream (Logs always requests Timestamps: true). Zero if the
// line didn't parse as the expected "<timestamp> <message>" shape;
// callers should fall back to their own clock in that case rather
// than treating a parse failure as fatal.
Timestamp time.Time
Message string
}
LogLine is one demultiplexed, timestamped line of container log output.
type NetworkAttachment ¶
NetworkAttachment is a container's non-default network attachment: which Docker network to join, and what DNS-resolvable alias sibling containers on that network can reach it by.
type NetworkInfo ¶
NetworkInfo is one Docker network, trimmed to what ListNetworksByPrefix's caller needs to decide whether it's still wanted.
type PortBinding ¶
type PortBinding struct {
ContainerPort int
HostPort int
// Protocol is "tcp" or "udp". Empty is treated as "tcp" by Create.
Protocol string
}
PortBinding maps one container port to a host port. In a ContainerSpec, HostPort of 0 means "let Docker assign one"; in an observed ContainerState it's always the concrete port actually bound, never 0.
Host ports, not direct container-IP routing, on purpose: this process runs as a plain host process today (the agent's single-node mode, where control plane and agent share a process), and container IPs on Docker's bridge network are only reachable from the host on Linux, not from Docker Desktop's macOS VM boundary. Every target this matters for (a Linux managed node, and this Mac during development) can always reach localhost:hostPort, so that's the one mechanism used everywhere rather than branching on platform.
type PruneBuildCacheResult ¶
PruneBuildCacheResult is one PruneBuildCache call's outcome.
type PruneContainersResult ¶
PruneContainersResult is one PruneContainers call's outcome.
type PruneImagesResult ¶
PruneImagesResult is one PruneDanglingImages call's outcome.
type PruneResult ¶
type PruneResult struct {
ContainersRemoved []string
ContainersReclaimedBytes uint64
ImagesRemoved []string
ImagesReclaimedBytes uint64
VolumesRemoved []string
VolumesReclaimedBytes uint64
BuildCacheRemoved []string
BuildCacheReclaimedBytes uint64
// Errors collects any stage's failure without aborting the rest:
// the same "one broken resource must not block others" principle
// internal/reconcile/application.Controller.removeContainers
// already applies to stale-container cleanup, applied here across
// prune stages instead of across containers.
Errors []string
}
PruneResult is what one Prune call actually did, returned as a single aggregate report so a caller (handleSystemPrune) can show an operator exactly what was removed, not just "done."
type PruneVolumesResult ¶
PruneVolumesResult is one PruneAnonymousVolumes call's outcome.
type RegistryAuth ¶
RegistryAuth is a plaintext username/password pair for pulling a private image, resolved by the caller (internal/reconcile/application) from internal/secrets immediately before Create; never persisted by this package.
type Resources ¶
type Resources struct {
MemoryBytes int64
NanoCPUs int64
// SwapMemoryBytes is Docker's own MemorySwap: total memory plus swap
// combined, only meaningful alongside MemoryBytes.
SwapMemoryBytes int64
// CPUSetCPUs pins the container to specific host CPUs, Docker's own
// cpuset-cpus format (e.g. "0-3" or "0,2").
CPUSetCPUs string
}
Resources caps a container's memory and CPU, in the Engine API's own units directly (bytes, nano-CPUs) rather than an invented "cores as float64" indirection. Translating from app.yaml's human-friendly units (spec.Resources: "512Mi", 0.5 cores) into these is the caller's job, keeping this package spec-agnostic, same as ContainerSpec.Image being a plain string rather than something spec-aware.
type Runtime ¶
type Runtime interface {
// InspectByName returns the current state of the container with this
// name, or (nil, nil) if no such container exists. It never returns
// an error for "not found": that's a valid observed state, not a
// failure.
InspectByName(ctx context.Context, name string) (*ContainerState, error)
// Create makes a container from spec but does not start it. Returns
// the new container's ID.
Create(ctx context.Context, spec ContainerSpec) (id string, err error)
// Start starts an existing container by ID.
Start(ctx context.Context, id string) error
// Events streams container lifecycle events until ctx is cancelled.
// The error channel receives at most one error (a stream failure)
// and is then closed; the event channel is closed once the stream
// ends for any reason.
Events(ctx context.Context) (<-chan Event, <-chan error)
// ListImages returns every locally-present tag under repo (e.g.
// "levelrail/myapp"), newest first, for rollback candidate discovery.
// An empty result and a nil error both mean "no images found", not
// an error: a service that's never been built has no images yet.
ListImages(ctx context.Context, repo string) ([]ImageInfo, error)
// ListByPrefix returns every container (running or not) whose name
// starts with prefix, for a controller to find every container
// belonging to a service it manages, not just the one canonical name
// nginxdemo's simpler world got away with.
ListByPrefix(ctx context.Context, prefix string) ([]ContainerState, error)
// Stop stops a running container, sending Signal (empty means
// Docker's default, SIGTERM) and waiting up to timeout before
// forcing it. A negative timeout waits indefinitely.
Stop(ctx context.Context, id string, timeout time.Duration) error
// Remove deletes a container. force also removes a still-running
// one (stopping it first); without force, removing a running
// container is an error, matching Docker's own default.
Remove(ctx context.Context, id string, force bool) error
// UpdateResources applies new memory, CPU, swap, and cpuset limits to
// an already-running container in place, via the Engine API's own
// ContainerUpdate call. Unlike every other resource-affecting change
// a controller can want (image, env, ports, health check), this one
// has a real live-update path: Docker itself supports adjusting a
// running container's cgroup limits without stopping it, so a
// reconciler can converge a resource-limit-only diff without the
// blue-green cutover a full recreate would otherwise force.
UpdateResources(ctx context.Context, id string, resources Resources) error
// EnsureVolume creates a named Docker volume if it doesn't already
// exist. Idempotent: calling it for a volume that's already present
// is not an error, matching the Engine API's own VolumeCreate
// semantics (creating by an existing name returns that volume
// unchanged rather than failing).
EnsureVolume(ctx context.Context, name string) error
// EnsureNetwork creates a Docker bridge network named name if it
// doesn't already exist, returning its ID either way. Idempotent:
// inspect first, create only on a genuine miss, the same shape the
// real implementation's ensureImage already uses for images.
// Reconcile calls happen often (event-driven plus periodic resync),
// so this must be safe to call on every pass, not just the first.
EnsureNetwork(ctx context.Context, name string) (id string, err error)
// RemoveNetwork deletes a Docker network by name. Removing a network
// that doesn't exist is not an error: cleanup is level-triggered and
// may run against state that's already converged.
RemoveNetwork(ctx context.Context, name string) error
// ListNetworksByPrefix returns every Docker network whose name
// starts with prefix, for a reconciler to diff observed networks
// against current desired state (internal/reconcile/application's
// NetworkCleanupController).
ListNetworksByPrefix(ctx context.Context, prefix string) ([]NetworkInfo, error)
// Exec runs cmd inside the already-running container containerID and
// returns its stdout as a stream, using the Engine API's exec
// facility (ContainerExecCreate then ContainerExecAttach), the same
// two calls `docker exec` itself is built on. This is the one Runtime
// method that reaches into a process running inside a container
// rather than managing the container's own lifecycle; it exists for
// internal/backup's Dumper, which has no other way to run a
// database's own native dump tool (pg_dump, mysqldump, redis-cli)
// against a live container without shelling out, which is exactly
// what every other method on this interface already exists to avoid.
//
// The returned ReadCloser carries only cmd's stdout. Stderr is
// captured by the implementation and, if cmd exits non-zero,
// surfaced as the error a later Read returns once the stream ends:
// the same "an error can arrive with or after the final bytes"
// contract every io.Reader already promises, so a caller only ever
// has one place to check for failure instead of a second out-of-band
// exit-code field. Close must be called once the caller is done with
// the stream, success or failure alike, to release the underlying
// exec connection.
Exec(ctx context.Context, containerID string, cmd []string) (io.ReadCloser, error)
// ExecWithInput is Exec plus a stdin stream: it runs cmd inside
// containerID exactly as Exec does (same stdout/stderr handling, same
// exit-code-becomes-a-trailing-error contract), except cmd's stdin is
// wired to stdin instead of being left unattached. This exists for
// internal/backup's Restorer, which needs to pipe a downloaded dump
// into psql/mysql running inside a live container; Exec itself can't
// do this; ContainerExecCreate has to be told AttachStdin: true up
// front for the exec session to have a stdin pipe at all, and every
// other caller of Exec (ContainerDumper) never needs one, so this is a
// second method rather than a third parameter Exec's own existing
// call site would otherwise have to grow to accommodate.
//
// Implementations read stdin to completion (EOF) and then signal
// end-of-input to the exec'd process the way closing stdin on a real
// terminal would, before returning; a command like psql that reads
// until EOF and then exits depends on that signal to ever finish, not
// only on stdin's bytes arriving. Callers are not required to provide
// a stdin that reaches EOF on its own if the command they're running
// doesn't need one; passing an empty reader is equivalent to Exec's
// own "nothing attached to stdin" behavior for that command.
//
// The returned ReadCloser carries stdout, identically to Exec's; Close
// must be called once done, same contract.
ExecWithInput(ctx context.Context, containerID string, cmd []string, stdin io.Reader) (io.ReadCloser, error)
}
Runtime is the surface reconcile controllers are allowed to depend on. The real implementation (Client, in client.go) talks to the Docker Engine API. Tests use a hand-written fake: see nginxdemo's test file for the pattern every future controller's tests should follow.
type TTYRuntime ¶
type TTYRuntime interface {
ExecTTY(ctx context.Context, containerID string, opts ExecTTYOptions) (ExecSession, error)
}
TTYRuntime is the interactive half of Runtime's exec surface, kept separate because Runtime is implemented by a dozen narrow test fakes that have no interactive exec to offer and no reason to grow a stub for one. Every real implementation (docker.Client, the agent's gRPC transport, and the in-process transport wrapping either) implements both, so a caller resolving a node's Runtime type-asserts to this and reports "not supported on this node" if it fails.
type TTYSize ¶
TTYSize is a terminal's character grid. Zero in either field means "let the daemon pick," the same as leaving Docker's own ConsoleSize unset.
type VolumeMount ¶
type VolumeMount struct {
Name string
ContainerPath string
// ReadOnly mounts the volume read-only inside the container. False
// (read-write) for every caller before internal/backup's volume
// archiver started setting it explicitly true, so this is
// byte-identical to every mount created before this field existed.
ReadOnly bool
}
VolumeMount attaches one named Docker volume to a path inside a container, e.g. Postgres's /var/lib/postgresql/data or Redis's /data. Name is a Docker volume name, not a host path: bind mounts aren't exposed here, keeping this package's surface to what a single-node managed database actually needs today.