controlplane

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package controlplane is the Burrow control plane: the product. It runs deploy orchestration, rollout and rollback, logs and status, scaling, the safety guardrails, and the record of who deployed what. It holds the cluster credentials and is the only layer that talks to Kubernetes.

This is the public API surface of the control plane — kept importable by a separate private module (the managed product). It currently holds the domain types (App, Release, Policy) and the seam interfaces (Clock, Kubernetes, Registry, Database) that core logic is written against (ADR-0010); the deploy engine and its constructor land in later phases (docs/PLAN.md). The implementation guts live in controlplane/internal, and in-memory fakes of the seams live in controlplane/internal/fake for tests.

This package is licensed Apache-2.0 (see LICENSING.md and ADR-0033).

Index

Constants

View Source
const (
	// ReasonImagePullBackOff is the kubelet's back-off state after repeated pull failures.
	ReasonImagePullBackOff = "ImagePullBackOff"
	// ReasonErrImagePull is the kubelet's first pull failure, before it backs off.
	ReasonErrImagePull = "ErrImagePull"
)

Blocking image-pull waiting reasons a container reports when the cluster cannot fetch the image. These are the only reasons Burrow surfaces as an Issue: a private registry with no pull credentials is the common, human-fixable cause (ADR-0017). Transient reasons like ContainerCreating or PodInitializing are deliberately excluded — they resolve on their own.

View Source
const DefaultEnvironment = "default"

DefaultEnvironment is the name of the implicit environment that always exists: the app namespace burrowd already runs against (BURROW_NAMESPACE), behaving exactly like before environments were introduced (ADR-0035 phase 2). It is reserved — it cannot be created with `burrow env add`, because it is synthesized rather than stored — and it is always listed first.

View Source
const ReleaseAnnotation = "burrow.cloud/release"

ReleaseAnnotation is the pod-template annotation Burrow stamps with the release ID so every deploy of a new release rolls the workload, even when the image reference is unchanged. A re-deploy of the same image is a new release with a new ID, so the pod template differs and the server-side apply triggers a rollout (replacing pods whose per-creation state is stale — e.g. an imagePullSecret bound before `burrow config registry login` fixed the pull credential).

View Source
const RestartedAtAnnotation = "burrow.cloud/restarted-at"

RestartedAtAnnotation is the pod-template annotation Burrow bumps to a fresh timestamp to force a rolling update when something the pod reads only at start changes — notably a secret value under an existing key, which does not otherwise mutate the template (ADR-0028).

Variables

View Source
var (
	// ErrInvalid marks a malformed request — a bad app name, an empty image
	// reference, a negative replica count. The caller must fix the request; retrying
	// it unchanged will fail the same way.
	ErrInvalid = errors.New("invalid request")

	// ErrNotImplemented marks an operation whose backing adapter is not wired in this
	// build yet (e.g. the cluster adapter before it ships). It is an honest-status
	// signal (ADR-0009), distinct from a malformed request or a system failure.
	ErrNotImplemented = errors.New("not implemented")

	// ErrNotFound marks a requested record or resource that does not exist. The seams
	// (seams.go) return it — possibly wrapped — so engine logic can branch on absence
	// with errors.Is without depending on a particular adapter.
	ErrNotFound = errors.New("not found")
)

The control plane classifies its failures with these sentinels so a front end (the HTTP API, the MCP server) can map them to the right status without parsing prose. They complement the typed GuardrailError (a deliberate policy refusal).

Functions

func AppSecretName

func AppSecretName(app string) string

AppSecretName is the per-app Kubernetes Secret that holds an app's secret env (ADR-0028): one object per app in the app namespace, keys = env-var names, values = secret values. The values live only here — never inlined into the Deployment spec, never written to Postgres, and never carried over MCP (ADR-0029/0004). A value may transit burrowd's authenticated control-plane API on `secret set`, which writes it here. The name is derived from the app (a DNS-1123 label, so the result is always a valid Secret name), so every layer computes it the same way rather than passing it around.

func BackupPath

func BackupPath(app, id string) string

BackupPath is the on-PVC path of app's dump for id, recorded in the backup row and used by the kube Job builder so both sides agree on the layout without the engine importing the kube package (ADR-0032). It is a path on a volume burrowd does not mount — never a credential.

func ContextWithClientVersion added in v0.10.0

func ContextWithClientVersion(ctx context.Context, version string) context.Context

ContextWithClientVersion returns a context carrying the acting client's release version for the audit record (ADR-0039). The API layer sets it from the X-Burrow-Client-Version header; the engine reads it back via clientVersionFromContext when it records a guarded operation. An empty version leaves the context unchanged, so a pre-handshake client records no version.

func EnvScopable added in v0.7.0

func EnvScopable(code GuardrailCode) bool

EnvScopable reports whether a guardrail can be scoped to a named environment (ADR-0035 phase 2c). The app-level guardrails (app.*) gate per-app operations that always carry an environment, so they can be locked down per environment — strict prod, permissive staging. The cluster-level guardrails (addon.*, dns.*) gate cluster-wide operations that are not env-scoped (installing an add-on or writing DNS affects the whole cluster), so they are only ever set globally.

func ImagePullIssue added in v0.8.0

func ImagePullIssue(image, reason, message string) string

ImagePullIssue builds the actionable Issue message for a workload whose pod cannot pull its image. burrowd no longer resolves the image before deploy (ADR-0040), so a bad tag or a missing credential surfaces here, asynchronously, as the kubelet's pull failure. When the kubelet's waiting message clearly reports the image is not present (a "manifest unknown" / "not found"), the Issue names the tag as the likely fix; otherwise it defaults to the common, human-fixable cause — a private registry with no pull credentials (ADR-0017) — and names the exact `burrow config registry login` command. The credential step is human- and CLI-only and never crosses MCP (ADR-0017), so the message points at the user's terminal.

func IsImagePullReason added in v0.8.0

func IsImagePullReason(reason string) bool

IsImagePullReason reports whether reason is a blocking image-pull failure Burrow surfaces as an actionable Issue. A pod waiting for any other reason (still creating, initializing, …) is not reported, so Status never flags a transient state as a problem.

func KnownGuardrail

func KnownGuardrail(code GuardrailCode) bool

KnownGuardrail reports whether code names a configurable guardrail.

func RegistryHost added in v0.8.0

func RegistryHost(image string) string

RegistryHost returns the registry host of an image reference following the Docker convention: the first "/"-separated component is the host when it looks like one (it contains a "." or a ":", or is "localhost"); otherwise the reference is an implicit Docker Hub name and the host is "docker.io". Examples: "ghcr.io/org/app:1" -> "ghcr.io", "library/nginx" -> "docker.io", "registry.example.com:5000/app:1" -> "registry.example.com:5000".

Types

type AddDomainRequest

type AddDomainRequest struct {
	Host     string `json:"host"`
	Provider string `json:"provider"`
	// Address is the external IP or hostname to point Host at. Optional when App is set.
	Address string `json:"address,omitempty"`
	// App is an exposed application whose ingress external address Host should point at, used
	// when Address is empty so the agent need not look the address up itself.
	App string `json:"app,omitempty"`
	// Confirm acknowledges the dns.write guardrail so the operation proceeds past it.
	Confirm bool `json:"confirm,omitempty"`
}

AddDomainRequest points a host at an address through a configured DNS provider (ADR-0018). The address is the cluster's external entry point — the ingress controller's IP or hostname. Supply it explicitly with Address, or name an exposed App and the control plane reads the controller-assigned address from that app's Ingress (the value `burrow app reachability` reports).

type AddProviderRequest

type AddProviderRequest struct {
	// Name identifies the provider; empty defaults to the type.
	Name string `json:"name,omitempty"`
	// Type is the vendor this provider talks to.
	Type ProviderType `json:"type"`
	// SecretKey is the key in burrow-credentials the token is written under; empty defaults to Name.
	SecretKey string `json:"secret_key,omitempty"`
	// Token is the vendor API token VALUE. It is written to burrow-credentials after validation and
	// is never logged, stored in Postgres, or echoed back (ADR-0030).
	Token string `json:"token,omitempty"`
}

AddProviderRequest registers a vendor credential (ADR-0023, ADR-0030). The token VALUE travels in this request over burrowd's authenticated, TLS-protected control-plane API; burrowd validates it and then writes it into the burrow-credentials Secret. The value is never logged, never stored in Postgres, never returned in a response, and still never carried over MCP — provider add is a human/CLI operation, not an agent one.

type AddonInfo

type AddonInfo struct {
	Name string    `json:"name"`
	Type AddonType `json:"type"`
	// Mode is how the backend is provided: "installed" (Burrow deployed it) or "connected"
	// (an existing backend the user runs). Installed-only for now; connect lands later (ADR-0026).
	Mode string `json:"mode"`
	// Backend is the concrete adapter implementation backing this add-on (e.g. "victorialogs").
	Backend      string   `json:"backend,omitempty"`
	Image        string   `json:"image,omitempty"`
	Endpoint     string   `json:"endpoint"` // in-cluster host:port the app or agent reaches it on
	Capabilities []string `json:"capabilities"`
	// SecretKey is the non-secret key under which this add-on's bearer token lives in the
	// burrow-credentials Secret (ADR-0023). Empty means the backend is unauthenticated; the
	// token itself never travels here — only the key (ADR-0004).
	SecretKey string `json:"secret_key,omitempty"`
	// CreatedAt is when the add-on was registered, read from the injected clock.
	CreatedAt time.Time `json:"created_at,omitempty"`
	// Ready is a live property — whether the backing Deployment is available. It is probed
	// from the cluster at list time and never persisted in the registry.
	Ready bool `json:"ready"`
}

AddonInfo is one installed add-on instance, as seen by `addon list` and the agent. It carries no secret — when an add-on needs a credential it lives in a cluster Secret, never here.

type AddonSpec

type AddonSpec struct {
	Type AddonType
	// Backend is the concrete adapter implementation that backs this add-on (e.g.
	// "victorialogs"), recorded in the registry so the agent knows which adapter serves it.
	Backend string
	// Image is the pinned container image for the backing service.
	Image string
	// Port is the service port the app (or the agent, for an observability add-on) reaches it on.
	Port int32
	// StorageGi requests a persistent volume of this size in GiB; 0 is ephemeral. Stateful
	// stores (logs, metrics) persist so data survives a restart.
	StorageGi int
	// Capabilities are what the agent can query this add-on for (e.g. "logs"). For an
	// installed default it is fixed; a connected backend may derive or probe its own (ADR-0026).
	Capabilities []string
	// Summary is a one-line description for the catalog listing.
	Summary string
}

AddonSpec is a catalog entry: how to deploy and reach one vetted backing service. The catalog is curated and permissively licensed (Apache / MIT / BSD) so Burrow can bundle it without copyleft friction (ADR-0025) — which is why logs is VictoriaLogs (Apache), not AGPL Loki.

func AddonCatalog

func AddonCatalog() []AddonSpec

AddonCatalog returns the catalog entries in a stable order.

func LookupAddon

func LookupAddon(t AddonType) (AddonSpec, bool)

LookupAddon returns the catalog spec for t, or false if t is not a known add-on type.

type AddonType

type AddonType string

AddonType identifies a building-block backing service in the curated catalog (ADR-0025).

const (
	// AddonLogs is a log-aggregation store (VictoriaLogs, Apache-2.0).
	AddonLogs AddonType = "logs"
	// AddonMetrics is a metrics store (VictoriaMetrics single-node, Apache-2.0) paired with a
	// vmagent scraper that collects app-pod metrics and remote-writes them into it.
	AddonMetrics AddonType = "metrics"
	// AddonCache is an in-memory cache (ValKey, BSD-3) the agent wires an app to — a backing
	// service the app connects to, not one the agent queries, so it has no query seam.
	AddonCache AddonType = "cache"
	// AddonPostgres is a cluster-shared PostgreSQL instance (the official postgres image, PostgreSQL
	// License) the agent attaches an app to — Burrow provisions a database and login role per app
	// inside the one instance and writes the app's DATABASE_URL into its per-app Secret (ADR-0031).
	AddonPostgres AddonType = "postgres"
)

type App

