Documentation
¶
Overview ¶
Package engine brings a bay up and takes it down.
Everything devbay creates in Docker carries the labels in this file. That is not bookkeeping: teardown is defined as "remove everything with these labels", so completeness is a property of the labelling rather than of remembering to delete each thing. A resource devbay creates without a label is a resource devbay will leak, which is why creation goes through one place.
Index ¶
- Constants
- func PortKeys(m *manifest.Manifest) []string
- type Endpoint
- type Engine
- func (e *Engine) Close() error
- func (e *Engine) Cool(ctx context.Context) error
- func (e *Engine) Down(ctx context.Context) error
- func (e *Engine) Focus(ctx context.Context, focused bool) error
- func (e *Engine) Focused() bool
- func (e *Engine) Freeze(ctx context.Context) error
- func (e *Engine) HasContainers(ctx context.Context) bool
- func (e *Engine) Logs(ctx context.Context, service string, n int) (string, error)
- func (e *Engine) Memory(ctx context.Context) (uint64, error)
- func (e *Engine) NetworkName() string
- func (e *Engine) RebuildService(ctx context.Context, name string) error
- func (e *Engine) Reload(ctx context.Context, name string) error
- func (e *Engine) Republish(ctx context.Context) error
- func (e *Engine) Resolver() *Resolver
- func (e *Engine) RestartService(ctx context.Context, name string) error
- func (e *Engine) Resume(ctx context.Context) error
- func (e *Engine) RunTask(ctx context.Context, taskName string) (*TaskResult, error)
- func (e *Engine) State(ctx context.Context) (State, error)
- func (e *Engine) Status(ctx context.Context) ([]ServiceStatus, error)
- func (e *Engine) Thaw(ctx context.Context) error
- func (e *Engine) URLs() map[string]string
- func (e *Engine) Up(ctx context.Context, plan *Plan) error
- func (e *Engine) Warm(ctx context.Context, plan *Plan) error
- type Options
- type Plan
- type Plane
- type Resolver
- func (r *Resolver) Endpoint(service string, plane Plane) (Endpoint, error)
- func (r *Resolver) Hostname(service string) string
- func (r *Resolver) NamedHostname(service, port string) string
- func (r *Resolver) ResolveEnv(env map[string]string, plane Plane) (map[string]string, error)
- func (r *Resolver) ResolveString(v string, plane Plane) (string, error)
- func (r *Resolver) SetHostPort(service string, port int)
- func (r *Resolver) SetNamedHostPort(service, name string, port int)
- func (r *Resolver) SetSecrets(f func(path string) (string, bool))
- type ServiceStatus
- type State
- type Step
- type TaskResult
Constants ¶
const ( LabelManaged = "dev.devbay.managed" LabelProject = "dev.devbay.project" LabelBay = "dev.devbay.bay" LabelService = "dev.devbay.service" )
Labels applied to every container, network and volume devbay creates.
const LabelSeed = "dev.devbay.seed"
LabelSeed marks a template volume, which is project-scoped rather than bay-scoped: that is the whole point of it, and it is also why teardown has to treat it specially.
const WorkspaceDir = "/workspace"
WorkspaceDir is where the worktree is mounted inside every container.
Variables ¶
This section is empty.
Functions ¶
func PortKeys ¶
PortKeys lists every publishable port in a manifest, sorted.
Sorted, because the mapping from key to port must depend only on what the manifest declares. Ranging over the services map directly would reassign ports on every boot and quietly undo the determinism the allocator exists to provide.
Types ¶
type Engine ¶
type Engine struct {
// Log receives progress lines. Never nil after New.
Log func(format string, args ...any)
// contains filtered or unexported fields
}
Engine operates on one bay.
func (*Engine) Cool ¶
Cool stops the bay's containers, keeping volumes.
This is the state transition a scheduler under memory pressure should make. A frozen bay costs the same memory as a running one.
func (*Engine) Down ¶
Down removes every Docker object belonging to this bay.
Teardown is expressed as a label query rather than as a list of things devbay remembers creating, because the two can disagree — after a crash, or a partial boot — and only the label query is right in both cases. A leaked volume or network after teardown is a bug of the same severity as a crash: it silently changes the behaviour of the next bay.
func (*Engine) Focus ¶
Focus marks this bay as the holder of the project's canonical hostname.
Named hostnames cover most work, but some things cannot be talked out of a fixed address: an OAuth redirect URI the provider will not wildcard, a mobile simulator, a native app config. Focus is what serves those.
func (*Engine) Freeze ¶
Freeze pauses every container in the bay.
Use this to stop a bay consuming CPU while keeping resume instant. Do not use it to relieve memory pressure: see StateFrozen.
func (*Engine) HasContainers ¶ added in v0.4.0
HasContainers reports whether this bay has anything the daemon knows about.
The difference between the two ways a boot fails. An image that cannot be pulled leaves nothing behind, so there is nothing to look at and a bay kept for inspection would be an empty shell; a service that started and never became healthy leaves the container that holds the explanation.
func (*Engine) Memory ¶
Memory reports the resident memory of the bay's containers, in bytes.
Measured inside the VM, because on macOS the host-side figure is the virtual machine's own footprint and says almost nothing about any individual bay. A scheduler budgeting against the host number would be budgeting against noise.
func (*Engine) NetworkName ¶
NetworkName is the Docker network a bay's containers share. The proxy joins it to reach services by name.
func (*Engine) RebuildService ¶ added in v0.1.1
RebuildService rebuilds a service's image and replaces its container.
The action behind `watch_action: rebuild`, for a service whose code is baked into the image rather than read from the mount -- a compiled binary, an asset bundle. Restarting one of those reruns the old build.
func (*Engine) Reload ¶ added in v0.1.1
Reload applies the action a service declares for a changed file.
func (*Engine) Republish ¶
Republish re-joins the proxy to this bay's network and re-announces its routes.
For the case where the proxy container is newer than the bay: a machine restart, a `docker rm`, an upgrade. The bay is still running and still answers on its published ports, but the proxy that knew how to reach it is gone, so the hostname a developer has bookmarked would 404 with no way back short of recreating the bay.
func (*Engine) Resolver ¶
Resolver exposes the address resolver, populated with published ports as containers start.
func (*Engine) RestartService ¶ added in v0.1.1
RestartService restarts one service and waits for it to be healthy again.
The action behind `watch_action: restart`, which is what a process that reads its configuration or its code once at startup needs when a file changes. Health is re-probed because a restart is exactly when a change breaks a service, and reporting "restarted" for a container that came back and immediately died would be worse than saying nothing.
func (*Engine) Resume ¶ added in v0.1.1
Resume brings a bay back from whichever resting state it is in.
"Thaw" is what a developer types to get a bay working again, and they should not have to know whether it was paused or stopped to type the right thing. Unpausing a stopped bay silently did nothing: the command reported the bay's state, which was still cold, and `devbay cool` was a one-way door out of a working bay -- the opposite of what it is documented to be, since cooling is exactly what a machine under memory pressure should do.
func (*Engine) RunTask ¶
RunTask materializes what a task needs, runs it, and parses the result.
The materialization is the point of making `needs` mandatory: a task that declares no services boots no containers and returns in the time the tests take, rather than the time a full stack takes to come up.
func (*Engine) Status ¶
func (e *Engine) Status(ctx context.Context) ([]ServiceStatus, error)
Status reports every container belonging to this bay.
func (*Engine) Thaw ¶
Thaw resumes a frozen bay. Nothing is lost, so no health probe is needed -- the processes never stopped, they were only descheduled.
func (*Engine) Up ¶
Up brings the plan's services up, wave by wave.
Services within a wave start concurrently and the wave is not complete until every service in it is healthy, so a slow database delays only what depends on it. A oneshot completes when it exits zero; a long-running service completes when its probe passes.
type Options ¶
type Options struct {
Manifest *manifest.Manifest
Bay string
Worktree string
Log func(format string, args ...any)
// Ports assigns stable host ports. When nil, Docker chooses ephemeral
// ones, which is fine for a throwaway bay but means a URL does not
// survive a restart.
Ports *ports.Allocator
// Proxy publishes the bay's hostnames. When nil, the bay is reachable at
// 127.0.0.1:<port> only, which works for agents and probes but gives up
// the per-bay browser origin.
Proxy *proxy.Proxy
// Scrubber removes secret values from anything the engine returns. When
// nil, a shape-only scrubber is used: returning raw output would be a
// worse default than a slightly over-eager one.
Scrubber *scrub.Scrubber
// Secrets resolves ${secret:path} at container spawn time. When nil, a
// manifest referencing a secret fails to boot rather than starting with a
// blank credential, which would fail later and less clearly.
Secrets func(path string) (string, bool)
// Egress enforces the per-service network allowlist. When nil, services
// keep whatever network Docker gives them, which is everything -- so this
// being nil is a decision, not a default.
Egress *egress.Enforcer
}
Options configure an Engine.
type Plan ¶
type Plan struct {
Steps []Step
}
Plan is an ordered set of steps that brings some part of a bay up.
func SeedPlan ¶
SeedPlan returns the oneshots whose completion defines a service's seeded state, in dependency order, along with the globs that decide staleness.
func TaskPlan ¶
TaskPlan returns the plan that materializes only what a task needs.
This is the reason tasks declare `needs`. Because an agent calls a named task rather than a shell command, devbay knows in advance which services the run touches and can start those and nothing else -- so a unit suite with `needs: []` boots zero containers and returns in the time the test takes, rather than the time a full stack takes to come up.
type Plane ¶
type Plane int
Plane is the vantage point an address is resolved for.
A service has more than one address and they are not interchangeable. This is the detail most likely to be got wrong, and getting it wrong produces the specific, confusing failure where a page renders in the browser but server-side rendering of the same page fails.
const ( // PlaneContainer is one container addressing another over the bay's // network, by service name. Ports are the ones the app actually listens // on, so nothing inside a bay needs port-offset awareness. PlaneContainer Plane = iota // PlaneHost is the daemon, the CLI, or an agent addressing a container // from outside, as 127.0.0.1:<published port>. Health probes always use // this plane. PlaneHost // PlaneBrowser is a browser addressing a container by hostname through the // proxy. Only ever produced for ${bay.<svc>.public_url}. // // Never usable by the daemon or by an application: *.localhost does not // resolve through getaddrinfo, so Go, Node, Python and Safari all fail on // it. Only Chrome, Firefox and curl special-case those names. PlaneBrowser )
type Resolver ¶
type Resolver struct {
// Bay and Project form the hostname namespace.
Bay string
Project string
// TLD is the browser-facing suffix; "localhost" by default.
TLD string
// Scheme is https once the proxy has a trusted certificate.
Scheme string
// contains filtered or unexported fields
}
Resolver turns ${bay...} and ${secret:...} references into concrete values.
It is constructed per bay, after ports are known, and answers differently depending on which plane is asking.
func NewResolver ¶
NewResolver builds a resolver for a bay.
func (*Resolver) Hostname ¶
Hostname returns the browser-facing name for a service.
The primary service claims the bare <bay>.<project>.<tld>; every other service is prefixed. Distinct hostnames are what give each bay its own browser origin, which is what stops two bays from sharing a cookie jar -- browsers key cookies by host and ignore the port, so without this, two bays of the same app overwrite each other's sessions.
func (*Resolver) NamedHostname ¶
NamedHostname returns the browser-facing name for a secondary port.
func (*Resolver) ResolveEnv ¶
ResolveEnv renders a service's environment for the given plane.
Container environments are rendered with PlaneContainer, so a service talking to another uses the container network. Values an agent or the CLI sees are rendered with PlaneHost. public_url always renders as the browser origin regardless of plane, because that is what it means.
func (*Resolver) ResolveString ¶
ResolveString expands every reference in v.
func (*Resolver) SetHostPort ¶
SetHostPort records the host port a service's primary port was published on.
func (*Resolver) SetNamedHostPort ¶
SetNamedHostPort records the host port for one of a service's named ports.
type ServiceStatus ¶
ServiceStatus is the observable state of one service.
type State ¶
type State string
State is how much of a bay is resident.
The names come from the original design, but two of the cost claims attached to them were wrong and have been corrected here, because building a scheduler on the wrong ones would produce a tool that promises to reclaim memory and does not.
const ( // StateCold means the containers exist but are stopped, or do not exist at // all. Volumes survive, so a cold bay keeps its data. // // This is the ONLY state that returns memory. Whether the host sees it // back is a separate question: on Apple's Virtualization.framework the // Linux VM does not return freed guest memory to macOS, so reclamation // there needs OrbStack or Docker's own VMM. StateCold State = "cold" // StateFrozen means the containers are paused with the cgroup freezer. // // Freezing stops scheduling, not allocation. CPU drops to zero and resume // is near-instant with no state lost, which is genuinely useful -- but the // memory stays exactly where it was. Anything that needs memory back must // go to cold instead. StateFrozen State = "frozen" // StateWarm means running and reachable at its own hostname. StateWarm State = "warm" // StateHot means running and additionally holding the project's canonical // hostname, so links and configs that hardcode it land here. StateHot State = "hot" // StateMixed means the containers disagree, usually mid-transition or // after a partial failure. Reported rather than papered over. StateMixed State = "mixed" )
func (State) ReclaimsMemory ¶
ReclaimsMemory reports whether entering this state frees the memory a bay was using. Only one state does, and a scheduler that assumes otherwise will pause bays forever while the machine keeps swapping.
type Step ¶
type Step struct {
Service string
// Oneshot steps run to completion and the plan waits for exit 0.
Oneshot bool
// Wave is the dependency depth. Everything in a wave can start
// concurrently; wave N starts once wave N-1 is healthy.
Wave int
}
Step is one unit of a boot plan.
type TaskResult ¶
type TaskResult struct {
Task string `json:"task"`
ExitCode int `json:"exit_code"`
DurationMS int64 `json:"duration_ms"`
Total int `json:"total,omitempty"`
// Always emitted, never omitted when zero. This is the agent-facing
// answer to "did my change work", and a missing key is not the same
// message as a zero: it makes a clean run indistinguishable from a run
// whose results could not be read, and one of those means try again.
Passed int `json:"passed"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Failures []report.Failure `json:"failures,omitempty"`
// Output is the tail of the run, scrubbed. Present so a failure with no
// parseable report is still actionable rather than opaque.
Output string `json:"output,omitempty"`
// Parsed reports whether structured results were available. An agent that
// sees false knows the failure list is empty because nothing could be
// parsed, not because nothing failed.
Parsed bool `json:"parsed"`
}
TaskResult is what running a task produces.
Everything here is typed. An agent that receives {file, line, message} can open the file and fix the line; an agent that receives stdout has to guess at the runner's format, and it guesses differently each time.
func (*TaskResult) Succeeded ¶
func (r *TaskResult) Succeeded() bool
Succeeded reports whether the task passed.