Documentation
¶
Overview ¶
Package bay is the orchestration layer: it turns "make me a bay" into a worktree, a port block, a set of containers, and a hostname, and turns "remove it" back into nothing.
The pieces below it each do one thing and are independently testable. This package exists because creating and destroying them has to be atomic: a worktree without containers is a checkout nobody asked for, containers without a worktree are pointing at a directory that no longer exists, and a port block without either is a leak that is invisible until allocation starts failing.
Index ¶
- Constants
- Variables
- func AwaitingApproval(err error) bool
- func DeriveAlias(branch string) string
- func EnvName(ref string) string
- type Bay
- type BootError
- type Booter
- type CreateOptions
- type Info
- type Manager
- func (m *Manager) Adopt(b *Bay)
- func (m *Manager) Approvals(ctx context.Context, project string) ([]approve.Record, error)
- func (m *Manager) Broker() *broker.Broker
- func (m *Manager) Close() error
- func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Bay, error)
- func (m *Manager) Describe(ctx context.Context, b *Bay) (Info, error)
- func (m *Manager) Destroy(ctx context.Context, name string, force bool) error
- func (m *Manager) EnsureWritable(ctx context.Context, worktree string)
- func (m *Manager) Focus(ctx context.Context, name string) error
- func (m *Manager) Get(name string) (*Bay, bool)
- func (m *Manager) GrantApproval(ctx context.Context, project string, d manifest.Diagnostic) error
- func (m *Manager) List(ctx context.Context) ([]Info, error)
- func (m *Manager) NewBooter(worktree, name string) *Booter
- func (m *Manager) NotFound(ctx context.Context, name string) error
- func (m *Manager) OwningProject(ctx context.Context, name string) (string, bool)
- func (m *Manager) PendingApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) []manifest.Diagnostic
- func (m *Manager) Proxy() *proxy.Proxy
- func (m *Manager) RequireApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) error
- func (m *Manager) RevokeApproval(ctx context.Context, key string) (bool, error)
- func (m *Manager) RunTask(ctx context.Context, name, task string) (*engine.TaskResult, error)
- func (m *Manager) Scrubber() *scrub.Scrubber
- func (m *Manager) Secrets() *Secrets
- func (m *Manager) SetSecret(ref, value string)
- func (m *Manager) VerifyManifest(ctx context.Context, candidate []byte, patcher verify.Patcher) (*verify.Result, error)
- type Options
- type Secrets
- type ServiceInfo
Constants ¶
const DefaultMaxResident = 5
DefaultMaxResident is how many bays devbay will leave running at once.
Five, because that is the number the whole design was sized against: five bays of a real application, on one developer's machine, without the machine becoming unusable. It is a default rather than a limit -- a developer who wants eight can say so -- and it is enforced by cooling, never by refusing.
const MaxAlias = 12
MaxAlias is the longest useful label. Past this a browser tab truncates it and the label stops doing its job.
Variables ¶
var ManifestNames = []string{"devbay.yaml", "devbay.yml"}
ManifestNames are the files a bay is described by, in precedence order.
Functions ¶
func AwaitingApproval ¶ added in v0.2.0
AwaitingApproval reports whether an error is a pending human decision.
func DeriveAlias ¶
DeriveAlias turns a branch name into a short label.
Agents produce branch names like feat/refactor-auth-middleware-to-support-refresh-token-rotation, which is useless in a tab strip or an `ls` column. The meaningful head of the name is almost always enough to tell two bays apart.
Types ¶
type Bay ¶
type Bay struct {
Name string
Branch string
// Alias is the short human label. Agents generate branch names far too
// long to read in a tab strip, so a bay carries a separate name capped
// short enough to survive being truncated.
Alias string
Worktree string
Adopted bool
Manifest *manifest.Manifest
Engine *engine.Engine
}
Bay is one running instance of a repository.
type BootError ¶ added in v0.4.0
BootError says a bay exists but something in it did not come up.
A distinct type because the two outcomes need different handling and used to be conflated: "the bay could not be created" is a dead end, while "the bay is running with one broken service" is the ordinary state of a stack somebody is still working on. The bay it names is real -- it will appear in `devbay ls`, its logs can be read, its healthy services serve, and `devbay rm` removes it.
type Booter ¶
type Booter struct {
// contains filtered or unexported fields
}
Booter boots a candidate manifest in a throwaway bay and tears it down.
The bay is disposable on purpose. Verification exists to answer one question -- does this configuration actually work -- and answering it by leaving containers, volumes and a port block behind would make `devbay init` a command that quietly costs resources every time it is run.
func (*Booter) Boot ¶
Boot brings a candidate up and reports the first thing that went wrong.
The failure carries the service and its own logs, because a patcher handed "boot failed" can do nothing useful with it, and the cause is nearly always in what the container printed: a missing variable, a refused connection, a migration that has not run.
type CreateOptions ¶
type CreateOptions struct {
Name string
Branch string
From string
Alias string
// Boot brings the services up. When false the worktree is prepared and
// the manifest validated, but nothing is started.
Boot bool
}
CreateOptions describe a bay to create.
type Info ¶
type Info struct {
Name string `json:"name"`
Alias string `json:"alias"`
Branch string `json:"branch"`
State string `json:"state"`
Worktree string `json:"worktree"`
Adopted bool `json:"adopted,omitempty"`
URLs map[string]string `json:"urls,omitempty"`
Ports map[string]int `json:"ports,omitempty"`
Services []ServiceInfo `json:"services,omitempty"`
MemoryMB int64 `json:"memory_mb,omitempty"`
Focused bool `json:"focused,omitempty"`
}
Info is the serialisable view of a bay, and is what crosses the MCP boundary.
type Manager ¶
type Manager struct {
RepoRoot string
Log func(format string, args ...any)
// contains filtered or unexported fields
}
Manager owns every bay for one repository.
func (*Manager) Adopt ¶
Adopt registers an already-created bay, used when the daemon restarts and finds containers it did not start in this process.
func (*Manager) Destroy ¶
Destroy removes a bay completely.
The order matters: containers, volumes and network first, then the port block, then the worktree. Removing the worktree while containers still bind-mount it would leave them pointing at a path that no longer exists.
func (*Manager) EnsureWritable ¶ added in v0.1.1
EnsureWritable gives a bay's worktree back to the developer if a container has taken it.
This is about the primary activity, not about cleanup. Containers write into the bind-mounted worktree as whatever user they run as, which for most images is root, and on Linux that is the filesystem's ownership -- so after a bay boots, the developer can find their own source tree read-only. Editing is the thing devbay exists to let you do in parallel; a bay you cannot edit is not a working bay.
Checked with a write rather than by inspecting modes, because the question is exactly "can this process write here" and permissions, ownership, ACLs and the platform all bear on the answer. Costs a stat and a create when nothing is wrong, which is the common case and the one that must stay cheap.
func (*Manager) GrantApproval ¶ added in v0.2.0
GrantApproval records a human's decision.
func (*Manager) OwningProject ¶ added in v0.5.2
OwningProject names the project a bay belongs to, when that is some project other than this repository's. Bays are scoped to a project and the state database is not, so a name absent here may be running one directory away.
func (*Manager) PendingApprovals ¶ added in v0.2.0
func (m *Manager) PendingApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) []manifest.Diagnostic
PendingApprovals is the subset of a validation's approvals not yet granted.
func (*Manager) RequireApprovals ¶ added in v0.2.0
func (m *Manager) RequireApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) error
RequireApprovals refuses to boot a bay whose manifest runs a command no human has agreed to.
The rule is only a boundary if it blocks. Printing "this needs approval" and then running the command teaches a developer that the warning is noise -- and the next warning, the one that matters, is scrolled past at the same speed. So an unapproved argv stops the bay before any container starts.
The error names the exact command and the exact way to allow it, because a refusal a developer cannot act on is indistinguishable from a bug, and the thing they will do about it is stop using the tool.
func (*Manager) RevokeApproval ¶ added in v0.2.0
RevokeApproval withdraws one by key.
type Options ¶
type Options struct {
// Dir is any path inside the repository.
Dir string
// StatePath is the SQLite file; empty uses ~/.devbay/state.db.
StatePath string
// WorktreeRoot is where new worktrees go; empty uses ~/.devbay/worktrees.
WorktreeRoot string
// ProxyPort is the host port for bay hostnames; 0 tries 80 then 8080.
ProxyPort int
// AdminPort is where the proxy's config API is published, on loopback.
AdminPort int
// NoProxy disables hostname routing, leaving bays reachable at
// 127.0.0.1:<port> only.
NoProxy bool
// Egress enforces per-service network allowlists. Off by default because
// it costs a privileged sidecar per service; on, a service reaches only
// what its manifest declares.
Egress bool
// AuditPath is the credential log; empty uses ~/.devbay/audit.jsonl.
AuditPath string
// SecretCommand is a secret manager to shell out to, with {ref}
// substituted -- for example ["op", "read", "op://{ref}"]. Configured by
// the developer, never by a manifest.
SecretCommand []string
Log func(format string, args ...any)
}
Options configure a Manager.
type Secrets ¶
type Secrets struct {
// contains filtered or unexported fields
}
Secrets resolves ${secret:path} references at container spawn time.
This is deliberately the smallest thing that can be correct rather than an attempt at a secrets manager. The ecosystem already converged on one shape -- `op run --`, `direnv exec`, `sops exec-env`, `dotenvx run --` all inject values into a subprocess and hold nothing -- so the useful thing to build is a consumer of those, not a competitor to them. What lives here is the registry that resolution and scrubbing share, plus an environment fallback.
Two properties matter more than where the values come from:
- A value registered here is registered with the scrubber in the same call. Resolving a secret without teaching the scrubber about it would hand an application a credential that devbay could no longer recognise in that application's own logs.
- Lookup happens when a container is created, not when a manifest is read, so a value is never held longer than it has to be.
func NewSecrets ¶
NewSecrets returns a registry that reports every value it hands out to s.
func (*Secrets) Lookup ¶
Lookup resolves a reference, falling back to the environment.
The environment fallback is what makes devbay work with every tool listed above without integrating with any of them: `op run -- devbay run ...` already puts the values in devbay's environment, and this finds them there.
type ServiceInfo ¶
type ServiceInfo struct {
Name string `json:"name"`
State string `json:"state"`
URL string `json:"url,omitempty"`
}
ServiceInfo is one service's observable state.