type App struct {
	// Name identifies the app within the control plane and names its Kubernetes
	// workload. It must be a DNS-1123 label (lowercase alphanumerics and '-').
	Name string
}

App is a deployable application: the logical unit an agent operates. A single App has a history of Releases; the most recent successful one is what is running.

func (App) Validate

func (a App) Validate() error

Validate reports whether the app name is a usable Kubernetes resource name.

type AttachResult

type AttachResult struct {
	App   string    `json:"app"`
	Addon AddonType `json:"addon"`
	// SecretKey is the env-var name under which the generated connection string was written into
	// the app's per-app Secret. The value is never returned (ADR-0029/0031).
	SecretKey string `json:"secret_key"`
}

AttachResult is the outcome of attaching an app to an add-on (ADR-0031). It carries the KEY NAME the connection string was written under (e.g. "DATABASE_URL") — never the value, which lives only in the app's Kubernetes Secret.

type AuditEntry

type AuditEntry struct {
	// ID is the store-assigned row identity (0 before it is persisted). Newest rows have the
	// largest IDs.
	ID int64 `json:"id,omitempty"`
	// Timestamp comes from the injected clock, never ambient time (ADR-0010).
	Timestamp time.Time `json:"timestamp"`
	// Operation is the logical operation: deploy, scale, rollback, app_delete, expose,
	// dns_write, dns_delete, addon_install, addon_remove.
	Operation string `json:"operation"`
	// Target is what the operation acted on: an app, a host, or an add-on name. It may be
	// empty for an operation that names nothing.
	Target string `json:"target,omitempty"`
	// Args is the redacted, per-operation allowlist of salient parameters. Never secret values.
	Args map[string]string `json:"args,omitempty"`
	// GuardrailCode is the guardrail that applied (e.g. app.expose_public). Empty on an execution
	// row, which is the second half of a trail whose decision row already named the guardrail.
	GuardrailCode string `json:"guardrail_code,omitempty"`
	// Disposition is the guardrail's effective disposition (allow/confirm/deny) at decision time.
	Disposition string `json:"disposition,omitempty"`
	// Outcome is the categorical result (see AuditOutcome).
	Outcome AuditOutcome `json:"outcome"`
	// Result is the execution result: empty on success, or a short error summary on a failed
	// outcome. It never carries a secret value.
	Result string `json:"result,omitempty"`
	// Caller is the authenticated caller. Identity is coarse until an auth model exists
	// (ADR-0027): today it is a constant naming the control-plane boundary.
	Caller string `json:"caller,omitempty"`
	// Principal is the acting identity — the actor behind the operation, distinct from Caller
	// (the control-plane boundary that authenticated the request). Today every agent shares one
	// ServiceAccount, so it is a constant ("shared-agent"); the principal seam (ADR-0038) is
	// where a future TokenReview→SSO identity resolution fills in a real per-user value, so
	// attribution becomes additive rather than a migration of past rows' meaning.
	Principal string `json:"principal,omitempty"`
	// ClientVersion is the release version of the client (CLI or MCP server) that drove the
	// operation, read from the X-Burrow-Client-Version handshake header (ADR-0039). It records which
	// client acted, next to the principal (who) and the server's own version — so version skew is
	// legible after the fact. Empty for a pre-handshake client that sent no version. The json tag must
	// match the client-side AuditEntry tag exactly (the struct crosses the API), or the field would
	// silently drop.
	ClientVersion string `json:"client_version,omitempty"`
}

AuditEntry is one append-only record of a guarded mutating operation and the guardrail decision that applied (ADR-0027). It is written at the control-plane boundary — the single choke point that holds both the credentials and the decision.

Args is redacted by construction: it carries only safe metadata (app/host/addon name, image reference, replica count, env/secret key NAMES) and never an env value or any secret value. Callers build it through the per-operation allowlist, never by serializing a raw request.

type AuditFilter

type AuditFilter struct {
	// App restricts to rows whose target is this app/host/addon name; empty matches any.
	App string
	// Operation restricts to one operation name; empty matches any.
	Operation string
	// Outcome restricts to one outcome; empty matches any.
	Outcome AuditOutcome
	// Limit caps the number of rows returned, newest first. Zero or negative applies the
	// store's default cap.
	Limit int
}

AuditFilter narrows an audit query. A zero value matches everything (subject to Limit).

type AuditOutcome

type AuditOutcome string

AuditOutcome is what happened to a guarded mutating operation once the guardrail had its say (ADR-0027). It is the categorical fact a reviewer reads off the trail.

const (
	// AuditAllowed: the guardrail allowed the operation (allow disposition, or a confirm
	// disposition the caller confirmed). It is recorded before the operation executes; the
	// execution outcome is a second row.
	AuditAllowed AuditOutcome = "allowed"
	// AuditHeld: a confirm-disposition guardrail held the operation for confirmation and the
	// caller did not confirm. The operation did NOT execute. A later confirmed run is a
	// separate row.
	AuditHeld AuditOutcome = "held"
	// AuditDenied: a deny-disposition guardrail refused the operation outright. It did NOT
	// execute.
	AuditDenied AuditOutcome = "denied"
	// AuditExecuted: the operation was allowed-or-confirmed and ran to success.
	AuditExecuted AuditOutcome = "executed"
	// AuditFailed: the operation was allowed-or-confirmed and ran, but its execution errored.
	// The error is summarized in AuditEntry.Result; a distinct outcome keeps "it ran and
	// broke" readable apart from "it ran and worked" (ADR-0027).
	AuditFailed AuditOutcome = "failed"
)

type AutoscaleResult added in v0.8.0

type AutoscaleResult struct {
	App              string `json:"app"`
	Env              string `json:"env,omitempty"`
	MinReplicas      int32  `json:"min_replicas"`
	MaxReplicas      int32  `json:"max_replicas"`
	CPUPercent       int32  `json:"cpu_percent"`
	MemoryPercent    int32  `json:"memory_percent,omitempty"`
	MetricsAvailable bool   `json:"metrics_available"`
	Warning          string `json:"warning,omitempty"`
}

AutoscaleResult reports the outcome of configuring autoscaling: the effective spec that was applied, the app and environment it acted in, and whether metrics-server is present. When it is absent, MetricsAvailable is false and Warning explains that the autoscaler is set but will not scale until metrics-server is installed — the HPA is applied regardless (its creation does not need metrics-server; only its scaling does).

type AutoscaleSpec added in v0.8.0

type AutoscaleSpec struct {
	// MinReplicas is the floor the autoscaler will not scale below. Must be at least 1.
	MinReplicas int32 `json:"min_replicas"`
	// MaxReplicas is the ceiling the autoscaler will not scale above. Must be >= MinReplicas and is
	// itself bounded by the replica-ceiling guardrail (Policy.MaxReplicas).
	MaxReplicas int32 `json:"max_replicas"`
	// CPUPercent is the target average CPU utilization (1..100) the autoscaler holds the app at.
	CPUPercent int32 `json:"cpu_percent"`
	// MemoryPercent is the target average memory utilization (1..100), or 0 to add no memory metric.
	MemoryPercent int32 `json:"memory_percent,omitempty"`
}

AutoscaleSpec is the desired autoscaling shape for an app's Deployment (ADR-0006): the replica band the HorizontalPodAutoscaler moves within and the resource-utilization targets it scales on. It is code-free metadata that travels over the API. CPUPercent is required; MemoryPercent is optional (0 means no memory metric).

type Backup

type Backup struct {
	// ID is the backup identifier, minted from the IDs seam — also the dump filename stem.
	ID string `json:"id"`
	// App is the application whose database was dumped.
	App string `json:"app"`
	// CreatedAt is when the backup was recorded, read from the injected clock.
	CreatedAt time.Time `json:"created_at"`
	// Path is the on-PVC location of the dump (e.g. /backups/<app>/<id>.dump). It is a path on a
	// volume burrowd does not mount — never a credential.
	Path string `json:"path,omitempty"`
	// SizeBytes is the dump's size in bytes, or 0 when unknown.
	SizeBytes int64 `json:"size_bytes,omitempty"`
	// Status is the lifecycle state of this backup.
	Status BackupStatus `json:"status"`
}

Backup is one recorded per-app database backup (ADR-0032): the control-plane index row for a logical dump written to the backup PVC. It names the app, the on-PVC path, the byte size (0 when unknown), and the lifecycle status — never a credential or a connection string. burrowd is not mounted to the backup PVC, so this row, not the volume, is what `addon backups` lists.

type BackupResult

type BackupResult struct {
	Backup Backup `json:"backup"`
}

BackupResult reports the outcome of an on-demand backup (ADR-0032): the recorded backup row. It carries the backup id, the app, the path, the size, and the status — never a secret value.

type BackupStatus

type BackupStatus string

BackupStatus is the lifecycle state of a recorded Postgres backup (ADR-0032). A backup is recorded pending when its Job is started and moves to completed on the Job's success or failed on its failure.

const (
	// BackupPending is a backup whose Job has been started but has not yet completed.
	BackupPending BackupStatus = "pending"
	// BackupCompleted is a backup whose Job finished successfully; the dump is on the PVC.
	BackupCompleted BackupStatus = "completed"
	// BackupFailed is a backup whose Job did not complete successfully.
	BackupFailed BackupStatus = "failed"
)

func (BackupStatus) Valid

func (s BackupStatus) Valid() bool

Valid reports whether s is a known BackupStatus.

type Capability

type Capability string

Capability is a service Burrow can drive a provider to perform on the user's behalf (ADR-0023). A capability is served by a chosen provider — DNS is the first; compute and registry capabilities follow. Selecting which provider serves a capability is explicit for now (the operation names a --provider); auto-detection comes later.

const (
	// CapabilityDNS is managing DNS records for a domain the user has delegated to Burrow.
	CapabilityDNS Capability = "dns"
)

type CertManagerCapability

type CertManagerCapability struct {
	Present bool `json:"present"`
}

CertManagerCapability reports whether cert-manager is installed. Present is true when the cert-manager.io API group is served — i.e. its CRDs are installed — detected via API-group discovery, which needs no RBAC.

type Clock

type Clock interface {
	// Now returns the current time.
	Now() time.Time
}

Clock is the control plane's only source of time. Injecting it keeps core logic deterministic: a release's CreatedAt and any timeouts come from here, never from time.Now.

type ClusterCapabilities

type ClusterCapabilities struct {
	// Ingress is the ingress-controller situation: whether a controller is actually running, and
	// which IngressClass(es) exist.
	Ingress IngressCapability `json:"ingress"`
	// Storage is the default-StorageClass situation: whether a default exists and its name.
	Storage StorageCapability `json:"storage"`
	// LoadBalancer is whether Service type=LoadBalancer is likely supported, detected from whatever
	// services LoadBalancers — a cloud provider, k3s's servicelb, or MetalLB.
	LoadBalancer LoadBalancerCapability `json:"load_balancer"`
	// CertManager is whether cert-manager is installed, detected via its API group (its CRDs).
	CertManager CertManagerCapability `json:"cert_manager"`
	// Provider is the detected cloud provider, inferred from node labels / providerID.
	Provider ProviderCapability `json:"provider"`
	// DNS is whether a DNS provider is configured in the registry (ADR-0023) — a control-plane
	// fact, not a cluster read. It is filled by the engine, not the cluster probe.
	DNS DNSCapability `json:"dns"`
}

ClusterCapabilities is a neutral, read-only report of what a cluster can do (ADR-0034): an ingress controller and its IngressClass, a default StorageClass, LoadBalancer support, whether cert-manager is installed, the cloud provider, and whether a DNS provider is configured. It is the low-trust agent entry point — the agent can survey a cluster and explain its state before anything is changed. It replaces the cluster-type picker: Burrow observes the cluster rather than asking the user to classify it. Each capability reports present / absent / inferred, never an opinion about whether the cluster "should" have it.

type ClusterProber

type ClusterProber interface {
	// DetectCapabilities reads the cluster's capabilities read-only. It never writes.
	DetectCapabilities(ctx context.Context) (ClusterCapabilities, error)
}

