dockerapi

package
v0.0.0-...-cd8c9d7 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package dockerapi talks to a docker engine, and is the only package in this repo that holds an engine client.

It exists because container execution used to spawn the `docker` binary, and a binary answers questions a library does not. Which daemon (the context store, not just DOCKER_HOST — see host.go) and which credentials (the config.json auths and the credential helpers — see auth.go) both arrived free with the CLI and stop being free the moment the daemon is reached any other way. Those answers live here rather than at the call sites, so there is one place that knows what "the docker CLI would have done" means.

It is deliberately a separate package from internal/shell rather than a few more imports inside it. shell is reached by nearly everything and had been stdlib-only; an engine client in that leaf makes "stdlib only" false for every package downstream of it, which is the same reasoning that put an ssh client in internal/venue instead.

Index

Constants

View Source
const DefaultHost = "unix:///var/run/docker.sock"

DefaultHost is the daemon a machine that says nothing else is assumed to have.

View Source
const SignalledExitCode = -1

SignalledExitCode is the status reported for a command that started and was then cut off rather than choosing its own — the same sentinel os/exec gives for a locally signalled process.

Variables

This section is empty.

Functions

func IsImageNotFound

func IsImageNotFound(err error) bool

IsImageNotFound reports whether err is the daemon saying it does not have the image asked for.

Exported because creating a container is the one place a caller has to act on it rather than report it: `docker run` pulled implicitly, and the engine API does not, so the caller restores that by pulling and asking again.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports a daemon answering that the thing asked about does not exist.

func OverlayOptions

func OverlayOptions(lower, upper, work string) map[string]string

OverlayOptions are the local driver's options for a copy-on-write child of lower; all three are daemon-side paths, and upper and work must share a filesystem. ponytail: paths are joined unescaped, safe while they are daemon Mountpoints; validate ',' and ':' if a caller ever passes its own.

func RegistryAuth

func RegistryAuth(ctx context.Context, ref string) string

RegistryAuth returns the value to send with a pull of ref, or "" for an anonymous one.

Empty is a real answer, not a failure: most pulls are anonymous, and sending a blank credential is NOT the same as sending none — a registry that serves anonymous reads can reject an empty login outright.

func ResolveHost

func ResolveHost() (string, error)

ResolveHost answers which daemon this process should talk to, in the order a docker CLI would: DOCKER_HOST, then DOCKER_CONTEXT, then the current context in the configuration directory, then the platform default.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a connection to one docker daemon.

func New

func New(host string) (*Client, error)

New connects to a daemon. An empty host resolves the same way a docker CLI would; a non-empty one is used verbatim, which is how a venue aims a step at the socket it forwards to a worker.

The client negotiates its API version with whatever daemon it finds, which this version does by default. That matters and is worth naming: the compiled-in version is newer than plenty of running daemons, and a pinned client would fail every call against an older one on the version rather than on anything about the request.

func NewDialer

func NewDialer(name string, dial func(ctx context.Context) (net.Conn, error)) (*Client, error)

NewDialer reaches a daemon through dial, for a socket with no address of its own here (one forwarded over ssh); name is what errors call it.

func (*Client) Close

func (c *Client) Close() error

Close releases the client's connections.

func (*Client) ContainerLogTail

func (c *Client) ContainerLogTail(ctx context.Context, id string, lines int) string

ContainerLogTail returns the last lines the container produced.

For a container that died at birth this is the image's own error message, and it is the whole diagnosis. Best effort by design: a failure to read it must not replace the failure being explained.

func (*Client) ContainerState

func (c *Client) ContainerState(ctx context.Context, id string) (running bool, exitCode int, err error)

ContainerState reports whether a container is still running, and the status it exited with if it is not.

func (*Client) CreateContainer

func (c *Client) CreateContainer(ctx context.Context, spec ContainerSpec) (string, error)

CreateContainer defines a container without starting it, returning its id.

An image the daemon does not have fails HERE, naming the image, rather than later as a container that would not start.

func (*Client) CreateHolder

func (c *Client) CreateHolder(ctx context.Context, name, image string, labels map[string]string, mounts []string) (string, error)

CreateHolder creates a never-started container whose only job is to mount volumes for the archive endpoints; image must already be present.

func (*Client) CreateVolume

func (c *Client) CreateVolume(ctx context.Context, name string, labels, driverOpts map[string]string) (Volume, error)

