Documentation
¶
Overview ¶
internal/driver/contract.go
internal/driver/docker.go
internal/driver/docker_exec.go
internal/driver/fake.go
Index ¶
- Constants
- func RunContract(t *testing.T, newDriver func(t *testing.T) (Driver, func()))
- type Docker
- func (d *Docker) Capacity(ctx context.Context) (int, int, error)
- func (d *Docker) Create(ctx context.Context, spec Spec) (Handle, error)
- func (d *Docker) Destroy(ctx context.Context, id string) error
- func (d *Docker) DestroyContainer(ctx context.Context, id string) error
- func (d *Docker) Inspect(ctx context.Context, id string) (Handle, error)
- func (d *Docker) List(ctx context.Context) ([]Listed, error)
- func (d *Docker) Prepull(ctx context.Context, ref string) error
- func (d *Docker) RemoveWorkspace(ctx context.Context, sessionID string) error
- func (d *Docker) Resume(ctx context.Context, id string) error
- func (d *Docker) Snapshot(ctx context.Context, id, ref string, stripEnv []string) (Snapshot, error)
- func (d *Docker) Suspend(ctx context.Context, id string, warm bool) error
- type DockerOpts
- type Driver
- type Fake
- func (f *Fake) Capacity(_ context.Context) (int, int, error)
- func (f *Fake) Create(_ context.Context, spec Spec) (Handle, error)
- func (f *Fake) Destroy(ctx context.Context, id string) error
- func (f *Fake) DestroyContainer(_ context.Context, id string) error
- func (f *Fake) Inspect(_ context.Context, id string) (Handle, error)
- func (f *Fake) LastSpec() Spec
- func (f *Fake) List(_ context.Context) ([]Listed, error)
- func (f *Fake) Prepull(_ context.Context, ref string) error
- func (f *Fake) Pulls() []string
- func (f *Fake) RemoveWorkspace(_ context.Context, sessionID string) error
- func (f *Fake) Resume(_ context.Context, id string) error
- func (f *Fake) Snapshot(_ context.Context, id, ref string, stripEnv []string) (Snapshot, error)
- func (f *Fake) Strips() [][]string
- func (f *Fake) Suspend(_ context.Context, id string, warm bool) error
- func (f *Fake) Volumes() []string
- type Handle
- type HomeMount
- type Listed
- type RepoSpec
- type Snapshot
- type Spec
- type State
Constants ¶
const MaxSetupBytes = 512 << 10
MaxSetupBytes caps Spec.Setup (and Spec.Init, which rides the same channel) before encoding. A script rides to the container in its environment block, which is not an unbounded place to put a file: an oversized one fails deep inside `docker run` with an errno that names nothing useful, or is silently truncated into a script that does something other than what the environment declared. Rejecting it in Create, before anything with a side effect runs, is what turns that into an error naming the limit and the input that broke it.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Docker ¶
type Docker struct {
// contains filtered or unexported fields
}
func NewDocker ¶
func NewDocker(opts DockerOpts) *Docker
func (*Docker) Capacity ¶
Capacity counts only slot-occupying containers — running or paused (warm suspend) — not cold-parked (stopped) ones. A stopped container still exists (its volume is kept for Resume) but isn't using a runtime slot, so counting it here would make cold sessions eat capacity they don't need. The two `--filter status=` flags OR together in `docker ps`, unlike most docker ps filters of the same key which AND across categories but OR within one — see https://docs.docker.com/reference/cli/docker/container/ls/#filter.
func (*Docker) Destroy ¶
Destroy removes the container and the session's workspace volume with it — the whole teardown, for an explicit `rainier rm`. A session's volume is the session's: leaving it behind on THIS path would grow the host's disk by one workspace per session ever removed, with nothing left that names it — the container that did is exactly what Destroy just removed.
Order matters. The session id is read off the container's label BEFORE the container goes, because afterwards there is nothing left to ask; and the volume is removed AFTER, because docker refuses to remove a volume any container still references, stopped ones included.
The agent home is not removed here, and nothing in this driver removes one. It is keyed by (creator, workspace), not by session: other sessions of the same person in the same workspace may be mounted on it at this moment, and taking it would log every one of them out of every agent they had signed in. Nothing reconstructs its name from a container either — the only place it appears is the mount the create was handed.
func (*Docker) DestroyContainer ¶
DestroyContainer removes the container and leaves the workspace volume standing — the crash path (see driver.Driver.DestroyContainer for why the two are separate operations). It is deliberately the whole of `docker rm -f`: nothing here reads a label or derives a volume name, so there is no code path from "reclaim this slot" to "remove a volume" at all.
func (*Docker) List ¶
List returns every rainier-labeled container, in any state — unlike Capacity, which only counts slot-occupying ones. Used by runnerd.Recover to rebuild its registry after a restart.
func (*Docker) Prepull ¶
Prepull fetches ref so a create that names it doesn't pay for the pull.
An empty ref is rejected before the exec: it means the command upstream carried no ref at all, and `docker pull ""` reports that as a CLI usage error whose text reads like a defect in this driver rather than in its caller.
func (*Docker) RemoveWorkspace ¶
RemoveWorkspace removes sessionID's workspace volume, tolerating one that is already gone.
type DockerOpts ¶
type DockerOpts struct {
Image string // default session image
Network string // internal docker network name (created by fleet compose)
TotalSlots int // capacity budget
Label string // docker label key marking rainier-managed containers
SessionSeccompProfile string // optional host path passed to Docker as seccomp=<path>
SessionAppArmorProfile string // optional loaded host profile passed as apparmor=<name>
}
type Driver ¶
type Driver interface {
Create(ctx context.Context, spec Spec) (Handle, error)
Suspend(ctx context.Context, id string, warm bool) error // warm=pause, cold=stop
Resume(ctx context.Context, id string) error
// Snapshot commits a session's current filesystem as an image.
//
// ref, when non-empty, is the exact tag to commit to, and comes back in
// the returned Snapshot verbatim. It is opaque here on purpose: the
// content-addressed environment refs Plan 4 caches
// (rainier-env:<envID>-<setupHash>) are minted by CONTROLD, which is the
// only component that knows an environment's setup hash and the only one
// that can make the same environment resolve to the same ref on every
// runner. A driver that decorated or re-derived the name would break that
// addressing: controld would record one ref while the image lived under
// another.
//
// An empty ref asks the driver to mint one of its own. That is the local
// dev surface's case (POST /sessions/{id}/snapshot names no environment),
// and the only caller that has no ref to give.
//
// stripEnv names environment variables that must NOT survive into the
// committed image's configuration. A commit captures the container's whole
// config, environment block included, and two classes of variable in there
// are actively harmful once they are baked into an image every later
// session boots:
//
// - the setup channel (RAINIER_SETUP_B64 and its timeout). sessiond runs
// a setup script whenever that variable is present, so an image
// carrying it makes every cache-booted session re-run the setup its
// cache exists to skip — the control plane deliberately dispatches no
// script, and the container runs one anyway.
// - every value the layer above injected as Spec.Env, which is where an
// environment's DECRYPTED secrets live. Baked into an image they are
// readable by anyone with a docker socket on that runner, and would be
// published outright by any registry-backed distribution of the cache.
//
// The keys are stripped, not deleted: a driver sets each to the empty
// string, which every consumer treats as absent (sessiond gates on
// `RAINIER_SETUP_B64 != ""`; a credential read as "" fails closed rather
// than silently wrong). A later `docker run -e K=V` still overrides it, so
// a session booted from the cache is handed its secrets fresh, as it must
// be — the environment's secret_refs are resolved per create, not per
// image.
Snapshot(ctx context.Context, id, ref string, stripEnv []string) (Snapshot, error)
// Prepull fetches ref into this runner's local image store ahead of a
// create that will need it, so the create isn't the thing paying for the
// pull. Advisory by design — controld dispatches it without waiting on
// the answer, and a failure costs nothing worse than the slow create the
// prepull was trying to avoid.
Prepull(ctx context.Context, ref string) error
// Destroy removes the container AND the session's workspace volume: the
// whole teardown, for the path where a user asked for the session to stop
// existing (`rainier rm`). It is DestroyContainer followed by
// RemoveWorkspace, and it is the only one of the three that takes a
// workspace away as a side effect of removing a container.
//
// The agent home (Spec.Home) is NOT taken. It belongs to the (creator,
// workspace), not to this session: other sessions of the same person in
// the same workspace may be mounted on it right now, and removing it
// would log them all out of every agent they had signed in.
Destroy(ctx context.Context, id string) error
// DestroyContainer removes the container and NOTHING else — the session's
// workspace volume survives.
//
// This is the crash path. When a container dies on its own, runnerd
// removes what is left of it to reclaim the capacity slot, and that
// removal is not a user asking for their files to be thrown away: a
// crashed session is precisely the case someone most wants their work
// back from, and /workspace is where every hour of it lives. Before the
// split, that path called Destroy — the same call `rainier rm` makes —
// so a container dying quietly took the workspace with it.
//
// The two are separate METHODS rather than a flag because the difference
// is not a parameter of one operation, it is which of two operations the
// caller means; a boolean would put "delete the user's work" one inverted
// condition away from "reclaim a slot".
DestroyContainer(ctx context.Context, id string) error
// RemoveWorkspace removes sessionID's workspace volume
// (rainier-ws-<sessionID>), the second act of an explicit teardown after
// a crash deliberately kept it. It takes the SESSION id, not a driver
// handle, because by the time anything wants a kept workspace gone the
// container that could have named it is long gone.
//
// An absent volume is success, not an error: every caller is a teardown
// path that may be running second (a full Destroy already took it, a
// reconcile got there first, an rm was retried), and failing there would
// turn a completed teardown into a reported failure. An empty sessionID
// is a no-op — "rainier-ws-" alone is a real volume name, and no driver
// may be talked into removing whatever is under it.
RemoveWorkspace(ctx context.Context, sessionID string) error
Inspect(ctx context.Context, id string) (Handle, error)
Capacity(ctx context.Context) (used, total int, err error)
// List returns every rainier-labeled resource, in any state (running,
// suspended, or otherwise still present) — unlike Capacity, which counts
// only slot-occupying ones. Used by runnerd.Recover to rebuild its
// registry from what the driver actually has after a restart.
List(ctx context.Context) ([]Listed, error)
}
type Fake ¶
type Fake struct {
// contains filtered or unexported fields
}
func (*Fake) Destroy ¶
Destroy is the whole teardown: the container AND the session's volume, in that order, exactly as in the docker driver. Suspend/Resume deliberately don't touch the volume, so this and RemoveWorkspace are the only two places a workspace disappears.
func (*Fake) DestroyContainer ¶
DestroyContainer removes the item and leaves its volume in place — the fake's model of the crash path. Getting this right in the FAKE is what makes runnerd's crash tests worth running: they assert on nothing but what this records, so a fake that dropped the volume here would go green against precisely the bug the split exists to prevent.
func (*Fake) Prepull ¶
Prepull records ref instead of fetching anything. Prepull changes nothing else about a driver's state, so the record is the only evidence a caller has that the command arrived at all — see Pulls.
The empty-ref rejection mirrors Docker.Prepull's: a fake that accepted one would let a runnerd test pass against a call production refuses.
func (*Fake) Pulls ¶
Pulls returns every ref passed to Prepull, in call order. Exported — unlike this fake's other accessors, which are package-internal — because runnerd's agent tests live in another package and this record is their whole assertion.
func (*Fake) RemoveWorkspace ¶
RemoveWorkspace releases the volume named for sessionID, tolerating one that was never there — the docker driver's `volume rm -f`, and the same no-op for an empty id (see driver.Driver.RemoveWorkspace).
func (*Fake) Strips ¶
Strips returns the stripEnv list of every Snapshot call, in call order. Exported for the same reason Pulls is: runnerd's and the e2e suite's tests live in other packages, and this record is the only place above the driver where "the secrets and the setup channel were named for stripping" is observable at all.
func (*Fake) Volumes ¶
Volumes returns every live workspace volume name, sorted. Exported — like Pulls and Strips, and unlike this fake's other accessors — because the packages whose behavior this models live elsewhere: runnerd's crash tests assert that a container's death did NOT take a volume, and above the driver this record is the only place that fact is observable at all.
type HomeMount ¶ added in v0.0.3
HomeMount names the agent home volume and where it lands in the container. The driver keeps its own copy of the wire's type for the same reason it keeps its own Spec and RepoSpec: this package is the sandbox boundary and must not depend on the control-plane vocabulary.
Volume is opaque here on purpose. It is a hash the control plane minted (controlapp.AgentHomeVolume) precisely so a volume name — readable by anyone with a shell on the runner — does not spell out whose account it holds. The driver mounts it and never parses it.
type Listed ¶
Listed pairs a driver handle with the session id it belongs to, for List's bulk view of every rainier-managed resource.
type RepoSpec ¶
type RepoSpec struct {
Owner string `json:"owner"`
Name string `json:"name"`
BaseBranch string `json:"base_branch"`
SessionBranch string `json:"session_branch"`
Dir string `json:"dir"`
}
RepoSpec is one repository a session clones, mirroring rwire.RepoSpec field for field — the driver layer keeps its own type for the same reason it keeps its own Spec: it is the boundary the fake and the docker driver share, and it must not depend on the control-plane wire package. The JSON tags are the ones sessiond decodes, so the two spellings have to stay identical.
type Spec ¶
type Spec struct {
Name string // human label
Image string // OCI ref (v0 default a bash image)
Cmd []string // entrypoint override; empty = image default
DialURL string // runnerd URL the container's sessiond dials (relay)
SessionID string // stable id runnerd assigns; sessiond registers with it
EgressAllow []string // hostnames the session may reach
// ProxyURL, when non-empty, is the egress proxy the session's outbound
// traffic must route through. Injected as both cases of HTTP_PROXY/
// HTTPS_PROXY (tools disagree on which they read: BusyBox wget and curl
// read lowercase, many Go/Node tools read uppercase) plus NO_PROXY/
// no_proxy for loopback and the docker-desktop host alias.
ProxyURL string
// Env is extra environment for the session container, injected as
// `docker run -e K=V`. Resolved by the layer above (an environment's
// declared env plus its resolved secret refs, Plan 4) — the driver only
// carries it through, in sorted-key order, after the proxy vars.
Env map[string]string
// Setup is the environment's setup script, run once inside the fresh
// container before the agent starts. It reaches the container as
// RAINIER_SETUP_B64 (base64 of these bytes): a setup script is
// multi-line by nature and `docker run -e K=V` carries one line per var,
// so the encoding is what makes an env var a viable channel for a file
// at all. sessiond decodes it back to /workspace/.rainier/setup.sh and
// wraps the agent behind it; see cmd/sessiond.
//
// Empty means "no setup" — which is the normal case for a session whose
// environment was already snapshot-cached, since that image IS the
// finished setup. Capped at MaxSetupBytes.
Setup string
// SetupTimeoutSec bounds that run, injected as RAINIER_SETUP_TIMEOUT
// alongside the script. It is carried, not policed, here: controld owns
// the default (design §4.3 — it sends 900 when an environment declares
// none) and sessiond treats anything non-positive as an unbounded run.
SetupTimeoutSec int
// Repos are the repositories this session clones at boot, resolved by
// controld. They reach the container as RAINIER_REPOS_B64: base64 of the
// JSON array, for the same reason the setup script is encoded — an env
// var carries one line, and JSON quoting through an argv is a parser
// nobody should have to write twice.
Repos []RepoSpec
// Init is the environment's per-boot hook, run after the clones. It rides
// as RAINIER_INIT_B64 with its bound in RAINIER_INIT_TIMEOUT, a channel
// separate from the setup one because a cache-hit create carries an init
// and no setup — the image IS the finished setup, and init is the part
// that could not be baked into it.
Init string
InitTimeoutSec int
// GitAuthorName and GitAuthorEmail are the identity the session's commits
// carry, injected as RAINIER_GIT_AUTHOR_NAME / RAINIER_GIT_AUTHOR_EMAIL
// for sessiond to write into the workspace gitconfig. Neither is a
// credential; the token never rides the environment block at all.
GitAuthorName string
GitAuthorEmail string
// Home is the agent home this session mounts: one writable volume per
// (creator, workspace), landing at Path, inside which each coding agent
// keeps its own configuration under its own subdirectory. It is the only
// writable place a session has outside its workspace, and it is what makes
// a login survive the session that performed it.
//
// The volume is NOT the session's. It outlives every session mounted on
// it, is shared by every concurrent session the same person runs in the
// same workspace on this runner, and no teardown here removes it — see
// Driver.Destroy.
//
// nil means "mount nothing", which is the honest state for a session with
// no creator and for every create a control plane older than the field
// sent. The session's agents then simply ask for a login.
Home *HomeMount
}