ClusterProber detects a cluster's capabilities read-only (ADR-0034): it reads IngressClasses, ingress-nginx controller Deployments, StorageClasses, and Nodes, and uses API-group discovery to detect cert-manager, then detects LoadBalancer support from whatever services LoadBalancers (a cloud provider, servicelb, or MetalLB). It is the seam over those reads so the engine stays unit-testable against a fake; the production adapter (controlplane/kube) wraps a client-go clientset, and the same detection runs whether driven by the kubeconfig client (install) or burrowd's in-cluster client. It returns only the cluster-derived capabilities; the DNS field is filled by the engine from the providers registry. It is an optional seam — present only when wired; ClusterCapabilities errors cleanly (ErrNotImplemented) when it is nil.

type ConnectBackend

type ConnectBackend struct {
	// Name is the backend identifier (e.g. "loki"), used as the add-on Backend and as the
	// querier key the engine dispatches on.
	Name string
	// Capabilities are what the agent can query a connected instance of this backend for. They
	// are derived from the backend, not declared by the user (ADR-0026): a single-capability
	// backend like Loki implies "logs".
	Capabilities []string
	// Summary is a one-line description for the connectable-backend listing.
	Summary string
}

ConnectBackend is a catalog entry for an existing backend the user already runs that Burrow can register and query (ADR-0026). Unlike an AddonSpec it carries no image or storage: connect is registration-only — Burrow never deploys a connected backend, it queries the one already there.

func ConnectCatalog

func ConnectCatalog() []ConnectBackend

ConnectCatalog returns the connectable backends in a stable name order.

func LookupConnectBackend

func LookupConnectBackend(name string) (ConnectBackend, bool)

LookupConnectBackend returns the catalog entry for name, or false if name is not a known connectable backend.

type Credentials

type Credentials interface {
	// Token returns the token stored under key in burrow-credentials, or ErrNotFound when
	// the Secret or the key is absent.
	Token(ctx context.Context, key string) (string, error)
	// SetToken upserts key=value into burrow-credentials, creating the Secret if absent
	// (ADR-0030). The value arrives over burrowd's authenticated, TLS-protected control-plane
	// API — never over MCP — and is written straight to the Secret: it is NEVER logged, never
	// stored in Postgres, and never returned in a response. Any error names the key only, never
	// the value.
	SetToken(ctx context.Context, key, value string) error
}

Credentials is the seam over the one burrow-credentials Secret that holds every vendor token (ADR-0023, ADR-0030). The control plane reads a provider's token through it at call time, so a rotation is picked up with no restart, and writes a token value it received over its authenticated control-plane API. It is the only path by which the control plane reads or writes a Secret's contents, and the production adapter is scoped to that single object — burrowd's least-privilege Role grants `get` and `update` on exactly burrow-credentials and nothing else.

type DNSCapability

type DNSCapability struct {
	Configured bool     `json:"configured"`
	Providers  []string `json:"providers,omitempty"`
}

DNSCapability reports whether a DNS provider is configured in the registry (ADR-0023). Configured is true when at least one provider serves the DNS capability; Providers names them.

type DNSFactory

type DNSFactory interface {
	DNS(t ProviderType, token string) (DNSProvider, error)
}

DNSFactory builds a DNSProvider for a vendor type and token (ADR-0023). It is the seam that lets the engine reach a vendor without importing its adapter: production maps each ProviderType to its adapter (controlplane/dns), and tests substitute a fake. It returns ErrNotImplemented for a type no adapter serves.

type DNSProvider

type DNSProvider interface {
	// VerifyAccess confirms the token authenticates and can manage DNS, with a cheap read
	// call against the vendor. It returns ErrInvalid when the vendor rejects the token.
	VerifyAccess(ctx context.Context) error
	// EnsureRecord creates or updates the record so r.Name resolves to r.Value (ADR-0018). It
	// is idempotent: re-applying the same record is a no-op. It returns ErrNotFound when no
	// zone the provider manages covers r.Name.
	EnsureRecord(ctx context.Context, r DNSRecord) error
	// DeleteRecord removes the A/CNAME record(s) the provider holds for host. It returns
	// ErrNotFound when no managed zone covers host or no such record exists.
	DeleteRecord(ctx context.Context, host string) error
}

DNSProvider is the seam over a single vendor's DNS API, holding one provider's token (ADR-0018, ADR-0023). burrowd is the only thing that talks to the vendor — the agent never holds the token and never calls the API directly. Writes are scoped to the zones the provider manages: an operation on a host no managed zone covers returns ErrNotFound.

type DNSRecord

type DNSRecord struct {
	// Type is A or CNAME.
	Type DNSRecordType
	// Name is the fully-qualified host, e.g. app.example.com.
	Name string
	// Value is the target: an IPv4 address for an A record, a hostname for a CNAME.
	Value string
	// TTL is the record's time to live in seconds; 0 means the provider's default.
	TTL int
}

DNSRecord is one record the control plane manages on the user's behalf.

type DNSRecordType

type DNSRecordType string

DNSRecordType is the kind of DNS record the control plane manages (ADR-0018). A host is pointed at an IPv4 address with an A record or at another hostname with a CNAME; the engine chooses based on the address it is given.

const (
	RecordA     DNSRecordType = "A"
	RecordCNAME DNSRecordType = "CNAME"
)

type Database

type Database interface {
	// SaveRelease persists r. An existing release with the same ID is overwritten
	// (releases are immutable in meaning; this supports status transitions during a
	// single rollout).
	SaveRelease(ctx context.Context, r Release) error
	// Release returns the release with the given ID, or ErrNotFound.
	Release(ctx context.Context, id string) (Release, error)
	// LatestRelease returns the most recently saved release for app, or ErrNotFound
	// if the app has no releases.
	LatestRelease(ctx context.Context, app string) (Release, error)
	// Releases returns all releases for app, oldest first. An app with no releases
	// yields an empty slice and no error.
	Releases(ctx context.Context, app string) ([]Release, error)
	// DeleteReleases removes all release records for app — the durable side of an app
	// teardown. Deleting the releases of an app that has none is a no-op, not an error.
	DeleteReleases(ctx context.Context, app string) error

	// AppEnv returns the non-secret environment store for app: the app-global current
	// config rendered into the workload at apply time (ADR-0028). An app with no env yields
	// an empty map and no error.
	AppEnv(ctx context.Context, app string) (map[string]string, error)
	// SetAppEnv upserts one env key for app in the store.
	SetAppEnv(ctx context.Context, app, key, value string) error
	// UnsetAppEnv removes one env key for app from the store. Removing a key that is not set
	// is a no-op, not an error.
	UnsetAppEnv(ctx context.Context, app, key string) error

	// Policy returns the current guardrail policy: the stored guardrail dispositions
	// overlaid on the built-in defaults (DefaultPolicy), so a store with nothing set
	// returns DefaultPolicy and newly-added guardrails get a sensible default (ADR-0020).
	Policy(ctx context.Context) (Policy, error)
	// SetGuardrail persists the disposition for one guardrail — the write behind
	// `guard set`. It rejects an invalid disposition.
	SetGuardrail(ctx context.Context, code GuardrailCode, d Disposition) error

	// SaveProvider upserts a provider in the registry by name (ADR-0023). It stores only
	// the non-secret registry entry — type, capabilities, and the key under which the token
	// lives in the burrow-credentials Secret — never the token itself.
	SaveProvider(ctx context.Context, p Provider) error
	// Provider returns the provider with the given name, or ErrNotFound.
	Provider(ctx context.Context, name string) (Provider, error)
	// Providers returns all configured providers, name order. None yields an empty slice
	// and no error.
	Providers(ctx context.Context) ([]Provider, error)

	// SaveAddon upserts an add-on in the registry by name (ADR-0025). It stores the non-secret
	// registry entry — type, mode, backend, endpoint, and capabilities — never the live
	// readiness, which is probed from the cluster.
	SaveAddon(ctx context.Context, a AddonInfo) error
	// Addon returns the add-on with the given name, or ErrNotFound.
	Addon(ctx context.Context, name string) (AddonInfo, error)
	// Addons returns all registered add-ons, name order. None yields an empty slice and no
	// error. The returned entries carry no live readiness.
	Addons(ctx context.Context) ([]AddonInfo, error)
	// DeleteAddon removes the named add-on from the registry, or ErrNotFound if absent.
	DeleteAddon(ctx context.Context, name string) error

	// AppendAudit appends one audit row (ADR-0027). The log is append-only: there is no
	// update or delete path. The store assigns the row identity and orders rows by it.
	AppendAudit(ctx context.Context, entry AuditEntry) error
	// Audit returns audit rows matching filter, newest first, capped by filter.Limit (a
	// store default when unset). No matches yields an empty slice and no error.
	Audit(ctx context.Context, filter AuditFilter) ([]AuditEntry, error)

	// RecordBackup persists a new backup row (ADR-0032). burrowd records it pending before
	// starting the backup Job. The row names the app, the on-PVC path, and the status — never a
	// credential. An existing row with the same ID is overwritten.
	RecordBackup(ctx context.Context, b Backup) error
	// SetBackupStatus updates a recorded backup's status (and, when known, its size) — the
	// completed/failed transition burrowd writes when the Job finishes. Setting the status of an
	// unknown backup id returns ErrNotFound.
	SetBackupStatus(ctx context.Context, id string, status BackupStatus, sizeBytes int64) error
	// ListBackups returns recorded backups, newest first. An empty app lists every app's backups;
	// a non-empty app restricts to that app. No matches yields an empty slice and no error.
	ListBackups(ctx context.Context, app string) ([]Backup, error)
	// GetBackup returns the backup with the given id, or ErrNotFound.
	GetBackup(ctx context.Context, id string) (Backup, error)

	// CreateEnvironment registers a named environment mapping name to namespace (ADR-0035 phase 2).
	// It rejects a duplicate name (the name is the primary key) with an ErrInvalid-wrapped error.
	// The reserved `default` environment is never stored here — it is synthesized by the engine.
	CreateEnvironment(ctx context.Context, name, namespace string) error
	// ListEnvironments returns the registered environments ordered by name. None yields an empty
	// slice and no error. The synthesized `default` environment is not included; the engine
	// prepends it.
	ListEnvironments(ctx context.Context) ([]Environment, error)
	// GetEnvironment returns the registered environment with the given name, or ErrNotFound.
	GetEnvironment(ctx context.Context, name string) (Environment, error)
}

Database is the seam over the control plane's own durable state (Postgres in production): the deploy records that form the history and the rollback handles (ADR-0007). It stores domain Releases; it is independent of cluster state.

type DatabaseProvisioner

type DatabaseProvisioner interface {
	// EnsureAppDatabase idempotently provisions an isolated database and login role for app on the
	// shared instance and returns the app's DATABASE_URL (a postgres:// connection string carrying
	// a freshly generated role password). It rotates the role password on every call, so a re-attach
	// returns a fresh, working URL with no orphaned state. The app name is validated against a strict
	// identifier pattern and every SQL identifier is quoted BEFORE any SQL runs, so a name can never
	// carry SQL. The returned string is a secret value — the caller writes it straight into the app's
	// Secret and never logs, audits, or returns it.
	EnsureAppDatabase(ctx context.Context, app string) (databaseURL string, err error)
	// DropAppDatabase removes app's database and login role from the shared instance — the
	// destructive side of detach. Dropping a database/role that is already absent is a no-op, not an
	// error. The app name is validated before any SQL, exactly as in EnsureAppDatabase.
	DropAppDatabase(ctx context.Context, app string) error
}

DatabaseProvisioner is the seam over the installed Postgres add-on's admin surface (ADR-0031). burrowd connects to the shared instance as the superuser and gives each app its own database and login role inside it; the engine calls this on attach/detach. It is an optional seam — present only when the Postgres add-on path is wired; the engine errors cleanly (ErrNotImplemented) on an attach when it is nil. The connection string it returns is a secret VALUE: it is handed only to SetSecretValue and never logged, audited, returned, or carried over MCP (ADR-0029/0031).

type DeployRequest