CreateVolume makes a local-driver volume, or returns an existing one of that name untouched, old contents and old options alike (the daemon's answer, not an error); options are checked only at first mount.

func (*Client) Exec

func (c *Client) Exec(ctx context.Context, containerID string, opts ExecOptions) (int, error)

Exec runs one command in a running container and returns its exit status.

The status is DATA, not an error: a command that exits nonzero has answered, and only a failure to run it at all is reported as an error here. That distinction is what the whole pipeline uses to tell a step saying no from the machinery breaking, and collapsing the two would classify every red step as infrastructure.

func (*Client) GetArchive

func (c *Client) GetArchive(ctx context.Context, id, src string) (io.ReadCloser, error)

GetArchive streams src out of the container as tar; the caller closes it.

func (*Client) Host

func (c *Client) Host() string

Host is the address this client talks to, for a message that has to say which daemon it means.

func (*Client) ImagePresent

func (c *Client) ImagePresent(ctx context.Context, image string) bool

ImagePresent reports whether the daemon already holds image.

A local call, which is what makes it worth asking before every pull: a warm daemon costs milliseconds and no network. It is also what keeps a locally-BUILT image working — one that exists in no registry is found here, so nothing is pulled and nothing 404s.

Two answers mean absent, and the second is easy to miss: an image name that is not a reference at all ("--privileged", "NOT A REF") is decided HERE, with the daemon's own invalid-argument answer kept only as a backstop for a grammar stricter than this one. Reading that as present pushes the failure into whichever step first needs it, reported as a container that would not start; reading it as absent sends it to the pull, which names the value and fails startup. The docker CLI behaves the same way, because `docker image inspect` exits nonzero for both.

Anything else — a daemon that is unreachable or unwell — is reported as present, deliberately. That is a problem the pull would hit too, and the pull says so far better than a message synthesised here would.

func (*Client) InspectVolume

func (c *Client) InspectVolume(ctx context.Context, name string) (Volume, error)

InspectVolume errors with IsNotFound for a volume the daemon does not have.

func (*Client) ListContainers

func (c *Client) ListContainers(ctx context.Context, labels map[string]string) ([]Container, error)

ListContainers returns every container — running or not — carrying all of the given labels.

All of them, not any: the filter is what keeps a sweep from removing a container belonging to another machine or another live process, which is the one mistake in this area that breaks a working build rather than merely failing to tidy up after a broken one.

Stopped containers are included because they are the orphans most worth reclaiming: a steps process killed outright leaves a container whose keepalive eventually expires, and it then sits as an inert Exited row that nothing else will ever remove.

func (*Client) ListVolumes

func (c *Client) ListVolumes(ctx context.Context, labels map[string]string) ([]Volume, error)

ListVolumes returns the volumes carrying every one of labels; no labels is refused, since a sweep over the result would reach every volume on the daemon.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

Ping reports whether the daemon is actually reachable.

Constructing a client does not connect — it only parses the address — so this is what a preflight check has to call. It is the `docker info` of the old shape, minus the payload nothing read.

func (*Client) Platform

func (c *Client) Platform(ctx context.Context) (string, string, error)

Platform is the daemon's own OS and architecture, in Go's spelling (linux, arm64), for a record of where a step ran.

func (*Client) PublishedPort

func (c *Client) PublishedPort(ctx context.Context, id, port string) (string, error)

PublishedPort is the host port a started container's port was bound to.

func (*Client) Pull

func (c *Client) Pull(ctx context.Context, image string, progress io.Writer) error

Pull fetches image, reporting the daemon's progress to progress.

The reporting is not decoration. An image pull is minutes of silence otherwise, and it happens at startup where the operator is the only audience — the same reason the docker CLI draws one and the same reason this is not routed through a logger.

The errors are the subtle part: a pull reports failure INSIDE the stream rather than on the HTTP call, so a caller that only checked what ImagePull returned would read "repository does not exist" as a successful pull and fail later, in a step, on an image that was never fetched.

func (*Client) PutArchive

func (c *Client) PutArchive(ctx context.Context, id, dst string, content io.Reader) error

PutArchive unpacks a tar stream at dst inside the container, which lands in whatever volume is mounted there.

func (*Client) RemoveContainer

func (c *Client) RemoveContainer(ctx context.Context, id string) error

RemoveContainer deletes a container by id or name, stopping it first.

A container that is already gone is a SUCCESS, not a failure. It happens constantly — one that exited on its own, a daemon that restarted, a teardown racing the sweep — and reporting it would put an error in the log of a run that cleaned up perfectly well. This used to be decided by matching the words "No such container" in the CLI's output, a sentence the daemon was free to reword at any time; the daemon now says which kind of failure it is.

So is a container the daemon is ALREADY removing, which it reports as a conflict rather than as a missing container. That is the ordinary outcome for a self-removing container whose caller also reclaims it by name — the belt and the braces arriving together — and calling it a failure fills the log of a perfectly clean run with warnings about the cleanup working twice.

RemoveVolumes takes the container's anonymous volumes with it (an image's VOLUME, like postgres's PGDATA); named volumes are never touched.

func (*Client) RemoveVolume

func (c *Client) RemoveVolume(ctx context.Context, name string) error

RemoveVolume treats an absent volume as removed, but never forces one in use: a lower pulled from under a mounted overlay is undefined behaviour.

func (*Client) RunOnce

func (c *Client) RunOnce(ctx context.Context, spec ContainerSpec) (int, string, string, error)

RunOnce runs spec to completion and removes it: its exit code, and stdout kept apart from stderr so a one-line answer can be read as one.

func (*Client) SettleFor

func (c *Client) SettleFor(ctx context.Context, id string, bound time.Duration) (died bool, exitCode int, err error)

SettleFor waits up to bound for a container to die, reporting whether it did and with what status.

This is the question "did it die AT BIRTH", and it cannot be answered by looking: starting a container reports that it STARTED, and an image whose entrypoint swallows the keepalive is still running at the instant the start returns — it exits a few milliseconds later. Inspecting immediately says "running" and is simply wrong, after which every exec reports a container that does not exist, naming neither the image nor the reason.

The bound is a real cost: a healthy container pays it once per step. It is paid deliberately, and it is not new — the shape this replaced spawned a whole `docker inspect` process to ask the same question, which cost more.

func (*Client) StartContainer

func (c *Client) StartContainer(ctx context.Context, id string) error

StartContainer runs a created container.

type Container

type Container struct {
	ID     string
	Labels map[string]string
}

Container is what a caller needs in order to decide whether a container is theirs and to act on it. Deliberately not the API's own type: this package exists so that the shape of an engine response stops at its boundary.

type ContainerSpec

type ContainerSpec struct {
	Image string
	// Cmd is the container's command, passed as an argv. It is subject to the
	// image's own ENTRYPOINT exactly as `docker run` would be — deliberately,
	// because an image that swallows the command is a real misconfiguration
	// and overriding the entrypoint here would hide it.
	Cmd  []string
	Name string
	// WorkingDir is also the bind mount's path on both sides; empty mounts
	// nothing and takes the image's own workdir.
	WorkingDir string
	// MountDir is the tree to bind when it is not WorkingDir itself, for a step whose commands run in a SUBDIRECTORY of what has to be mounted. A placed agent with dir: is the case: the whole step directory must cross to the worker so its declared outputs resolve, while the model works one level down. Empty keeps WorkingDir as both, which is every other step.
	MountDir string
	// Env are already-resolved NAME=value pairs. A variable the caller could
	// not resolve is simply absent from this, which is what makes naming an
	// optional variable safe.
	Env         []string
	Labels      map[string]string
	User        string
	Network     string
	Privileged  bool
	CPUShares   int64
	MemoryBytes int64
	// Init supplies a real PID 1. It matters for a session container more
	// than for a one-shot: processes an exec leaves behind reparent to PID 1,
	// and a keepalive that is a bare `sleep` would never reap them.
	Init bool
	// AutoRemove has the daemon delete the container when it exits. Right for
	// a foreground run whose lifetime IS the process's; wrong for a session
	// container, whose postmortem is the whole diagnosis when an image
	// rejects the keepalive.
	AutoRemove bool
	// ExtraHosts are `name:address` entries, for reaching the host from
	// inside by a name that works on Docker Desktop and Linux alike.
	ExtraHosts []string
	// Mounts are bind mounts beyond WorkingDir, as `source:target[:ro]`.
	Mounts []string
	// Volumes are named volumes as `name:target`; set, they replace the tree bind, since their tree is on the daemon already.
	Volumes []string
	// OpenStdin keeps the container's stdin open for a caller that attaches
	// to it, which a foreground run does and a session container does not.
	OpenStdin bool
	// Publish are container ports ("5432/tcp") bound to an ephemeral port on
	// 127.0.0.1 only; PublishedPort says which. Loopback because nothing
	// that asks for one wants it reachable from another host.
	Publish []string
}

ContainerSpec is everything that decides how a container is configured.

Values, not flags. The distinction is worth naming because it removes a class of problem rather than guarding against it: there is no argument vector for an image name to be misread as an option in, and no `-e NAME` forwarding trick needed to keep a secret out of one, since the environment travels in a request body that no process list can show.

type ExecOptions

type ExecOptions struct {
	Cmd []string
	// Stdin is fed to the command; nil attaches nothing, so a command that
	// reads stdin sees an immediate end of file rather than waiting.
	Stdin          io.Reader
	Stdout, Stderr io.Writer
	// Env adds NAME=value pairs for this command only, over the container's own.
	Env []string
}

ExecOptions is one command to run in an already-running container.

type Volume

type Volume struct {
	Name       string
	Mountpoint string
	Labels     map[string]string
	// CreatedAt is zero when the daemon did not say or said something unparseable.
	CreatedAt time.Time
}

Volume is a daemon-side directory; Mountpoint is its path on the DAEMON's disk, which is what an overlay's options must name.

Jump to

Keyboard shortcuts

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