type DeployRequest struct {
	App string `json:"app"`
	// Env is the environment to deploy into (ADR-0035 phase 2b): empty or "default" targets the
	// implicit default environment's namespace, a registered name targets that environment's
	// namespace. An unregistered name is an error.
	Env     string   `json:"env,omitempty"`
	Image   string   `json:"image"`
	Command []string `json:"command,omitempty"`
	// MetricsPort, when positive, annotates the deployed pod so the metrics add-on scrapes
	// /metrics on this container port (ADR-0026). Zero adds no annotations.
	MetricsPort int32 `json:"metrics_port,omitempty"`
	Replicas    int32 `json:"replicas"`
	// Confirm acknowledges a guardrail whose disposition is confirm, letting the operation
	// proceed past it (ADR-0020). It has no effect on a guardrail set to deny.
	Confirm bool `json:"confirm,omitempty"`
}

DeployRequest is the small, code-free description of a deploy: a pullable image plus metadata (ADR-0004). No code travels here. Env is deliberately absent: an app's non-secret config is an independently-managed, app-global store, sourced at apply time rather than passed per deploy (ADR-0028) — set it with SetConfig before deploying.

type DeployResult

type DeployResult struct {
	// Release is the new release that is now running.
	Release Release `json:"release"`
	// SupersededReleaseID is the release this deploy replaced, or "" if it was the
	// first deploy of the app. It is the handle a rollback would return to.
	SupersededReleaseID string `json:"superseded_release_id,omitempty"`
}

DeployResult reports the outcome of a successful deploy.

type Deps

type Deps struct {
	Kubernetes Kubernetes
	Database   Database
	Clock      Clock
	IDs        IDSource
	Resolver   Resolver
	// Credentials reads vendor tokens from the burrow-credentials Secret (ADR-0023).
	Credentials Credentials
	// DNS builds a DNSProvider for a vendor type and token (ADR-0023).
	DNS DNSFactory
	// Logs maps a backend id (e.g. "victorialogs", "loki") to the querier serving an installed
	// or connected logs add-on. Optional — an empty or nil map is allowed, and the engine errors
	// cleanly on a logs query when no querier is wired for the add-on's backend (ADR-0026).
	Logs map[string]LogsQuerier
	// Metrics maps a backend id (e.g. "prometheus", "victoriametrics") to the querier serving an
	// installed or connected metrics add-on. Optional — an empty or nil map is allowed, and the
	// engine errors cleanly on a metrics query when no querier is wired for the add-on's backend
	// (ADR-0026).
	Metrics map[string]MetricsQuerier
	// DatabaseProvisioner provisions a per-app database and role on the installed Postgres add-on
	// (ADR-0031). Optional — nil is allowed, and the engine errors cleanly (ErrNotImplemented) on a
	// Postgres attach when it is not wired.
	DatabaseProvisioner DatabaseProvisioner
	// ClusterProber detects the cluster's read-only capabilities (ADR-0034). Optional — nil is
	// allowed, and the engine errors cleanly (ErrNotImplemented) on a capabilities read when it is
	// not wired.
	ClusterProber ClusterProber
	// AppNamespace is the namespace burrowd deploys apps into (BURROW_NAMESPACE) — the namespace of
	// the implicit `default` environment (ADR-0035 phase 2). Optional — an empty value defaults to
	// "default", matching the kube Adapter.
	AppNamespace string
}

Deps are the dependencies an Engine needs. All seams are required. The guardrail policy is not a dependency here: the engine reads the live policy from the Database seam on each guarded operation (ADR-0020), so a `guard set` takes effect without restarting.

type Disposition

type Disposition string

Disposition is how the control plane enforces a guardrail when an operation trips it: allow it silently, hold it for explicit confirmation, or deny it outright (ADR-0020).

const (
	DispositionAllow   Disposition = "allow"
	DispositionConfirm Disposition = "confirm"
	DispositionDeny    Disposition = "deny"
)

func (Disposition) Valid

func (d Disposition) Valid() bool

Valid reports whether d is a known disposition.

type DomainResult

type DomainResult struct {
	Host     string `json:"host"`
	Provider string `json:"provider"`
	Type     string `json:"type,omitempty"`
	Address  string `json:"address,omitempty"`
}

DomainResult reports the DNS record a domain operation created, updated, or removed.

type Engine

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

Engine is the control plane's deploy orchestrator: the product. It turns an agent's deploy / status / logs / rollback / scale requests into guarded operations against the cluster, records every deploy, and returns structured results (ADR-0002, ADR-0006). It owns no global state and reads no ambient time or randomness — every external dependency is an injected seam (ADR-0010), so the engine is deterministic and unit-testable against fakes.

func New

func New(d Deps) (*Engine, error)

New constructs an Engine, validating that every seam is supplied and the policy is coherent. It returns an error rather than panicking so wiring mistakes surface at startup.

func (*Engine) AddDomain

func (e *Engine) AddDomain(ctx context.Context, req AddDomainRequest) (DomainResult, error)

AddDomain creates or updates a DNS record pointing Host at an address, through the named provider (ADR-0018). The address is given explicitly (req.Address) or derived from an exposed app's ingress (req.App) so the agent need not look it up. It is a guarded operation: pointing a public hostname at the cluster is a blast-radius change, so it trips the dns.write guardrail (confirm by default). The record is an A record when the address is an IPv4 address, a CNAME otherwise. The provider must be configured and serve DNS; burrowd holds the token and is the only thing that calls the vendor.

func (*Engine) AddEnvironment added in v0.7.0

func (e *Engine) AddEnvironment(ctx context.Context, name, namespace string) (Environment, error)

AddEnvironment registers a named environment mapping name to namespace (ADR-0035 phase 2). It validates name as a DNS-1123-label-safe lowercase token and rejects the reserved `default` (which is implicit), then records it. The namespace and burrowd's Role there are created kubeconfig-side by `burrow env add` before this call — burrowd holds only namespaced Roles and cannot create namespaces or RBAC itself (least privilege), so the engine only records the registry entry. A duplicate name is rejected by the store.

func (*Engine) AddProvider

func (e *Engine) AddProvider(ctx context.Context, req AddProviderRequest) (Provider, error)

AddProvider records a vendor credential in the registry, after validating the token and writing it into the burrow-credentials Secret (ADR-0023, ADR-0030). The token VALUE arrives in req over burrowd's authenticated control-plane API; burrowd is the only thing that holds it. The order is validate-then-write: build the vendor adapter with the passed token and make a cheap authenticated call, and only on success write the token to the Secret and record the registry entry. A rejected token returns an error and writes NOTHING — no rollback needed, since nothing is written until validation passes. The token is never logged, never stored in Postgres, and never returned. The provider's capabilities are derived from its type.

func (*Engine) AttachAddon

func (e *Engine) AttachAddon(ctx context.Context, t AddonType, app string) (AttachResult, error)

AttachAddon gives app its own database on the installed Postgres add-on and wires it into the app (ADR-0031). burrowd provisions an isolated database + login role on the shared instance, generates the DATABASE_URL server-side, writes it into the app's per-app Secret via the SetSecretValue path (ADR-0029), and restarts the app so envFrom picks it up. Attach provisions and destroys nothing, so it is allowed by default (no guardrail) and is safe over MCP: no secret value crosses MCP — the agent supplies only the app name; burrowd generates the value and never returns it. The audit row records {addon, app} only — never the URL.

func (*Engine) Audit

func (e *Engine) Audit(ctx context.Context, filter AuditFilter) ([]AuditEntry, error)

Audit returns audit rows matching filter, newest first (ADR-0027). It is a read-only, guarded control-plane operation: the agent and the operator may review the trail, but no API path writes to or alters it. The rows never carry a secret value — they were redacted at write time.

func (*Engine) Autoscale added in v0.8.0

func (e *Engine) Autoscale(ctx context.Context, app, env string, spec AutoscaleSpec, confirm bool) (AutoscaleResult, error)

Autoscale configures autoscaling for an app: it applies an autoscaling/v2 HorizontalPodAutoscaler on the app's Deployment with the requested replica band and utilization targets (ADR-0006). It is guarded twice — the app.autoscale guardrail gates the operation (allow by default), and the app.replica_ceiling guardrail bounds the requested max the same way it bounds a manual scale, so a max above the ceiling is denied exactly like scaling above it. The HPA is applied even when metrics-server is absent (creating it needs no metrics); the result then carries a Warning that it will not scale until metrics-server is installed.

func (*Engine) BackupAddon

func (e *Engine) BackupAddon(ctx context.Context, t AddonType, app string) (BackupResult, error)

BackupAddon backs up app's database on the installed Postgres add-on (ADR-0032): burrowd records a pending backup, runs an in-cluster Job that pg_dumps the database to the backup PVC, and marks the backup completed (or failed). The backup is recorded in the control-plane database — burrowd is not mounted to the backup PVC, so the database, not the volume, is the index of backups. It moves no secret value: the Job reads the superuser password only via secretKeyRef, and the audit row and the returned result name the add-on, app, backup id, path, and size — never a credential. Backup is allowed by default (it destroys nothing) and safe over MCP.

func (*Engine) ClusterCapabilities

func (e *Engine) ClusterCapabilities(ctx context.Context) (ClusterCapabilities, error)

ClusterCapabilities reports what the cluster can do, read live so out-of-band changes are always reflected (ADR-0034). It runs the cluster probe through the ClusterProber seam and fills the DNS capability from the providers registry (ADR-0023) — a control-plane fact, not a cluster read. It is read-only: it changes nothing in the cluster or the registry.

func (*Engine) ConnectAddon

func (e *Engine) ConnectAddon(ctx context.Context, backend, endpoint, secretKey, token string) (AddonInfo, error)

ConnectAddon registers an existing backend the user already runs (e.g. an in-cluster Loki) as a queryable add-on, recording its endpoint and derived capabilities in the registry (ADR-0026). Unlike install it deploys nothing and is not guarded — connect is registration-only. Connecting the same backend twice upserts, updating the endpoint. secretKey is the (non-secret) key under which a bearer token for an authenticated backend lives in the burrow-credentials Secret; "" means the backend is unauthenticated. token is the bearer token VALUE for an authenticated backend: it arrives over burrowd's authenticated control-plane API and is written into burrow-credentials under secretKey (ADR-0030). The value is never logged, never stored in Postgres, never returned, and never carried over MCP — only the key is recorded in the registry (ADR-0004/0023). A token without a secretKey is invalid. The registry entry that crosses the API holds only the key.

func (*Engine) DeleteApp

func (e *Engine) DeleteApp(ctx context.Context, app, env string, confirm bool) error

DeleteApp removes an app entirely: its workload, its routing (Service/Ingress), and its release history, so the app disappears from the apps listing and from status. It is guarded by app.delete, which holds the destructive teardown for confirmation by default (ADR-0020). The app must exist — it has either recorded releases or a live workload; an app unknown to both is ErrNotFound. Teardown tolerates an already-absent piece: an ErrNotFound from the workload or routing delete means that piece is already gone, not a failure.

func (*Engine) Deploy

func (e *Engine) Deploy(ctx context.Context, req DeployRequest) (DeployResult, error)

Deploy rolls out an image by reference (ADR-0007). It validates the request, applies the guardrails, records a new release, applies it to the cluster, and records the outcome — superseding the previously running release on success. burrowd never contacts the registry: the workload is applied by image reference and the kubelet resolves and pulls it with the imagePullSecret (ADR-0040). The image bytes never pass through here; only the reference does (ADR-0004).

func (*Engine) DetachAddon

func (e *Engine) DetachAddon(ctx context.Context, t AddonType, app string, confirm bool) error

DetachAddon removes app's DATABASE_URL and, behind the addon.detach confirm guardrail (it destroys data), drops app's database and role from the cluster-shared Postgres instance (ADR-0031). The audit row records {addon, app} only.

func (*Engine) DisableAutoscale added in v0.8.0

func (e *Engine) DisableAutoscale(ctx context.Context, app, env string, confirm bool) error

DisableAutoscale turns autoscaling off for an app by removing its HorizontalPodAutoscaler (ADR-0006). It is guarded by the same app.autoscale guardrail and audited. It is idempotent: removing autoscaling from an app that has none succeeds without error.

func (*Engine) Expose

func (e *Engine) Expose(ctx context.Context, req ExposeRequest) (ExposeResult, error)

Expose makes an app reachable at a hostname through an Ingress (ADR-0018). It is a guarded operation: public exposure trips the app.expose_public guardrail, which holds for confirmation by default. The app must already be deployed.

func (*Engine) Guardrails

func (e *Engine) Guardrails(ctx context.Context, env string) ([]GuardrailInfo, error)

Guardrails returns the guardrail policy as a list for inspection (ADR-0020). With an empty or "default" env it returns the global policy; with a named environment it returns that environment's effective policy under the env to global to default fallback, each entry marking whether its disposition is env-specific or inherited (ADR-0035 phase 2c). A named environment must be registered; an unknown one is a clear ErrNotFound.

func (*Engine) InstallAddon

func (e *Engine) InstallAddon(ctx context.Context, t AddonType, confirm bool) (AddonInfo, error)

InstallAddon deploys the vetted backing service for the named add-on type and registers it as a queryable capability (ADR-0025/0026). It is guarded by addon.install.

func (*Engine) ListAddons

func (e *Engine) ListAddons(ctx context.Context) ([]AddonInfo, error)

ListAddons returns the registered add-on instances from the registry, with live readiness probed from the cluster for installed ones (ADR-0025). A readiness probe failure leaves an entry not-ready rather than failing the whole listing.

func (*Engine) ListApps

func (e *Engine) ListApps(ctx context.Context, env string) ([]WorkloadStatus, error)

Status returns the combined control-plane and cluster view of an app: the most recent ListApps returns the workload status of every Burrow-managed app, for an apps listing. It reads the cluster — the source of truth for what is running.

func (*Engine) ListBackups

func (e *Engine) ListBackups(ctx context.Context, t AddonType, app string) ([]Backup, error)

ListBackups returns recorded backups, newest first, from the control-plane database (ADR-0032). An empty app lists every app's backups; a non-empty app restricts to that app. Read-only and safe over MCP — it names the app, size, time, and on-PVC path, never a credential.

func (*Engine) ListConfig added in v0.7.0

func (e *Engine) ListConfig(ctx context.Context, app, env string) (map[string]string, error)

ListConfig returns the app's non-secret config store (ADR-0028). An app with no config yields an empty map and no error.

func (*Engine) ListEnvironments added in v0.7.0

func (e *Engine) ListEnvironments(ctx context.Context) ([]Environment, error)

ListEnvironments returns the environments the cluster's burrowd knows about (ADR-0035 phase 2): the implicit `default` environment first (the app namespace burrowd runs against, behaving like today), followed by the registered environments in name order. The default is synthesized rather than stored, so multi-environment is opt-in with no regression.

func (*Engine) ListSecrets

func (e *Engine) ListSecrets(ctx context.Context, app, env string) ([]string, error)

ListSecrets returns the env-var KEYS in an app's per-app Secret, sorted, never the values (ADR-0028/0004). Secret values live only in the Kubernetes Secret and never cross the API or MCP, so this read returns keys only. An app with no secrets yields an empty slice.

func (*Engine) Logs

func (e *Engine) Logs(ctx context.Context, app, env string, opts LogOptions) ([]LogLine, error)

Logs returns recent log lines for an app's workload.

func (*Engine) Providers

func (e *Engine) Providers(ctx context.Context) ([]Provider, error)

Providers returns the configured providers, name order (ADR-0023). It reports the non-secret registry only; no token is read.

func (*Engine) QueryLogs

func (e *Engine) QueryLogs(ctx context.Context, query string, limit int, backend string) ([]LogEntry, error)

QueryLogs runs query against the installed logs add-on and returns up to limit records. It is the read path behind the agent's logs-query tool: it locates the add-on advertising the "logs" capability and queries it through the LogsQuerier seam (ADR-0026). An empty backend picks the first logs add-on; a non-empty backend targets a specific one (by its concrete backend or its registry name) when more than one serves logs.

func (*Engine) QueryMetrics

func (e *Engine) QueryMetrics(ctx context.Context, query string, backend string) ([]MetricSample, error)

QueryMetrics runs an instant PromQL query against the connected metrics add-on and returns the matching samples. It is the read path behind the agent's metrics-query tool: it locates the add-on advertising the "metrics" capability and queries it through the MetricsQuerier seam (ADR-0026). An empty backend picks the first metrics add-on; a non-empty backend targets a specific one (by its concrete backend or its registry name) when more than one serves metrics.

func (*Engine) Reachability

func (e *Engine) Reachability(ctx context.Context, app, env string) (ReachabilityResult, error)

Reachability reports, link by link, whether an app is reachable at its hostname (ADR-0018): deployed and ready, exposed, given an external address by an ingress controller, and DNS pointing the host at that address. It returns a structured chain plus a one-line plain summary for a non-expert; it never errors on a missing link — that is the answer.

func (*Engine) RemoveAddon

func (e *Engine) RemoveAddon(ctx context.Context, name string, confirm bool) error

RemoveAddon removes the named add-on instance. It is guarded by addon.remove (removing a backing service can break dependent apps).

func (*Engine) RemoveDomain

func (e *Engine) RemoveDomain(ctx context.Context, req RemoveDomainRequest) (DomainResult, error)

RemoveDomain deletes the DNS record the provider holds for Host (ADR-0018). It trips the dns.delete guardrail (confirm by default) — it is the destructive side of DNS management.

func (*Engine) RestoreAddon

func (e *Engine) RestoreAddon(ctx context.Context, t AddonType, app, backupID string, confirm bool) error

RestoreAddon restores app's database from a recorded backup, overwriting its live contents (ADR-0032). It is behind the addon.restore confirm guardrail (it destroys live data), runs an in-cluster Job that pg_restores the named dump, and records the restore in the audit log. The Job reads the superuser password only via secretKeyRef; the audit row records {addon, app, backup} only — never a credential.

func (*Engine) Rollback

func (e *Engine) Rollback(ctx context.Context, app, env string, confirm bool) (RollbackResult, error)

Rollback restores the app's previously running release by redeploying its reference (ADR-0007). It finds the current running release, re-applies the release that one superseded, and records the rollback as a new release. It returns ErrNotFound when there is nothing to roll back from or to.

func (*Engine) Scale

func (e *Engine) Scale(ctx context.Context, app, env string, replicas int32, confirm bool) (ScaleResult, error)

Scale changes an app's replica count, guarded against scale-to-zero and the policy ceiling (ADR-0006). It does not create a new release: scaling adjusts the running workload, while a release records a deploy.

func (*Engine) SetConfig added in v0.7.0

func (e *Engine) SetConfig(ctx context.Context, app, env, key, value string, noRestart bool) error

SetConfig upserts one non-secret config var for an app in the config store (ADR-0028). The store is the single source of truth for the app's config. By default the change re-applies the running workload so it rolls and the running app picks the value up; with noRestart the value is only persisted and lands on the next deploy. An app with no running release simply persists and skips the apply — not an error. Config vars are non-secret, so there is no guardrail.

func (*Engine) SetGuardrail

func (e *Engine) SetGuardrail(ctx context.Context, env string, code GuardrailCode, d Disposition) error

SetGuardrail sets one guardrail's disposition (ADR-0020). It rejects an unknown guardrail or an invalid disposition as ErrInvalid. This is the operator's lever — exposed via the CLI, never as an MCP tool, so the agent cannot change its own guardrails.

With an empty or "default" env it sets the global disposition for code (today's behavior). With a named environment it stores the env-prefixed code (e.g. prod.app.delete) so the environment's policy can diverge from the global one (ADR-0035 phase 2c). A named environment must be registered (an unknown one is ErrNotFound, catching typos), and only the app-level guardrails are env-scopable: a cluster-level guardrail (addon.*, dns.*) gates a cluster-wide operation and can only be set globally, so env-scoping one is rejected as ErrInvalid.

func (*Engine) SetSecret

func (e *Engine) SetSecret(ctx context.Context, app, env, key, value string, noRestart bool) error

SetSecret upserts one key=value into an app's per-app Secret and, unless noRestart, rolls the running workload so it picks the value up (ADR-0029). The value arrives over burrowd's authenticated control-plane API and is written here through the Kubernetes seam; it is NEVER logged, never audited, never stored in Postgres, and never carried over MCP — only its KEY name appears in any error (the value is never formatted into one). Setting a value still cannot be done over MCP: there is no secret-set MCP tool. An app with no running workload just writes the Secret; the change lands on the next deploy.

func (*Engine) Status

func (e *Engine) Status(ctx context.Context, app, env string) (StatusResult, error)

recorded release and the live workload state. It returns ErrNotFound only when the app is unknown to both.

func (*Engine) Unexpose

func (e *Engine) Unexpose(ctx context.Context, app, env string) error

Unexpose removes an app's exposure (its Service and Ingress). It does not affect the workload. Unexposing an app that was never exposed returns ErrNotFound.

func (*Engine) UnsetConfig added in v0.7.0

func (e *Engine) UnsetConfig(ctx context.Context, app, env, key string, noRestart bool) error

UnsetConfig removes one config var for an app from the config store (ADR-0028). Like SetConfig it re-applies the running workload by default so the running app drops the value, or only persists with noRestart. An app with no running release simply persists and skips the apply.

func (*Engine) UnsetSecret

func (e *Engine) UnsetSecret(ctx context.Context, app, env, key string, noRestart bool) error

UnsetSecret removes one key from an app's per-app Secret and, unless noRestart, rolls the running workload so it drops the value (ADR-0028). Removing a key carries no value, and this is MCP-allowed. An app with no running workload just updates the Secret; the change lands on the next deploy. Removing an absent key succeeds.

type Environment added in v0.7.0

type Environment struct {
	// Name is the environment handle, a DNS-1123 label (e.g. "staging", "prod").
	Name string `json:"name"`
	// Namespace is the Kubernetes namespace this environment's apps deploy into.
	Namespace string `json:"namespace"`
	// Default reports whether this is the implicit `default` environment (the app namespace
	// burrowd already runs against). Registered environments are never default.
	Default bool `json:"default"`
	// CreatedAt is when the environment was registered, read from the injected clock. It is the
	// zero time for the synthesized default environment.
	CreatedAt time.Time `json:"created_at,omitempty"`
}

Environment is a named app environment for namespace-per-environment (ADR-0035 phase 2): one cluster, several app namespaces, one per environment. Name is the operator-facing handle (a DNS-1123 label); Namespace is the Kubernetes namespace the environment's apps deploy into. Default marks the implicit `default` environment, which is synthesized from the engine's app namespace rather than registered.

type ExposeRequest

type ExposeRequest struct {
	App string `json:"app"`
	// Env is the environment whose namespace the app lives in (ADR-0035 phase 2b): empty or
	// "default" targets the default environment, a registered name targets that environment.
	Env  string `json:"env,omitempty"`
	Host string `json:"host"`
	Port int32  `json:"port"`
	// TLS requests an HTTPS certificate for Host via cert-manager; Issuer names the
	// ClusterIssuer to use.
	TLS    bool   `json:"tls,omitempty"`
	Issuer string `json:"issuer,omitempty"`
	// Confirm acknowledges the app.expose_public guardrail so the operation proceeds past it.
	Confirm bool `json:"confirm,omitempty"`
}

ScaleResult reports the outcome of a scale. ExposeRequest describes making an app reachable at a hostname (ADR-0018).

type ExposeResult

type ExposeResult struct {
	App  string `json:"app"`
	Host string `json:"host"`
	Port int32  `json:"port"`
	URL  string `json:"url"`
}

ExposeResult reports the outcome of exposing an app.

type ExposeSpec

type ExposeSpec struct {
	// App is the application to expose; its workload provides the Service's backends.
	App string
	// Host is the external hostname to route, e.g. app.example.com.
	Host string
	// Port is the app's container port the Service forwards to. Must be positive.
	Port int32
	// TLS requests an HTTPS certificate for Host via cert-manager (the Ingress is annotated
	// for the Issuer ClusterIssuer, and a TLS Secret is named for cert-manager to fill).
	TLS bool
	// Issuer is the cert-manager ClusterIssuer to request the certificate from when TLS.
	Issuer string
}

ExposeSpec describes how to make an app reachable at a hostname (ADR-0018). v0.2 routes HTTP to the app's Service via an Ingress, optionally with TLS issued by cert-manager.

type ExposureStatus

type ExposureStatus struct {
	Exposed bool
	Host    string
	Address string
	// TLS reports whether the Ingress requests a certificate (its spec has a TLS entry).
	TLS bool
	// CertReady reports whether the requested TLS certificate has been issued (its Secret holds a
	// certificate). It is meaningful only when TLS is true.
	CertReady bool
}

ExposureStatus is the observed state of an app's exposure, for the reachability surface (ADR-0018). Address is the controller-assigned external IP or hostname, read from the Ingress's status; it is empty until an ingress controller assigns one.

type GuardrailCode

type GuardrailCode string

GuardrailCode identifies a guardrail: it is both the key a Policy configures the guardrail's disposition under and the machine-readable reason it appears in a refusal, so an agent can branch on the cause rather than parse prose (ADR-0006, ADR-0020).

const (
	// GuardrailAppDeploy: the operation would deploy a new release of an app. Deploy is the
	// core action, so it is allowed by default and gated only if an operator opts in — set it
	// to confirm to require sign-off before a deploy (e.g. in prod), or to deny to freeze
	// deploys entirely, per environment. Realizes ADR-0007: the explicit deploy call is where
	// the guardrails live.
	GuardrailAppDeploy GuardrailCode = "app.deploy"
	// GuardrailReplicaCeiling: the requested replica count exceeds Policy.MaxReplicas.
	GuardrailReplicaCeiling GuardrailCode = "app.replica_ceiling"
	// GuardrailScaleToZero: the operation would scale to zero replicas.
	GuardrailScaleToZero GuardrailCode = "app.scale_to_zero"
	// GuardrailExposePublic: the operation would make an app reachable from outside the
	// cluster (expose it at a hostname).
	GuardrailExposePublic GuardrailCode = "app.expose_public"
	// GuardrailDNSWrite: the operation would create or update a public DNS record at a
	// configured provider (ADR-0018).
	GuardrailDNSWrite GuardrailCode = "dns.write"
	// GuardrailDNSDelete: the operation would delete a public DNS record at a configured
	// provider — the destructive side of DNS management (ADR-0018).
	GuardrailDNSDelete GuardrailCode = "dns.delete"
	// GuardrailAddonInstall: the operation would install a building-block backing service
	// (a vetted add-on like logs or metrics) onto the cluster (ADR-0025).
	GuardrailAddonInstall GuardrailCode = "addon.install"
	// GuardrailAddonRemove: the operation would remove an installed add-on — the destructive
	// side, since dependent apps may rely on it (ADR-0025).
	GuardrailAddonRemove GuardrailCode = "addon.remove"
	// GuardrailAddonDetach: the operation would detach an app from an add-on — for Postgres,
	// dropping the app's database and role and destroying its data (ADR-0031). Held for
	// confirmation by default. (Attach is not guarded: it provisions, it destroys nothing.)
	GuardrailAddonDetach GuardrailCode = "addon.detach"
	// GuardrailAddonRestore: the operation would restore an app's database from a backup,
	// overwriting its live contents (ADR-0032). Held for confirmation by default, like detach and
	// app delete. (Backup and list are not guarded: they destroy nothing.)
	GuardrailAddonRestore GuardrailCode = "addon.restore"
	// GuardrailAppDelete: the operation would delete an app entirely — its workload, routing,
	// and release history — so it disappears from the apps listing. The destructive teardown
	// of a deployed application.
	GuardrailAppDelete GuardrailCode = "app.delete"
	// GuardrailRollback: the operation would roll an app back to its previous release. A
	// production mutation, but a recovery one — allowed by default so an agent can restore a
	// broken app quickly; an operator can set it to confirm or deny to require sign-off for
	// server-side, agent-independent enforcement (ADR-0020).
	GuardrailRollback GuardrailCode = "app.rollback"
	// GuardrailAutoscale: the operation would configure (or turn off) autoscaling for an app — apply
	// a HorizontalPodAutoscaler on its Deployment. Allowed by default: autoscaling is helpful and
	// non-destructive, and the autoscaler's max is independently bounded by the replica ceiling
	// (GuardrailReplicaCeiling). An operator can raise it to confirm or deny per environment, e.g.
	// deny in prod so only a human sets the scaling shape there.
	GuardrailAutoscale GuardrailCode = "app.autoscale"
)

type GuardrailError

type GuardrailError struct {
	// Operation is the operation that was gated (e.g. "deploy", "scale").
	Operation string
	// Code is the machine-readable guardrail that tripped.
	Code GuardrailCode
	// Message is a human-readable explanation.
	Message string
	// Requested is the value the caller asked for (e.g. the replica count).
	Requested int32
	// Limit is the relevant policy limit, when the code involves one.
	Limit int32
	// NeedsConfirmation is true when the operation was not refused outright but requires
	// explicit confirmation to proceed (disposition confirm). A plain deny leaves it false.
	NeedsConfirmation bool
}

GuardrailError is returned when the control plane declines a dangerous operation or holds it for confirmation. It is a structured outcome, not a system failure: the operation was understood and deliberately gated. Callers distinguish it with AsGuardrail.

func AsGuardrail

func AsGuardrail(err error) (*GuardrailError, bool)

AsGuardrail reports whether err is (or wraps) a GuardrailError and returns it.

func (*GuardrailError) Error

func (e *GuardrailError) Error() string

type GuardrailInfo

type GuardrailInfo struct {
	Code        GuardrailCode `json:"code"`
	Disposition Disposition   `json:"disposition"`
	Description string        `json:"description"`
	// Source reports where the effective disposition came from when the guardrail is inspected for a
	// named environment (ADR-0035 phase 2c): "env" for an environment-specific override, "global" for
	// the global policy, or "default" for the built-in default. It is empty for the global listing.
	Source string `json:"source,omitempty"`
}

GuardrailInfo describes a guardrail and its current disposition, for inspection through `guard list` and the read-only MCP guard tool (ADR-0020).

type IDSource

type IDSource interface {
	// NewID returns a fresh release identifier.
	NewID() string
}

IDSource mints release identifiers. It is a seam (ADR-0010): the engine never reads ambient randomness or time to make an ID, so a test can supply a deterministic counter while production supplies a UUID minter. Implementations must return a non-empty, unique string on each call.

type IngressCapability

type IngressCapability struct {
	// Present is true only when a ready ingress controller is running — the signal that an expose
	// will actually get an external address and admission webhook. It is not implied by Classes.
	Present bool `json:"present"`
	// Classes are the IngressClass names that exist, sorted. A class may be present while Present is
	// false (an orphan class whose controller was removed).
	Classes []string `json:"classes,omitempty"`
}

IngressCapability reports the cluster's ingress-controller situation. Present — the "you can expose" signal — is true only when an ingress controller is actually running (a ready ingress-nginx controller Deployment), NOT merely when an IngressClass exists: an IngressClass is cluster-scoped and can outlive the controller that created it (deleting the ingress-nginx release and its namespace leaves the "nginx" class orphaned), and an orphan class routes nothing. Classes are the IngressClass names found, sorted; they are reported independently of Present because binding an Ingress still needs the class name (e.g. while the controller is being reinstalled).

type Kubernetes

type Kubernetes interface {
	// WithNamespace returns a view of this seam whose per-app resource operations (deploy,
	// status, logs, scale, delete, expose/unexpose, and the per-app Secret) act in ns instead
	// of the configured app namespace — the mechanism that routes an operation to a named
	// environment's namespace (ADR-0035 phase 2). Add-on operations are unaffected: add-ons live
	// in their own namespace. An empty ns, or ns equal to the configured app namespace, returns a
	// view equivalent to the receiver, so default-environment behavior is identical to before
	// environments existed.
	WithNamespace(ns string) Kubernetes

	// ApplyWorkload creates or updates the workload for spec.App to match spec.
	ApplyWorkload(ctx context.Context, spec WorkloadSpec) error
	// WorkloadStatus returns the observed state of app's workload, or ErrNotFound if
	// no workload exists for it.
	WorkloadStatus(ctx context.Context, app string) (WorkloadStatus, error)
	// ListWorkloads returns the observed state of every Burrow-managed workload in the
	// namespace (for an apps listing). No workloads is an empty slice, not an error.
	ListWorkloads(ctx context.Context) ([]WorkloadStatus, error)

	// DeployAddon installs a building-block backing service per spec — a workload, a
	// ClusterIP Service, and a persistent volume when the spec asks for one — and returns
	// the instance's connection info (ADR-0025). Installing an already-installed add-on is
	// idempotent.
	DeployAddon(ctx context.Context, spec AddonSpec) (AddonInfo, error)
	// AddonReady reports whether the named add-on's backing Deployment is available. It is a
	// cheap single-Deployment readiness probe — readiness is a live property, not stored in the
	// registry. A missing Deployment is reported as not ready (false, nil), not an error.
	AddonReady(ctx context.Context, name string) (bool, error)
	// DeleteAddon removes the named add-on instance and its resources. Removing an add-on
	// that is not installed returns ErrNotFound.
	DeleteAddon(ctx context.Context, name string) error
	// ScaleWorkload sets the desired replica count for app's workload.
	ScaleWorkload(ctx context.Context, app string, replicas int32) error

	// ApplyAutoscaler creates or updates an autoscaling/v2 HorizontalPodAutoscaler named after app,
	// targeting app's Deployment, per spec — the replica band and the CPU (and optional memory)
	// utilization targets (ADR-0006). It is create-or-update: re-applying adjusts the existing HPA.
	// Creating the HPA does not require metrics-server; only its scaling does.
	ApplyAutoscaler(ctx context.Context, app string, spec AutoscaleSpec) error
	// DeleteAutoscaler removes app's HorizontalPodAutoscaler. Deleting an absent HPA is a no-op, not
	// an error, so turning autoscaling off is idempotent.
	DeleteAutoscaler(ctx context.Context, app string) error
	// AutoscalerActive reports whether app has an active HorizontalPodAutoscaler owning its replica
	// count. A workload apply consults it so a deploy (or rollback, or config/secret reapply) leaves
	// the HPA-managed count untouched rather than resetting it. A missing HPA is reported as inactive
	// (false, nil), not an error.
	AutoscalerActive(ctx context.Context, app string) (bool, error)
	// MetricsAPIAvailable reports whether the metrics.k8s.io API group is served (metrics-server is
	// installed), so the engine can warn that an applied HPA will not scale until it is. It is
	// best-effort by contract: the engine treats an error as "absent" and warns rather than failing,
	// so a discovery hiccup never blocks applying the HPA.
	MetricsAPIAvailable(ctx context.Context) (bool, error)
	// Logs returns recent log lines for app's workload.
	Logs(ctx context.Context, app string, opts LogOptions) ([]LogLine, error)
	// DeleteWorkload removes app's workload. Deleting a missing workload returns
	// ErrNotFound.
	DeleteWorkload(ctx context.Context, app string) error

	// Expose makes app reachable at a hostname by creating (or updating) a Service and an
	// Ingress that routes the host to it (ADR-0018). It does not create the workload —
	// Deploy does — and whether the host is actually reachable also depends on an ingress
	// controller and DNS, which the reachability surface reports on.
	Expose(ctx context.Context, spec ExposeSpec) error
	// Unexpose removes the Service and Ingress created by Expose. Unexposing an app that
	// was never exposed returns ErrNotFound.
	Unexpose(ctx context.Context, app string) error
	// ExposureStatus reports whether app is exposed, at what host, and the external address
	// the ingress controller assigned its Ingress (read from the Ingress's
	// status.loadBalancer — empty until a controller processes it). A never-exposed app
	// returns a zero ExposureStatus and no error.
	ExposureStatus(ctx context.Context, app string) (ExposureStatus, error)

	// SecretKeys returns the env-var names held in app's per-app Secret
	// (burrow-app-<app>-secrets), sorted, never the values (ADR-0028/0004). A missing
	// Secret yields an empty slice and no error — an app with no secrets set.
	SecretKeys(ctx context.Context, app string) ([]string, error)
	// SetSecretValue upserts one key=value into app's per-app Secret, creating the
	// Secret if absent (ADR-0029). The value arrives over burrowd's authenticated
	// control-plane API and is written here to the Kubernetes Secret — it is NEVER
	// logged, never audited (the audit log records the key name only), never stored in
	// Postgres, and never carried over MCP. Any error this returns must name the app and
	// key only, never the value.
	SetSecretValue(ctx context.Context, app, key, value string) error
	// UnsetSecretKey removes one key from app's per-app Secret. A missing Secret or a
	// missing key is a no-op, not an error — unsetting what is already absent succeeds.
	// The value never crosses this seam: only the key name does.
	UnsetSecretKey(ctx context.Context, app, key string) error
	// RestartWorkload triggers a rolling update of app's Deployment by bumping the
	// pod-template annotation burrow.cloud/restarted-at to at. It is how a secret change
	// (read only at pod start via envFrom) forces the running app to pick it up. A missing
	// Deployment returns ErrNotFound; the caller treats that as "nothing running to roll".
	RestartWorkload(ctx context.Context, app string, at time.Time) error

	// RunBackupJob runs a one-shot Job in the add-on namespace that pg_dumps app's database on the
	// installed Postgres instance to /<backup-pvc>/<app>/<backupID>.dump (custom format), ensuring
	// the backup PVC first (ADR-0032). The Job connects as the superuser, reading the password from
	// the burrow-postgres Secret via secretKeyRef env — never a CLI argument, never logged. It
	// blocks until the Job completes, returns an error if the Job fails or times out, and reaps the
	// Job on success. It returns the dump's size in bytes when the dump container reported it (the
	// pod's terminated-state message), or 0 when unknown. app is validated before any Job is built.
	RunBackupJob(ctx context.Context, app, backupID string) (sizeBytes int64, err error)
	// RunRestoreJob runs a one-shot Job in the add-on namespace that pg_restores
	// /<backup-pvc>/<app>/<backupID>.dump into app's database (--clean --if-exists, so it replaces
	// current contents). Like RunBackupJob it reads the superuser password only via secretKeyRef,
	// blocks until the Job completes, errors on failure or timeout, and reaps the Job on success.
	// app is validated before any Job is built.
	RunRestoreJob(ctx context.Context, app, backupID string) error
}

Kubernetes is the seam over the target cluster: the only path from the control plane to the runtime. It is deliberately narrow — the v0.1 operations (deploy, status, logs, scale, and the delete that supports teardown) and nothing more.

type LoadBalancerCapability

type LoadBalancerCapability struct {
	Supported bool   `json:"supported"`
	Inferred  bool   `json:"inferred"`
	Provider  string `json:"provider,omitempty"`
}

LoadBalancerCapability reports whether Service type=LoadBalancer is likely supported, and by what (ADR-0043). Supported is true when any LoadBalancer provider is present: a recognized cloud provider (a billable cloud load balancer), k3s's built-in servicelb, or MetalLB. Provider names which one — a cloud id (e.g. "digitalocean"), "servicelb", or "metallb" — empty when none is detected; only a cloud provider is billable. Inferred is always true: this recognizes a provider, not a direct probe (provisioning a LoadBalancer is the real test).

type LogEntry

type LogEntry struct {
	Time    string `json:"time,omitempty"`
	Message string `json:"message"`
	Pod     string `json:"pod,omitempty"`
}

LogEntry is one record returned by a logs query.

type LogLine

type LogLine struct {
	Pod       string    `json:"pod"`
	Timestamp time.Time `json:"timestamp"`
	Message   string    `json:"message"`
}

LogLine is a single line of application log output.

type LogOptions

type LogOptions struct {
	// TailLines bounds how many of the most recent lines to return. Zero means an
	// adapter-defined default.
	TailLines int
}

LogOptions selects which log lines to return.

type LogsQuerier

type LogsQuerier interface {
	// QueryLogs runs query against the logs store reachable at endpoint (an in-cluster
	// host:port) and returns up to limit matching records, most recent first. token is a bearer
	// credential for an authenticated backend; an empty token means unauthenticated and no
	// Authorization header is sent.
	QueryLogs(ctx context.Context, endpoint, query string, limit int, token string) ([]LogEntry, error)
}

LogsQuerier queries a logs backing service (an installed or connected add-on) for records matching a query, so the agent can answer "what happened? / why is it slow?" (ADR-0026). It is an optional seam — present only when logs querying is wired; the engine errors cleanly if not.

type MetricSample

type MetricSample struct {
	Labels map[string]string `json:"labels,omitempty"`
	Value  string            `json:"value"`
	Time   string            `json:"time,omitempty"`
}

MetricSample is one sample returned by a metrics query. Value is the metric's value as a string so PromQL's exact numeric formatting (precision, NaN/Inf) is preserved rather than lost to a float round-trip.

type MetricsQuerier

type MetricsQuerier interface {
	// QueryMetrics runs an instant PromQL query against the Prometheus-API-compatible store reachable
	// at endpoint (an in-cluster host:port) and returns the matching samples. token is a bearer
	// credential for an authenticated backend; an empty token means unauthenticated and no
	// Authorization header is sent.
	QueryMetrics(ctx context.Context, endpoint, query string, token string) ([]MetricSample, error)
}

MetricsQuerier runs an instant PromQL query against a Prometheus-API-compatible metrics store (an installed or connected add-on) so the agent can answer "how is my app performing? / what's the CPU, memory, or error rate?" (ADR-0026). It is an optional seam — present only when metrics querying is wired; the engine errors cleanly if not.

type MissingPrerequisitesError added in v0.8.0

type MissingPrerequisitesError struct {
	// Host is the hostname the exposure targeted.
	Host string
	// TLS reports whether the exposure asked for HTTPS (so TLS prerequisites applied).
	TLS bool
	// Missing enumerates the prerequisites that are absent, each with its fix.
	Missing []Prerequisite
}

MissingPrerequisitesError reports that an expose request cannot be satisfied because the cluster is not set up for the public, optionally TLS-terminated reachability it asked for. It is a structured outcome, not a system failure: the request was understood, but the cluster lacks one or more prerequisites. It enumerates each missing prerequisite and the burrow command that resolves it, so an agent can guide the user back onto Burrow's path rather than falling back to raw kubectl to diagnose the cluster (ADR-0006). Callers distinguish it with AsMissingPrerequisites.

func AsMissingPrerequisites added in v0.8.0

func AsMissingPrerequisites(err error) (*MissingPrerequisitesError, bool)

AsMissingPrerequisites reports whether err is (or wraps) a MissingPrerequisitesError and returns it, mirroring AsGuardrail so a front end (the HTTP API, the MCP server) can surface the structured checklist without parsing prose.

func (*MissingPrerequisitesError) Error added in v0.8.0

func (e *MissingPrerequisitesError) Error() string

type Policy

type Policy struct {
	// Dispositions configures how each guardrail is enforced — allow, confirm, or deny
	// (ADR-0020), keyed by GuardrailCode. A guardrail with no entry here defaults to deny:
	// the safe default.
	Dispositions map[GuardrailCode]Disposition
	// MaxReplicas is the largest replica count permitted before the app.replica_ceiling
	// guardrail's disposition applies. Must be positive.
	MaxReplicas int32
}

Policy is the guardrail configuration the control plane evaluates dangerous operations against (ADR-0006). This type carries the limits; the evaluation that gates, constrains, or refuses an operation against them is the deploy engine's job.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy returns the conservative starting guardrail policy (ADR-0020): a modest replica ceiling that denies oversized scale-ups, and scale-to-zero held for confirmation — recoverable with an explicit confirm rather than silently allowed or hard-denied. The operator can relax or tighten any of these with `guard set`.

func (Policy) Guardrails

func (p Policy) Guardrails() []GuardrailInfo

Guardrails returns each known guardrail with its effective disposition under the global policy (ADR-0020). Use GuardrailsFor to inspect a named environment's effective policy.

func (Policy) GuardrailsFor added in v0.7.0

func (p Policy) GuardrailsFor(env string) []GuardrailInfo

GuardrailsFor returns each known guardrail with its effective disposition for the named environment (ADR-0035 phase 2c): the disposition under the env-prefixed override, falling back to the global override, then the built-in default. Each entry's Source records where the effective disposition came from ("env", "global", or "default") so `guard list --env` can show which guardrails are env-specific and which are inherited. An empty or "default" env reproduces the global policy exactly (and leaves Source unset, as for Guardrails).

func (Policy) Validate

func (p Policy) Validate() error

Validate reports whether the policy is internally coherent.

func (Policy) With

func (p Policy) With(code GuardrailCode, d Disposition) Policy

With returns a copy of the policy with code's disposition set, leaving the receiver unchanged — the basis for `guard set` and for composing policies.

type Prerequisite added in v0.8.0

type Prerequisite struct {
	// Name is the missing piece (e.g. "ingress controller", "cert-manager", "DNS provider").
	Name string `json:"name"`
	// Detail says what is missing and why the exposure needs it.
	Detail string `json:"detail"`
	// Fix is the burrow command that provisions it.
	Fix string `json:"fix"`
}

Prerequisite names one cluster prerequisite a public, optionally TLS-terminated exposure needs but which is missing, paired with the exact burrow command that provisions it (ADR-0006, ADR-0034). It is the structured unit an agent reads to fix the cluster's setup without inspecting it directly with kubectl.

type Provider

type Provider struct {
	// Name identifies the provider within the registry. It defaults to the type but can be
	// set explicitly so a user can register two providers of the same type. It must be a
	// DNS-1123 label.
	Name string `json:"name"`
	// Type is the vendor this provider talks to.
	Type ProviderType `json:"type"`
	// Capabilities are the services this provider serves, derived from its type.
	Capabilities []Capability `json:"capabilities"`
	// SecretKey is the key under which this provider's token lives in burrow-credentials.
	SecretKey string `json:"secret_key"`
	// CreatedAt is when the provider was registered, read from the injected clock.
	CreatedAt time.Time `json:"created_at"`
}

Provider is a configured vendor credential in the registry (ADR-0023): a name, the vendor type, the capabilities it serves, and the key in the burrow-credentials Secret that holds its token. The token itself never appears here — the registry is the non-secret structure, stored in the database; the control plane reads the token from the Secret at call time, so a rotation needs no restart.

func (Provider) Serves

func (p Provider) Serves(c Capability) bool

Serves reports whether the provider serves capability c.

func (Provider) Validate

func (p Provider) Validate() error

Validate reports whether the provider is well-formed enough to record.

type ProviderCapability

type ProviderCapability struct {
	Cloud string `json:"cloud,omitempty"`
	Name  string `json:"name,omitempty"`
}

ProviderCapability reports the detected cloud provider. Cloud is the provider id (e.g. "digitalocean", "aws"), empty when unknown or bare-metal; Name is a human label (e.g. "DigitalOcean").

type ProviderType

type ProviderType string

ProviderType identifies a vendor Burrow knows how to talk to. The type implies the capabilities it can serve; the adapter that actually makes the API calls plugs in per capability (ADR-0023). A credential is held per provider, so a user can split services across vendors (e.g. compute on DigitalOcean, DNS on Cloudflare).

const (
	// ProviderDigitalOcean is DigitalOcean (DNS for v0.2; compute later).
	ProviderDigitalOcean ProviderType = "digitalocean"
	// ProviderCloudflare is Cloudflare (DNS).
	ProviderCloudflare ProviderType = "cloudflare"
)

func SupportedProviderTypes

func SupportedProviderTypes() []ProviderType

SupportedProviderTypes returns the provider types Burrow supports, sorted — for help text and the message shown when an unsupported type is requested.

func (ProviderType) Capabilities

func (t ProviderType) Capabilities() []Capability

Capabilities returns the capabilities a provider of type t serves, as a fresh slice so callers cannot mutate the registry's knowledge.

func (ProviderType) Valid

func (t ProviderType) Valid() bool

Valid reports whether t is a provider type Burrow supports.

type ReachabilityResult

type ReachabilityResult struct {
	App                string   `json:"app"`
	Deployed           bool     `json:"deployed"`
	Ready              bool     `json:"ready"`
	Exposed            bool     `json:"exposed"`
	Host               string   `json:"host,omitempty"`
	Address            string   `json:"address,omitempty"` // controller-assigned external address
	TLS                bool     `json:"tls"`               // the Ingress requests an HTTPS certificate
	CertReady          bool     `json:"cert_ready"`        // the requested TLS certificate has been issued
	DNSPointsAtCluster bool     `json:"dns_points_at_cluster"`
	DNSAddresses       []string `json:"dns_addresses,omitempty"`
	// Reachable is the converged verdict: every link in the chain is green and the app is live.
	Reachable bool `json:"reachable"`
	// URL is where the app is live when Reachable (https when TLS was requested, else http); it
	// is empty until Reachable.
	URL string `json:"url,omitempty"`
	// BlockedOn names the first unready link when not Reachable (e.g. "ingress controller",
	// "tls certificate", "dns"); it is empty when Reachable. It is the one link to fix next.
	BlockedOn string `json:"blocked_on,omitempty"`
	Summary   string `json:"summary"`
}

ReachabilityResult reports whether an app is reachable at its hostname, link by link, for the reachability surface (ADR-0018, ADR-0022). Summary is a one-line, plain-English verdict for a non-expert; the fields are the full chain for advanced users and the agent.

type Release

type Release struct {
	// ID is the control-plane-assigned identifier for this release. It is minted by
	// the deploy engine, not here, so the domain type stays free of ambient identity.
	ID string `json:"id"`
	// App is the name of the App this release belongs to.
	App string `json:"app"`
	// Image is the pullable container image reference the deploy named (ADR-0007).
	Image string `json:"image"`
	// Digest is the content digest of the running Image, when known (e.g. "sha256:...").
	// burrowd never resolves it against the registry (ADR-0040); it is an observed value
	// read back from Kubernetes, so it is empty until observed. Read-back is a deferred
	// follow-up (ADR-0040 §3), so for now it is left unset.
	Digest string `json:"digest,omitempty"`
	// Env is the environment passed to the workload.
	Env map[string]string `json:"env,omitempty"`
	// Command overrides the image's default command, when set.
	Command []string `json:"command,omitempty"`
	// MetricsPort, when positive, is the container port the app serves Prometheus metrics on.
	// The deploy annotates the pod (prometheus.io/scrape, /port, /path) so the metrics add-on's
	// scraper discovers and scrapes /metrics on it. Zero means no metrics annotations (ADR-0026).
	MetricsPort int32 `json:"metrics_port,omitempty"`
	// Replicas is the desired replica count.
	Replicas int32 `json:"replicas"`
	// Status is the lifecycle state of this release.
	Status ReleaseStatus `json:"status"`
	// Supersedes is the ID of the release this one replaced, if any — the chain that
	// lets rollback walk back to a prior known-good release.
	Supersedes string `json:"supersedes,omitempty"`
	// CreatedAt is when the control plane recorded this release, read from the
	// injected clock (never from ambient time).
	CreatedAt time.Time `json:"created_at"`
}

Release is one immutable deploy of an App: the unit recorded in the deploy history and the handle rollback redeploys. It captures exactly what was asked for — the pullable image, the resolved digest, and the small metadata that travels over MCP (env, command, replica count) — never any code (ADR-0004).

func (Release) Validate

func (r Release) Validate() error

Validate reports whether the release is well-formed enough to act on. It checks the fields a caller supplies; the engine-assigned ID is not required here so the same validation can run before an ID is minted.

type ReleaseStatus

type ReleaseStatus string

ReleaseStatus is the lifecycle state of a Release.

const (
	// ReleasePending is a release that has been recorded but not yet rolled out.
	ReleasePending ReleaseStatus = "pending"
	// ReleaseDeployed is a release that rolled out successfully and is (or was) running.
	ReleaseDeployed ReleaseStatus = "deployed"
	// ReleaseFailed is a release whose rollout did not succeed.
	ReleaseFailed ReleaseStatus = "failed"
	// ReleaseSuperseded is a release replaced by a newer one (deploy or rollback).
	ReleaseSuperseded ReleaseStatus = "superseded"
)

func (ReleaseStatus) Valid

func (s ReleaseStatus) Valid() bool

Valid reports whether s is a known ReleaseStatus.

type RemoveDomainRequest

type RemoveDomainRequest struct {
	Host     string `json:"host"`
	Provider string `json:"provider"`
	// Confirm acknowledges the dns.delete guardrail so the operation proceeds past it.
	Confirm bool `json:"confirm,omitempty"`
}

RemoveDomainRequest removes the DNS record a provider holds for a host (ADR-0018).

type Resolver

type Resolver interface {
	// LookupHost returns the IP addresses host resolves to, or an error (e.g. NXDOMAIN).
	LookupHost(ctx context.Context, host string) ([]string, error)
}

Resolver is the control plane's DNS lookups, injected so reachability checks stay deterministic in tests (ADR-0018). It reports the addresses a hostname resolves to.

type RollbackResult

type RollbackResult struct {
	// Release is the new release created by the rollback (carrying the prior
	// reference) and now running.
	Release Release `json:"release"`
	// RolledBackToReleaseID is the prior release whose reference was restored.
	RolledBackToReleaseID string `json:"rolled_back_to_release_id"`
	// SupersededReleaseID is the release that was running before the rollback.
	SupersededReleaseID string `json:"superseded_release_id"`
}

RollbackResult reports the outcome of a rollback. A rollback is itself a forward deploy of a prior reference (ADR-0007), so it produces a new Release.

type ScaleResult

type ScaleResult struct {
	App              string `json:"app"`
	PreviousReplicas int32  `json:"previous_replicas"`
	Replicas         int32  `json:"replicas"`
}

type StatusResult

type StatusResult struct {
	App string `json:"app"`
	// HasRelease reports whether the control plane has any release recorded for the
	// app; Release holds the most recent one when true.
	HasRelease bool    `json:"has_release"`
	Release    Release `json:"release,omitempty"`
	// Running reports whether a workload currently exists in the cluster; Workload
	// holds its observed state when true.
	Running  bool           `json:"running"`
	Workload WorkloadStatus `json:"workload,omitempty"`
}

StatusResult is the combined control-plane and cluster view of an app.

type StorageCapability

type StorageCapability struct {
	DefaultPresent bool     `json:"default_present"`
	DefaultClass   string   `json:"default_class,omitempty"`
	Classes        []string `json:"classes,omitempty"`
}

StorageCapability reports the cluster's persistent-storage situation. DefaultPresent is true when a StorageClass carries the default-class annotation; DefaultClass is its name; Classes are all StorageClass names found, sorted.

type WorkloadKind

type WorkloadKind string

WorkloadKind names the Kubernetes resource a workload maps to (ADR-0011). The seam speaks in workloads rather than a single resource type so new kinds are additive. The empty value means WorkloadDeployment.

const (
	// WorkloadDeployment is a stateless Deployment — the only kind v0.1 uses.
	WorkloadDeployment WorkloadKind = "Deployment"
	// WorkloadStatefulSet is a stateful StatefulSet, for workloads needing stable
	// identity, persistent volumes, or ordered rollout. Not used in v0.1; reserved so
	// adding it later is additive, not a rename.
	WorkloadStatefulSet WorkloadKind = "StatefulSet"
)

type WorkloadSpec

type WorkloadSpec struct {
	App     string
	Kind    WorkloadKind
	Image   string
	Env     map[string]string
	Command []string
	// MetricsPort, when positive, is the container port the app serves Prometheus metrics on.
	// buildDeployment annotates the pod template (prometheus.io/scrape, /port, /path) so the
	// metrics add-on's scraper discovers and scrapes /metrics on it. Zero adds no annotations.
	MetricsPort int32
	Replicas    int32
	// ReleaseID is the release this workload is applying. It is stamped on the pod template (under
	// ReleaseAnnotation) so a new release always rolls the workload, even when the image reference
	// is unchanged; re-applying the same release reuses the same ID, so the apply stays idempotent.
	// Empty adds no annotation.
	ReleaseID string
}

WorkloadSpec is the desired state of one App's Kubernetes workload — the small, code-free description a deploy turns into (ADR-0004): a kind, a pullable image, and metadata.

type WorkloadStatus

type WorkloadStatus struct {
	App             string       `json:"app"`
	Kind            WorkloadKind `json:"kind"`
	Image           string       `json:"image"`
	DesiredReplicas int32        `json:"desired_replicas"`
	ReadyReplicas   int32        `json:"ready_replicas"`
	UpdatedReplicas int32        `json:"updated_replicas"`
	// Available reports whether the workload currently meets its availability
	// condition (enough ready replicas to serve).
	Available bool `json:"available"`
	// Issue is a human- and agent-actionable explanation of why an unavailable workload is
	// blocked, when the cluster reports a genuinely blocking pod condition — e.g. a pull
	// failure that names the image, the registry host, and the `burrow config registry login`
	// fix (ADR-0006). It is best-effort enrichment: empty when the workload is healthy or
	// when no blocking condition was observed, so it never becomes a required field.
	Issue string `json:"issue,omitempty"`
	// IssueReason is the raw, machine-usable Kubernetes reason behind Issue (e.g.
	// "ImagePullBackOff" or "ErrImagePull"), for an agent that wants to branch on the cause
	// rather than parse the prose. Empty whenever Issue is empty.
	IssueReason string `json:"issue_reason,omitempty"`
}

WorkloadStatus is the observed state of an App's workload, as reported by the cluster.

Directories

Path Synopsis
Package api is the control plane's HTTP front end: it exposes the deploy engine's operations over JSON and authenticates its callers with a bearer token (ADR-0005).
Package api is the control plane's HTTP front end: it exposes the deploy engine's operations over JSON and authenticates its callers with a bearer token (ADR-0005).
Package dns is the production controlplane.DNSFactory and the per-vendor DNS adapters (ADR-0018, ADR-0023).
Package dns is the production controlplane.DNSFactory and the per-vendor DNS adapters (ADR-0018, ADR-0023).
Package e2e holds end-to-end tests that drive the deploy engine through its real adapters — the client-go Kubernetes adapter against a live cluster and the registry resolver against a real registry — proving the whole vertical slice composes (ADR-0010).
Package e2e holds end-to-end tests that drive the deploy engine through its real adapters — the client-go Kubernetes adapter against a live cluster and the registry resolver against a real registry — proving the whole vertical slice composes (ADR-0010).
Package internal holds the control plane's implementation guts: the deploy state machine, the Kubernetes/registry/database seam adapters, and the guardrail policy.
Package internal holds the control plane's implementation guts: the deploy state machine, the Kubernetes/registry/database seam adapters, and the guardrail policy.
fake
Package fake provides in-memory implementations of the control-plane seams (controlplane.Clock, Kubernetes, Registry, Database) for tests.
Package fake provides in-memory implementations of the control-plane seams (controlplane.Clock, Kubernetes, Registry, Database) for tests.
Package kube is the production controlplane.Kubernetes adapter, built on the official client-go SDK (ADR-0011).
Package kube is the production controlplane.Kubernetes adapter, built on the official client-go SDK (ADR-0011).
Package logs holds the production controlplane.LogsQuerier adapters — the seam burrowd uses to query an installed or connected logs backing service (ADR-0026).
Package logs holds the production controlplane.LogsQuerier adapters — the seam burrowd uses to query an installed or connected logs backing service (ADR-0026).
Package metrics holds the adapters that query a metrics backing service (an installed or connected add-on) for the agent's metrics-query path (ADR-0026).
Package metrics holds the adapters that query a metrics backing service (an installed or connected add-on) for the agent's metrics-query path (ADR-0026).
Package postgres is the production controlplane.Database adapter, backed by Postgres running in the cluster (ADR-0012).
Package postgres is the production controlplane.Database adapter, backed by Postgres running in the cluster (ADR-0012).
Package sys holds the production implementations of the control plane's system seams — the wall Clock, a crypto/rand ID source, and the DNS Resolver — the concrete values cmd/burrowd injects in place of the test fakes (ADR-0010).
Package sys holds the production implementations of the control plane's system seams — the wall Clock, a crypto/rand ID source, and the DNS Resolver — the concrete values cmd/burrowd injects in place of the test fakes (ADR-0010).

Jump to

Keyboard shortcuts

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