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
- Variables
- func AddonDataVolumeName(t AddonType, instance string) string
- func AddonInstanceName(t AddonType, env string) (string, error)
- func AppSecretName(app string) string
- func BackupFailureReasons() []string
- func BackupObjectKey(app, env, id string) string
- func BackupPath(app, id string) string
- func BackupVolumeName(t AddonType, env string) (string, error)
- func BuildFitsState(state ClusterResourceState) (fits bool, verdict string)
- func ContextWithClientVersion(ctx context.Context, version string) context.Context
- func DependencyReasons() []string
- func DiscrepancyReasons() []string
- func EnvScopable(code GuardrailCode) bool
- func EnvScopableLimit(code LimitCode) bool
- func HumanCPU(millis int64) string
- func HumanMemory(bytes int64) string
- func ImagePullIssue(image, reason, message string) string
- func IsBackupFailureReason(reason string) bool
- func IsDependencyReason(reason string) bool
- func IsImagePullReason(reason string) bool
- func IsIssueReason(reason string) bool
- func IsLedgerReason(reason string) bool
- func IssueReasons() []string
- func KnownGuardrail(code GuardrailCode) bool
- func KnownHookPhase(p HookPhase) bool
- func KnownLimit(code LimitCode) bool
- func LedgerDetail(s string) string
- func LedgerReasons() []string
- func MarshalProbePlan(p ProbePlan) (string, error)
- func PgBackRestLabelFromManifestKey(key string) string
- func PgBackRestManifestKey(repoPath, stanza, label string) string
- func PgBackRestRepoPath(env string) string
- func RegistryHost(image string) string
- func ReservedEnvironmentNames() []string
- func ResolveAutoDeploy(current string, available []string, level AutoDeployLevel) (target string, upgrade string)
- func ValidateHealthPath(path string) error
- func ValidateHealthPort(port int32) error
- type AddDomainRequest
- type AddProviderRequest
- type AddonInfo
- type AddonRemoval
- type AddonSpec
- type AddonType
- type AddonVolume
- type AmbiguousEnvironmentError
- type App
- type AppDatabaseLister
- type AppEnvRef
- type ArchiveDestination
- type AttachResult
- type AuditEntry
- type AuditFilter
- type AuditOutcome
- type AutoDeployConfig
- type AutoDeployLevel
- type AutoDeployPoller
- type AutoDeployStatus
- type AutoscaleResult
- type AutoscaleSpec
- type Backup
- type BackupDestination
- type BackupDestinationHealth
- type BackupDestinationKind
- type BackupHealth
- type BackupHealthState
- type BackupJobOutcome
- type BackupKind
- type BackupObservation
- type BackupResult
- type BackupStatus
- type BuildRequest
- type BuildResult
- type Builder
- type Capability
- type CapacityProber
- type CapacityReport
- type CertManagerCapability
- type ChecksReport
- type Clock
- type CloudNativePGCapability
- type ClusterCapabilities
- type ClusterConfigFunc
- type ClusterProber
- type ClusterResourceState
- type ConnectBackend
- type Consumer
- type Coverage
- type CoverageGap
- type Credentials
- type DNSCapability
- type DNSFactory
- type DNSProvider
- type DNSRecord
- type DNSRecordType
- type Database
- type DatabaseProvisioner
- type Dependency
- type DependencyKind
- type DependencyOutcome
- type DependencyResult
- type DeployRequest
- type DeployResult
- type Deps
- type Disposition
- type DomainResult
- type Engine
- func (e *Engine) AddDomain(ctx context.Context, req AddDomainRequest) (DomainResult, error)
- func (e *Engine) AddEnvironment(ctx context.Context, name, namespace string) (Environment, error)
- func (e *Engine) AddProvider(ctx context.Context, req AddProviderRequest) (Provider, error)
- func (e *Engine) AppChecks(ctx context.Context, app, env string) (ChecksReport, error)
- func (e *Engine) AppHealth(ctx context.Context, app, env string) (HealthReport, error)
- func (e *Engine) AttachAddon(ctx context.Context, t AddonType, app, env string) (AttachResult, error)
- func (e *Engine) Audit(ctx context.Context, filter AuditFilter) ([]AuditEntry, error)
- func (e *Engine) AutoDeploy(ctx context.Context, app, env string) (AutoDeployLevel, error)
- func (e *Engine) AutoDeployStatus(ctx context.Context, app, env string) (AutoDeployStatus, error)
- func (e *Engine) Autoscale(ctx context.Context, app, env string, spec AutoscaleSpec, confirm bool) (AutoscaleResult, error)
- func (e *Engine) BackupAddon(ctx context.Context, t AddonType, app, env, destination string) (BackupResult, error)
- func (e *Engine) BackupHealth(ctx context.Context, t AddonType, app, env string) (BackupHealth, error)
- func (e *Engine) BackupInstance(ctx context.Context, t AddonType, env, destination string) (BackupResult, error)
- func (e *Engine) Build(ctx context.Context, req BuildRequest) (BuildResult, error)
- func (e *Engine) ClusterCapabilities(ctx context.Context) (ClusterCapabilities, error)
- func (e *Engine) ClusterCapacity(ctx context.Context) (CapacityReport, error)
- func (e *Engine) ConnectAddon(ctx context.Context, backend, endpoint, secretKey, token string) (AddonInfo, error)
- func (e *Engine) DeleteApp(ctx context.Context, app, env string, confirm bool) error
- func (e *Engine) Deploy(ctx context.Context, req DeployRequest) (DeployResult, error)
- func (e *Engine) DetachAddon(ctx context.Context, t AddonType, app, env string, confirm bool) error
- func (e *Engine) DisableAutoscale(ctx context.Context, app, env string, confirm bool) error
- func (e *Engine) EnsureDefaultEnvironment(ctx context.Context) (Environment, error)
- func (e *Engine) Expose(ctx context.Context, req ExposeRequest) (ExposeResult, error)
- func (e *Engine) Failures(ctx context.Context, q FailureQuery) (FailureReport, error)
- func (e *Engine) Guardrails(ctx context.Context, env string) ([]GuardrailInfo, error)
- func (e *Engine) History(ctx context.Context, app, env string) ([]Release, error)
- func (e *Engine) Hooks(ctx context.Context, app, env string) ([]Hook, error)
- func (e *Engine) InstallAddon(ctx context.Context, t AddonType, env string, opts InstallAddonOptions) (AddonInfo, error)
- func (e *Engine) Limits(ctx context.Context, env string) ([]LimitInfo, error)
- func (e *Engine) ListAddons(ctx context.Context) ([]AddonInfo, error)
- func (e *Engine) ListApps(ctx context.Context, env string) ([]WorkloadStatus, error)
- func (e *Engine) ListBackups(ctx context.Context, t AddonType, app, env string) ([]Backup, error)
- func (e *Engine) ListConfig(ctx context.Context, app, env string) (map[string]string, error)
- func (e *Engine) ListEnvironments(ctx context.Context) ([]Environment, error)
- func (e *Engine) ListSecrets(ctx context.Context, app, env string) ([]string, error)
- func (e *Engine) Logs(ctx context.Context, app, env string, opts LogOptions) ([]LogLine, error)
- func (e *Engine) NewAutoDeployPoller(cfg AutoDeployConfig) *AutoDeployPoller
- func (e *Engine) NewObserver(cfg ObserverConfig) *Observer
- func (e *Engine) NextTag(ctx context.Context, app, env string) (NextTagResult, error)
- func (e *Engine) ObjectStoreCredentialFor(ctx context.Context, p Provider) (ObjectStoreCredential, error)
- func (e *Engine) Providers(ctx context.Context) ([]Provider, error)
- func (e *Engine) QueryLogs(ctx context.Context, query string, limit int, backend string) ([]LogEntry, error)
- func (e *Engine) QueryMetrics(ctx context.Context, query string, backend string) ([]MetricSample, error)
- func (e *Engine) QueryMetricsRange(ctx context.Context, query string, backend string, start, end time.Time, ...) ([]MetricSeries, error)
- func (e *Engine) Reachability(ctx context.Context, app, env string) (ReachabilityResult, error)
- func (e *Engine) RemoveAddon(ctx context.Context, name string, opts RemoveAddonOptions) (RemoveAddonResult, error)
- func (e *Engine) RemoveDomain(ctx context.Context, req RemoveDomainRequest) (DomainResult, error)
- func (e *Engine) RemoveEnvironment(ctx context.Context, name string) error
- func (e *Engine) RestoreAddon(ctx context.Context, t AddonType, app, backupID, env string, confirm bool) error
- func (e *Engine) RestoreInstance(ctx context.Context, t AddonType, env string, opts RestoreInstanceOptions) (RestoreInstanceResult, error)
- func (e *Engine) RetainedAddonVolumes(ctx context.Context) ([]AddonVolume, error)
- func (e *Engine) Rollback(ctx context.Context, app, env string, confirm bool) (RollbackResult, error)
- func (e *Engine) Run(ctx context.Context, req RunRequest) (RunResult, error)
- func (e *Engine) Scale(ctx context.Context, app, env string, replicas int32, confirm bool) (ScaleResult, error)
- func (e *Engine) SetAppChecks(ctx context.Context, app, env string, enabled bool) (ChecksReport, error)
- func (e *Engine) SetAppHealth(ctx context.Context, app, env, path string, port int32) (HealthReport, error)
- func (e *Engine) SetAutoDeploy(ctx context.Context, app, env string, level AutoDeployLevel) error
- func (e *Engine) SetConfig(ctx context.Context, app, env, key, value string, noRestart bool) error
- func (e *Engine) SetGuardrail(ctx context.Context, env string, code GuardrailCode, d Disposition) error
- func (e *Engine) SetHook(ctx context.Context, app, env string, phase HookPhase, command []string) (Hook, error)
- func (e *Engine) SetLimit(ctx context.Context, env string, code LimitCode, value string) error
- func (e *Engine) SetSecret(ctx context.Context, app, env, key, value string, noRestart bool) error
- func (e *Engine) Status(ctx context.Context, app, env string) (StatusResult, error)
- func (e *Engine) Unexpose(ctx context.Context, app, env string) error
- func (e *Engine) UnsetAppHealth(ctx context.Context, app, env string) (HealthReport, error)
- func (e *Engine) UnsetConfig(ctx context.Context, app, env, key string, noRestart bool) error
- func (e *Engine) UnsetHook(ctx context.Context, app, env string, phase HookPhase) error
- func (e *Engine) UnsetSecret(ctx context.Context, app, env, key string, noRestart bool) error
- type Environment
- type ExposeRequest
- type ExposeResult
- type ExposeSpec
- type Exposure
- type ExposureStatus
- type Failure
- type FailureFilter
- type FailureKey
- type FailureKind
- type FailureObservation
- type FailureQuery
- type FailureReport
- type GuardrailCode
- type GuardrailError
- type GuardrailInfo
- type HealthEndpoint
- type HealthReport
- type Hook
- type HookError
- type HookPhase
- type IDSource
- type IngressCapability
- type InstallAddonOptions
- type IssueEvidence
- type JobBlockedError
- type Kubernetes
- type LedgerPruneResult
- type LifecycleCheck
- type LifecycleRule
- type LifecycleStatus
- type LimitCode
- type LimitError
- type LimitInfo
- type LimitKind
- type LoadBalancerCapability
- type LogEntry
- type LogLine
- type LogOptions
- type LogsQuerier
- type MetricPoint
- type MetricSample
- type MetricSeries
- type MetricsQuerier
- type MetricsRangeQuerier
- type MetricsServerCapability
- type MissingPrerequisitesError
- type NextTagResult
- type NextTags
- type NodeAllocatable
- type NodeCapacity
- type ObjectRef
- type ObjectStore
- type ObjectStoreConfig
- type ObjectStoreCredential
- type ObjectStoreFactory
- type ObservationWindow
- type Observer
- type ObserverConfig
- type OperationalConfig
- func (c OperationalConfig) Count(env string, code LimitCode) (int64, string)
- func (c OperationalConfig) Duration(env string, code LimitCode) (time.Duration, string)
- func (c OperationalConfig) Limits(env string) []LimitInfo
- func (c OperationalConfig) ReplicaCeiling(env string) (int32, string)
- func (c OperationalConfig) With(code LimitCode, value string) OperationalConfig
- type PgBackRestCapability
- type PhysicalBackupOutcome
- type PodRequest
- type Policy
- type Prerequisite
- type ProbeCheck
- type ProbePlan
- type ProbeReport
- type ProbeSpec
- type Provider
- type ProviderCapability
- type ProviderType
- type ProviderVerification
- type ReachabilityResult
- type ReadinessCheck
- type RegistryAuth
- type RegistryClient
- type Release
- type ReleaseStatus
- type ReleaseTrigger
- type RemoveAddonOptions
- type RemoveAddonResult
- type RemoveDomainRequest
- type Resolver
- type RestoreInstanceOptions
- type RestoreInstanceOutcome
- type RestoreInstanceRequest
- type RestoreInstanceResult
- type RollbackResult
- type RolloutOutcome
- type RunRequest
- type RunResult
- type RunSpec
- type ScaleResult
- type SourceCredential
- type SourceRef
- type StatusResult
- type StorageCapability
- type StrandedApp
- type WorkloadKind
- type WorkloadSpec
- type WorkloadStatus
Constants ¶
const ( // AddonVolumeData is the add-on's own data volume: the claim its workload mounts. AddonVolumeData = "data" // AddonVolumeBackup is the Postgres add-on's dump volume (PostgresBackupVolume). AddonVolumeBackup = "backup" )
Add-on volume roles: what a claim in the add-on namespace holds. The role decides what a retained claim is worth keeping for — a data claim comes back to life on reinstall, a backup claim is a pile of dumps that outlives the database it came from (ADR-0032, ADR-0064 §4).
const ( // MaxDeploySettleTimeout is the largest value an operator may set the deploy.settle_timeout // limit to. It is the limit catalogue's declared ceiling (see knownLimits), stated as a // constant so the catalogue and the callers that must outlast it read the same symbol. MaxDeploySettleTimeout = 30 * time.Minute // RunJobTimeout is how long the control plane waits for a one-off command Job to reach a // terminal state before it reports the command as timed out (ADR-0048). Lifecycle hooks are // the same machinery and take the same bound (ADR-0072), so a deploy can spend it twice: once // on a `pre-deploy` hook and once on a `post-deploy` one. RunJobTimeout = 10 * time.Minute // BuildJobTimeout is how long the control plane waits for an in-cluster build Job (ADR-0053). // A build ends in a deploy, so a build call can spend this AND the whole deploy budget. BuildJobTimeout = 30 * time.Minute // BackupJobTimeout is how long the control plane waits for a backup or restore Job // (ADR-0032, ADR-0063). BackupJobTimeout = 10 * time.Minute // PostgresRecoveryTimeout is how long the control plane waits for an instance recovered from its // pgBackRest repository to start serving (ADR-0066 §4). It is much larger than a backup's bound // because a recovery restores a base backup and then replays archived write-ahead log, both // proportional to the data — and unlike a backup, giving up on the wait does NOT stop the work: // the recovering `Cluster` is left in place and the operator watches it. PostgresRecoveryTimeout = 30 * time.Minute // PostgresClaimRemovalTimeout is how long the control plane waits for the pre-restore instance's // data claim to actually disappear before recovering into its name (ADR-0066 §4). A claim is held // by the pvc-protection finalizer while any pod mounts it, so the delete returns long before the // object does — and creating the recovering instance too early would reattach it to the volume it // was asked to leave behind. PostgresClaimRemovalTimeout = 5 * time.Minute // ObjectStoreCallTimeout bounds ONE HTTP call to an S3-compatible object store (ADR-0063). // Registering an object-storage provider makes several in a row — create the bucket, write, // read and delete the probe object, read and reconcile the lifecycle configuration — so the // call's own bound is a multiple of this, not this. ObjectStoreCallTimeout = 30 * time.Second )
How long an API call may occupy the server, declared in one place so a CLIENT can derive its own bound from it instead of choosing one next to it.
Several control-plane calls wait on the cluster before they answer: a deploy waits for its rollout to settle, a build waits for a build Job, a run waits for a one-off Job. Each of those waits is bounded on the server by a number the control plane owns. A client that gives up FIRST does not stop any of it — burrowd carries on, finishes the rollout, runs the hook, and records the release — it only reports a failure that did not happen, and the obvious response to a failed deploy is to deploy again.
That is not hypothetical. The direct-URL client carried a blanket sixty-second HTTP timeout chosen before any call legitimately took minutes; once a deploy started waiting for its rollout the shorter of the two bounds won, and a successful deploy was reported as failed (issue #404). The two numbers were never wrong individually — they were chosen independently, which is the defect this file exists to remove. Everything a caller needs to size its own bound is declared here, and `client` derives its per-request budgets from these constants rather than restating them.
The values are CEILINGS, not expectations. A deploy normally answers in seconds; MaxDeployWait is what a deploy is ALLOWED to take when every phase runs to its own bound, which is the only number a client bound may safely be compared against. Sizing a client to the typical case is how the bug comes back the first time an operator raises a limit.
const ( // MaxDeployWait is the longest a deploy request can occupy the server: a `pre-deploy` hook Job, // then the rollout settle, then the deploy-time dependency check (ADR-0076 §4), then the // `post-deploy` hook's Job (ADR-0072 §4). // // The settle bound appears ONCE, and that is load-bearing. Both the check and the hook want to // know how the rollout went, and each used to wait for itself — so on a WEDGED rollout, which is // precisely the case a bound exists for, a deploy ran the settle timeout twice and this constant // had to declare the doubled figure to every client that derives from it (issue #407). The deploy // now settles once and hands the same observation to both, so the ceiling is the bound the // operator actually configured rather than two of them. // // Rollback runs a `pre-rollback` hook and the same `post-deploy` settle and hook, which is strictly // less than this, so a caller that sizes rollback to the deploy budget covers it. MaxDeployWait = RunJobTimeout + MaxDeploySettleTimeout + dependencyCheckDeadline + RunJobTimeout // post-deploy hook Job // MaxBuildWait is the longest a build request can occupy the server: the build Job, then the // deploy the built image rejoins (ADR-0053 §4). MaxBuildWait = BuildJobTimeout + MaxDeployWait // MaxRunWait is the longest a one-off command request can occupy the server. MaxRunWait = RunJobTimeout // MaxBackupWait is the longest a backup, a restore, or an add-on removal that takes a final // backup first (ADR-0064 §5) can occupy the server. MaxBackupWait = BackupJobTimeout // MaxRestoreInstanceWait is the longest a physical restore can occupy the server: the safety // backup of the state it is about to destroy, the wait for that state's volume to go, and the // recovery itself (ADR-0066 §4). // // The DATABASE_URL cutover afterwards is not a phase with its own ceiling — it is a handful of // admin statements and a Secret write per app — and it is not added here rather than being given // an invented bound, which is the rule this file holds: a client's budget is a sum of bounds the // control plane declares, never of ones a reader estimated. MaxRestoreInstanceWait = MaxBackupWait + PostgresClaimRemovalTimeout + PostgresRecoveryTimeout // MaxProviderWait is the longest registering an object-storage provider can occupy the server: // six object-store calls at their own bound — create the bucket, put/get/delete the probe // object, and read then write the lifecycle configuration (ADR-0063 §2-§4). MaxProviderWait = 6 * ObjectStoreCallTimeout )
The longest one API request may take, per call, assembled from the bounds above. Each is the sum of the phases that request runs, every phase at its own ceiling.
const ( // ReasonCredentialUnset is the credential Burrow wrote not being present in the container's // environment at all. It is the misconfiguration this check exists for: the app was attached, and // something — a Secret that was not remounted, an env var shadowed by config — means the running // container does not see it. ReasonCredentialUnset = "CredentialUnset" // ReasonCredentialUnparsable is a credential present but not a connection string the driver can // read. The value itself is never quoted. ReasonCredentialUnparsable = "CredentialUnparsable" // ReasonHostUnresolvable is the dependency's hostname not resolving from inside the app's own // container — most often an app pointed at a Service in another namespace. ReasonHostUnresolvable = "HostUnresolvable" // ReasonConnectionRefused is the host resolving and nothing accepting on the port. ReasonConnectionRefused = "ConnectionRefused" // ReasonAuthenticationFailed is the dependency rejecting the credential. It names no part of the // credential. ReasonAuthenticationFailed = "AuthenticationFailed" // ReasonTimedOut is the dependency not answering inside the check's own dial window. ReasonTimedOut = "TimedOut" // ReasonQueryFailed is a connection that succeeded and a trivial query that did not — the shape a // role without CONNECT on the database it was pointed at leaves. ReasonQueryFailed = "QueryFailed" // ReasonUnreachable is every other way the dependency did not answer. It is the catch-all so that // an unclassified failure is still a member of the closed set rather than prose. ReasonUnreachable = "Unreachable" // ReasonCheckNotRun is a SKIPPED reason: the check pod could not be run to completion — it could // not be scheduled, the image could not be pulled, or Burrow stopped waiting. It says nothing // about the dependency. ReasonCheckNotRun = "CheckNotRun" // ReasonNotDerivable is a SKIPPED reason: Burrow could not read what it needed to decide whether // the dependency exists. An unreachable Postgres instance leaves this rather than an empty list, // because "no dependency" and "could not ask" must not read the same. ReasonNotDerivable = "NotDerivable" )
The closed reason vocabulary for a dependency check. It is deliberately SEPARATE from ADR-0074 §2's IssueReason set and §6's discrepancy reasons rather than an extension of either: every IssueReason is a blocking condition the cluster reported about a pod, and every discrepancy reason is a registry-versus-cluster verdict, whereas these are the answers a connection attempt made from inside the app's own container can produce. Merging them would put a value that no Kubernetes controller can ever emit into a field documented as the cluster's own reason.
Every one of these is Burrow-authored. None is a driver message, and none can carry a value the user's environment supplied — which is what keeps a DSN out of an audit row and out of a deploy result.
const ( // ProbeMountPath is where the emptyDir carrying the probe is mounted in both containers. It is // deliberately an unlikely path: it lands in the USER's image, and a mount that shadowed a real // directory would break the check pod in a way that looks like the app's fault. ProbeMountPath = "/burrow-probe" // ProbeBinaryName is the filename the init container writes its own executable to. ProbeBinaryName = "burrowd" // ProbePath is the full path the check container executes. ProbePath = ProbeMountPath + "/" + ProbeBinaryName // ProbeVolumeName is the emptyDir's name in the pod spec. ProbeVolumeName = "burrow-probe" // ProbeInstallCommand is the init container's subcommand: burrowd copying its own executable into // the directory named by its argument. ProbeInstallCommand = "install-probe" // ProbeCheckCommand is the check container's subcommand: burrowd running the plan and printing // the results. ProbeCheckCommand = "check-dependencies" // ProbePlanEnv is the environment variable the check container reads its plan from. The plan is // non-secret by construction — it carries key NAMES and endpoints — which is what makes it safe // to put in a Job spec anything that can read Jobs in the namespace can see. ProbePlanEnv = "BURROW_CHECK_PLAN" // ProbeResultPrefix marks the single line of the probe's output that carries its results. The // probe runs in the user's image, so the engine reads the marked line rather than trusting the // whole stream: an entrypoint wrapper or a shell profile that printed a banner would otherwise // make a working check unparseable. ProbeResultPrefix = "burrow-dependency-check: " )
The probe's contract with the check pod. These are the two halves of ADR-0076's consequence that "Burrow needs a way to run a check inside the app's container, and the app's image may contain no shell, no psql, no curl" — which is exactly the minimal image users are told to build.
THE DECISION: an init container running BURROW'S OWN IMAGE copies the burrowd binary into an emptyDir, and the check container runs THE APP'S IMAGE with that emptyDir mounted, executing the copied binary. The check therefore runs with the app's filesystem, the app's service account, the app's namespace and network policy, the app's config, and — through envFrom — the app's Secret, while depending on nothing being present in the image at all.
The alternatives were both worse. REQUIRING THE IMAGE TO CARRY TOOLS fails on precisely the images this is most needed for: a scratch or distroless image has no shell to run and no client to run, so the check would be absent wherever an app is best packaged. RUNNING THE CHECK FROM BURROWD'S OWN POD proves the CONTROL PLANE can reach the database, not that the APP can — and the difference is exactly where misconfiguration lives (§4), since the app's DNS search path, its network policy and its credential are the things that go wrong.
The binary copied is burrowd itself rather than a purpose-built one, following the shape ADR-0063 §7's backup shipper already established: the same binary under a different subcommand, so there is no second image to publish, no second artifact to keep in step with a release, and no second implementation of the connection logic to drift from the one the control plane uses. The init container copies its own executable, so it needs no `cp` and no shell either.
const ( // ReadinessInitialDelaySeconds is zero on purpose. A readiness probe that has not passed yet // simply keeps the pod out of its Service, which costs nothing, so delaying the first attempt // would only delay traffic to a pod that was ready sooner. Slow starts are absorbed by the // Deployment's progress deadline, not by a guessed delay. ReadinessInitialDelaySeconds int32 = 0 // ReadinessPeriodSeconds is how often the check runs. ReadinessPeriodSeconds int32 = 10 // ReadinessTimeoutSeconds is generous rather than tight: a loaded pod can be slow to accept a // connection or serve a handler, and a timeout that fires on load pulls the pod out of service // exactly when the remaining replicas can least absorb it. ReadinessTimeoutSeconds int32 = 5 // ReadinessFailureThreshold is how many consecutive failures remove a SERVING pod from its // Service — six, so roughly a minute of continuous failure is required. A brief stall is not // a reason to stop serving. ReadinessFailureThreshold int32 = 6 // ReadinessSuccessThreshold is one: a single success restores the pod. Kubernetes requires this // to be 1 for a readiness probe in any case. ReadinessSuccessThreshold int32 = 1 )
The readiness probe's timing, chosen to fail toward DEPLOYED (ADR-0076 §6) rather than toward a tidy signal. These are deliberately not operator-tunable: ADR-0076 asks for no such knob, and ADR-0068 §2's test for what belongs on the limits mechanism is whether a human has a real reason to pick a different number — a probe cadence is an implementation detail of the probe, not a bound anyone is trying to enforce.
const ( // HealthSourceEndpoint is a probe that came from a declared endpoint. HealthSourceEndpoint = "endpoint" // HealthSourceExposure is §3's conservative default: a TCP check on the published port. HealthSourceExposure = "exposure" // HealthSourceNone is no probe at all — the app is not published and declared nothing. HealthSourceNone = "none" )
const ( // DeployKindDeploy is an ordinary deploy, explicit or unattended. DeployKindDeploy = "deploy" // DeployKindRollback is a rollback: the same question — did this settle and is it serving — // asked about an image that moved backwards (ADR-0072 §4). DeployKindRollback = "rollback" )
The values hookEnvKind takes, so a hook branches on a fixed string rather than on prose.
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: a private registry with no pull credentials is the common, human-fixable cause (ADR-0017). They are two members of the closed IssueReason set, which issues.go enumerates — ADR-0074 §2 widened it to the other blocking classes. Transient reasons like ContainerCreating or PodInitializing remain deliberately excluded, on the criterion issues.go now states for the whole vocabulary: they resolve on their own.
const ( // ReasonUnschedulable is the PodScheduled condition reason when no node can run the pod. Its // detail is the scheduler's own message, which names what could not be satisfied. ReasonUnschedulable = "Unschedulable" // unbound claim, no provisioner, or a volume pinned to a node the pod cannot run on. ReasonVolumeUnavailable = "VolumeUnavailable" // ReasonCrashLoopBackOff is the container waiting reason once the kubelet has backed off // restarting a container that keeps exiting. ReasonCrashLoopBackOff = "CrashLoopBackOff" // ReasonCreateContainerConfigError is the container waiting reason when the container's // configuration cannot be resolved — most often a ConfigMap or Secret key that is not there. ReasonCreateContainerConfigError = "CreateContainerConfigError" // ReasonOOMKilled is the terminated-state reason when the kernel killed the container for // exceeding its memory limit. ReasonOOMKilled = "OOMKilled" // ReasonProgressDeadlineExceeded is the Deployment Progressing-condition reason when a rollout // has not made progress within .spec.progressDeadlineSeconds. ReasonProgressDeadlineExceeded = "ProgressDeadlineExceeded" // ReasonDeadlineExceeded is the reason a Job ran out of time — the Job Failed-condition reason // when Kubernetes enforces activeDeadlineSeconds, and the reason Burrow's own Job waiters carry // when their client-side deadline expires (issue #352). It is the BACKSTOP member: a waiter // reaches it only when no pod reported anything blocking, so its detail carries what the waiter // did observe rather than a bare elapsed time. ReasonDeadlineExceeded = "DeadlineExceeded" )
The closed IssueReason set. An agent branches on these rather than parsing the prose, so the set is enumerated (IssueReasons) and validated (IsIssueReason) rather than left implicit in a switch — ADR-0074 §5: the most important consumer of this surface is an agent, not a person.
Each value is the raw Kubernetes reason string for its condition wherever Kubernetes has one, so the reason an agent sees is the reason it would find in `kubectl describe`. ReasonVolumeUnavailable is the one Burrow-coined member, because Kubernetes has no single reason string for "this pod cannot get its volume": the evidence arrives as a scheduler message, and splitting it out from ReasonUnschedulable is worth a coined name because the fix is a different one (a claim or a StorageClass, not capacity or a toleration).
const ( // IssueLogTailLines bounds how many lines of a crash-looping container's previous log an Issue // carries. Enough to see a stack trace's first frames or a startup error; not a log viewer. IssueLogTailLines = 20 // IssueLogTailBytes bounds the same tail in bytes, because a line count alone bounds nothing — // one line of minified output or a base64 blob can be megabytes. Both bounds apply. IssueLogTailBytes = 1200 )
const ( // ReasonWorkloadMissing is a registered app — one whose release history says a Deployment was // created — with no Deployment in the cluster: deleted by hand, evicted and never rescheduled, // or never created because a write failed. ReasonWorkloadMissing = "WorkloadMissing" // ReasonAddonNotRunning is an add-on the registry records as installed whose backing workload is // not available. ReasonAddonNotRunning = "AddonNotRunning" // ReasonBackupJobMissing is a backup row still `pending` whose Job no longer exists — the shape // a backup interrupted by a burrowd restart leaves behind, and one that is otherwise // indistinguishable from a backup still running. ReasonBackupJobMissing = "BackupJobMissing" // ReasonIngressMissing is a recorded exposure with no Ingress: the app is registered as reachable // at a host and nothing routes it. ReasonIngressMissing = "IngressMissing" // ReasonCertificateNotIssued is a recorded TLS exposure whose Ingress exists but whose // certificate has not been issued. It is separated from ReasonIngressMissing because the fix is // a different one — cert-manager, an issuer, or a DNS/HTTP-01 path — not a missing route. ReasonCertificateNotIssued = "CertificateNotIssued" )
The §6 reasons: what Burrow INTENDED to exist, compared with what the cluster has.
These are additions to ADR-0074 §2's IssueReason vocabulary, not a second vocabulary beside it. They are separate constants because they are a different KIND of fact: every IssueReason is a condition the cluster reported about an object that exists, and each of these is an ABSENCE, which no Kubernetes reason string names because no controller is there to emit one. That is exactly why §6 calls it the diagnosis kubectl structurally cannot make, and why the ledger has to be the thing that makes it: only the side that knows what was intended can see it.
They are deliberately NOT members of IssueReasons(): that set gates the live status surface, whose criterion is a blocking condition observed ON a pod (ADR-0074 §2). Widening it here would put a registry-versus-cluster verdict into a field documented as the cluster's own reason.
const ( LimitScopeEnvironment = "environment" LimitScopeCluster = "cluster" LimitScopeDefault = "default" )
The tier an effective limit value came from, reported next to it so a refusal can name where the line was drawn and `cluster config list` can show what is set versus inherited.
const ( // BackupReasonDumpFailed is the backup command itself failing, with nothing offered to the store. // For a logical dump that is pg_dump: the database refused the connection, the PVC ran out of // disk, the command errored. For a physical one it is the `Backup` object reaching `failed` — // pgBackRest would not take the base backup, or CloudNativePG rejected the request. A `Backup` // OBJECT failing and a backup failing to LEAVE THE CLUSTER are different facts (ADR-0063 §7), and // this is the first of them; the store reasons below are the second. BackupReasonDumpFailed = "DumpFailed" // BackupReasonStoreUnreachable is the destination not answering — DNS, TLS, connection refused, // or a 5xx — after every retry. This is the one ADR-0063 §7 says to retry, because a transient // network failure is the common case; reaching this reason means the retries were used up. On the // physical path it is also what `walArchivingFailing` means: the instance is producing // write-ahead log the repository is not accepting, so the archive the base backup depends on is // not arriving. BackupReasonStoreUnreachable = "StoreUnreachable" // BackupReasonStoreRejected is the destination answering, and saying no: a credential that is // wrong or has been revoked, a bucket that is gone, a write the policy forbids. It is NOT // retried — a 403 does not become a 200 by being asked again — so it fails on the first answer // and says so, rather than spending the retry budget proving it. BackupReasonStoreRejected = "StoreRejected" // BackupReasonObjectNotReadable is the destination accepting the write and then failing to serve // the object back, or serving it back at the wrong length. It is the reason that exists because // a 200 on a PUT is not the same fact as a durable, readable object, and a Backup row is only // allowed to claim the second one. BackupReasonObjectNotReadable = "ObjectNotReadable" // BackupReasonNotRecorded is the backup that reached the store but whose completion could not be // written to the registry. The bytes are safe and the row is not, which is the harmless // direction: an under-reported backup age is a false alarm, and a false assurance is not. BackupReasonNotRecorded = "NotRecorded" )
The closed BackupFailureReason set: why a backup did not reach its destination, in terms a caller can BRANCH on rather than parse (ADR-0074 §5). It is deliberately a separate vocabulary from ADR-0074 §2's IssueReason set, which describes why a POD is blocked; these describe what happened once the pod was running. Both sets travel on the same row — a backup whose Job never started records the IssueReason the waiter returned (issue #352), and one whose Job ran records one of these — so a reader gets the closed reason either way, and IsBackupFailureReason is how a caller tells which vocabulary it is holding.
NO SECRET VALUE ENTERS A REASON OR ITS DETAIL. The detail is Burrow-authored — an HTTP status, an attempt count, a byte count — and never the vendor's response body, which is the one place an access key id is known to be echoed back. The body goes to the Job's pod log, which is the operator's to read and is no wider an exposure than the credential the pod already mounts.
const ( // OutcomeSucceeded is a rollout that completed with no pod reporting a blocking condition. OutcomeSucceeded = "succeeded" // OutcomeFailed is every other verdict, always accompanied by a reason from the closed set. OutcomeFailed = "failed" )
The two values RolloutOutcome.Outcome renders, and therefore the two values a `post-deploy` hook ever sees in BURROW_DEPLOY_OUTCOME (ADR-0072 §4).
const AppDatabaseURLKey = "DATABASE_URL"
AppDatabaseURLKey is the environment variable Burrow writes an attached app's connection string into (ADR-0031). It is the KEY NAME and nothing else; the value lives only in the app's Kubernetes Secret. It is named here so the derivation and the attach path cannot drift.
const CNPGPostgresImage = "ghcr.io/cloudnative-pg/postgresql:17.10-minimal-trixie"
CNPGPostgresImage is the PostgreSQL operand image a Burrow-authored `Cluster` runs. Burrow ships no third-party bytes: this names an image the CLUSTER pulls from the publisher who built it.
Three things are deliberate about which image this is.
- It is CloudNativePG's own operand image rather than a stock `postgres` one. CNPG's instance manager runs as PID 1 inside it and the entrypoint is the operator's, so an arbitrary PostgreSQL image is not a substitution CNPG supports.
- It is the MINIMAL variant. CNPG's standard operand images bundle barman-cloud, which shells out to GPL-3.0 tooling; ADR-0066 §3 declines barman on exactly that ground, and its rejection of the WAL-G plugin ("a plugin's licence is not its image's licence") is the record saying this project names images and not just repositories. The minimal image carries PostgreSQL and the instance manager and no backup tooling at all — which is also the right base for §3's pgBackRest plugin, since a CNPG-I plugin ships its own sidecar rather than living in this image.
- It is PostgreSQL 17, the major version the add-on has always run, so adopting the operator is not also a major-version jump.
It is pinned to a patch release for the reason every other image in the catalog is: an install that happens twice should be the same install. It moves independently of the operator's own pin (kube.CNPGVersion) — the operator and the operand are separately released, and CNPG supports a range of operands per operator — so it is not derived from it.
const DefaultAutoDeployBackoff = time.Hour
DefaultAutoDeployBackoff is how long the watcher holds off after a tag's deploy fails (or after a registry rate-limits it with no Retry-After hint): a failed tag is not re-attempted before this, so a bad image cannot become a redeploy crash-loop (ADR-0052 §5). A strictly newer tag is always tried; the hold applies only to re-attempting the exact tag that failed.
const DefaultAutoDeployInterval = 5 * time.Minute
DefaultAutoDeployInterval is the conservative default poll cadence (ADR-0052 §7): ~5 minutes. The watcher is deliberately not a low-latency channel, because it need not be — the explicit deploy is always the immediate path — so a conservative interval protects registry rate limits without a latency complaint.
const DefaultAutoDeployLevel = AutoDeployOff
DefaultAutoDeployLevel is the level that applies to every app, new or already deployed, until an operator sets one: off, so auto-deploy is opt-in — the watcher never polls or moves an app that has not deliberately opted in (ADR-0058, revising ADR-0052 §2's on-by-default default). This keeps a pre-existing app from being silently polled the moment a cluster is upgraded to a version carrying the poller; set a level (patch/minor/major) to turn auto-deploy on.
const DefaultEnvironment = "prod"
DefaultEnvironment is the name of the environment install creates: `prod`, mapped to the app namespace burrowd runs against (BURROW_NAMESPACE). It is the environment an operation that names none resolves to, and — since a fresh install has exactly one — the only one a single-environment self-hoster ever sees (ADR-0067 §2–§3, superseding ADR-0035 phase 2's implicit `default`).
THE NAME IS A GUARDRAIL DECISION, NOT NAMING TASTE. ADR-0065 §3 makes app.delete and dns.delete deny-by-default and expects the operator to relax them per environment — allow in development, confirm in staging, deny in production. That gradient needs an environment whose name says what it is. An environment called `default` invites `guard set --env default app.delete allow` as the obvious way to make the friction stop, and the operator has then relaxed PRODUCTION without ever typing the word. `prod` makes the same command read as what it is. A self-hoster's single environment IS production: the absence of a staging environment does not make it not production (ADR-0067 §2).
The NAME and the NAMESPACE are separate values, which is what keeps an install predating this from migrating: it gains an environment named `prod` pointing at the namespace its apps are already in (`burrow-apps`), not at `burrow-apps-prod`, and nothing moves (ADR-0067 §3). Resource names follow the same rule one level down — AddonInstanceName gives the DEFAULT environment the unqualified name (`burrow-postgres`) by switching on THIS CONSTANT rather than on its value, so changing the value from `default` to `prod` renamed no instance, no volume, and no Secret.
It is reserved: `burrow env add prod` is rejected because install already created it.
const DefaultFailureLookback = 24 * time.Hour
DefaultFailureLookback is the window the coverage record is read over when a query names none. It bounds only the COVERAGE half of the answer: the failures themselves are the active ones, which have no age. A day is the span over which "was Burrow watching?" is a question with a useful answer — long enough to cover a night nobody was present for, short enough that the gap list is about the current incident rather than the month.
const DefaultLedgerPruneInterval = time.Hour
DefaultLedgerPruneInterval is how often retention is enforced. Pruning every sweep would spend a DELETE per minute to remove nothing; hourly keeps the table bounded without the cost.
const DefaultLedgerRetention = 30 * 24 * time.Hour
DefaultLedgerRetention is how long a RESOLVED failure and an elapsed observation window are kept. ADR-0074 §4 requires the bound and does not fix its size: a month is long enough to answer "has this happened before" across a release cycle, and short enough that the table cannot become the reason the control plane's own database fills up — which is an outage, not untidiness. An ACTIVE failure is never pruned, however old: a thing that is still broken is not history.
const DefaultObserveInterval = time.Minute
DefaultObserveInterval is how often the observer sweeps what Burrow manages. A minute is short enough that a crash loop is recorded while it is happening and long enough that the cost is a handful of list calls per minute against the API server, scaling with what Burrow manages rather than with usage (ADR-0074's consequences). Unlike the auto-deploy poller's cadence it needs no jitter: the thing being called is the cluster's own API server, not a third party with a rate limit, and there is one burrowd per cluster rather than a fleet that could fall into lockstep.
const DependencyChecksDisabledNote = "" /* 152-byte string literal not displayed */
DependencyChecksDisabledNote is what the surface says when the default is off for an app.
const DependencyFailureHint = "" /* 360-byte string literal not displayed */
DependencyFailureHint is the non-blocking note a deploy carries when a dependency check failed. It states the two things a reader needs and would otherwise guess at: that the deploy is live and was not undone, and that the failure is about a thing Burrow itself provisioned, so it is worth believing.
const LedgerDetailBytes = 400
LedgerDetailBytes bounds a ledger row's detail. A detail is one line of context beside a reason, not a report: the reason is what an agent branches on, and an unbounded detail would make the ledger a place log output accumulates.
const NoDependenciesNote = "" /* 208-byte string literal not displayed */
NoDependenciesNote is what the surface says when there is genuinely nothing to check.
const NoHealthEndpointHint = "" /* 561-byte string literal not displayed */
NoHealthEndpointHint is ADR-0076 §5's guidance, stated where an agent will meet it: on the health surface and on the result of a deploy that has no declared endpoint.
The last sentence is the one that earns its place. An agent asked to add /healthz will reach for the internet's most common example, which checks the database — the single worst thing to put in a readiness probe on this product, because the database is shared and one blip would then take every app out of service at once (§2). Saying so explicitly does not stop it happening every time, but leaving it unsaid guarantees it.
const PostgresBackupVolume = "burrow-postgres-backups"
PostgresBackupVolume is the DEFAULT environment's Postgres backup claim, and the only backup claim that existed before backups were per-environment: every dump taken before that change is on it. It is named here, next to BackupPath, so the engine can report that removing the add-on deliberately LEFT IT IN PLACE without importing the kube package — the same reason BackupPath lives in this package rather than in the Job builder.
It is a constant rather than a call to BackupVolumeName because it is also the value migration 00027 backfilled every pre-existing backup row's volume to, and a backfill is a historical fact about bytes on a disk: it must not move if the derivation is ever revised. BackupVolumeName agrees with it for the default environment, asserted by a test.
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).
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).
const StatusFailureLookback = DefaultFailureLookback
StatusFailureLookback is how far back an app's status reaches for its recent-failure history (ADR-0074 §8: "a short recent-failure history for that app"). It is the same span as the listing's default coverage window, so the two surfaces answer about the same period and a reader moving between them is not comparing a day against a month.
Variables ¶
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, and through it the `burrow` CLI and `burrow-agent`) can map them to the right status without parsing prose. They complement the typed GuardrailError (a deliberate policy refusal).
var ErrLifecycleUnknown = errors.New("bucket lifecycle configuration could not be read")
ErrLifecycleUnknown marks a bucket lifecycle configuration Burrow could not READ — the vendor does not implement the lifecycle API, or the credential is not permitted to read it. It is not a failure: the destination still works, and registration proceeds. What it changes is what Burrow is allowed to CLAIM. ADR-0063 §3 is explicit that an unverifiable invariant reported as verified is worse than one reported as unknown, so the reconciliation is reported as unknown, with the reason, and never as "checked".
Functions ¶
func AddonDataVolumeName ¶
AddonDataVolumeName is the PersistentVolumeClaim holding the data of add-on type t's instance named instance. It exists so a message about a removal names the volume that removal actually acts on (ADR-0064 §3) — "this destroys the data volume X" is only informed consent while X is the volume being destroyed.
Postgres names it differently from every other add-on, and neither name is a convention Burrow is free to pick. The claim Burrow creates for a Deployment-backed add-on (logs, metrics) is named after the instance; a Postgres instance is a CloudNativePG `Cluster`, which composes one claim per instance and calls it `<instance>-<serial>` — and the single-instance `Cluster` Burrow authors (ADR-0066 §1) therefore has exactly one, `<instance>-1`.
IT IS FOR SAYING, NOT FOR ACTING. Every path that DELETES or RETAINS a CloudNativePG claim finds it by the label the operator puts on it rather than by this derivation, because a constructed name that stopped matching would retain one claim out of a volume group and silently strand the rest. A prose name that is wrong costs a confusing sentence; an act aimed at the wrong name costs data.
func AddonInstanceName ¶
AddonInstanceName is the name of add-on type t's instance in environment env: the registry key (addons.name) and the resource name the instance's Deployment, Service, volume and — for Postgres — its superuser Secret carry in the cluster. It is the ONE place an environment is turned into an instance (ADR-0067 §1). Isolation comes from the instance rather than from a naming convention inside a shared server, so there is deliberately no name two environments can both resolve to: sharing one instance across environments is not merely discouraged, it is inexpressible (ADR-0067 §5).
The DEFAULT environment keeps the unqualified name (burrow-postgres), which is what lets an install predating environments carry on against the instance, the volume, and the superuser credential it already has — it gains an environment, and nothing moves (ADR-0067 §3). Every other environment is suffixed with its own name.
Note the switch is on the CONSTANT DefaultEnvironment, not on its value. That is load-bearing: ADR-0067 §2 renamed the default environment from `default` to `prod`, and because the unqualified case keys on the constant, `burrow-postgres` stayed `burrow-postgres` through the rename. What a user sees the environment called and what the instance is called are decoupled on purpose — a name is legibility, a resource name is live state, and only one of the two is safe to change.
env is REQUIRED: an empty value is an error, not a synonym for the default environment. "A signature that can omit it is a signature that will omit it" (ADR-0067 §1) — callers canonicalize an operation's environment first (envName), so an empty value here only ever arrives from a path that forgot, which is exactly the path that must not silently land on another environment's data.
func AppSecretName ¶
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 the agent control channel (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 BackupFailureReasons ¶
func BackupFailureReasons() []string
BackupFailureReasons returns every member of the closed set, so a caller (or a generated agent schema) can enumerate the vocabulary instead of hard-coding it.
func BackupObjectKey ¶
BackupObjectKey is the object-store key of app's dump for id in environment env, recorded on the backup row and handed to the Job that writes it (ADR-0063). It is derived here, next to BackupPath, so the engine and the Job builder cannot disagree about where a backup lives.
The layout is <prefix>/<env>/<app>/<id>.dump: the environment leads because it is the coarsest thing an operator or a lifecycle rule ever scopes to, and the id is unique on its own, so the path is legible at the vendor rather than being a flat bucket of opaque names. The prefix keeps Burrow's objects together in a bucket the operator may have pointed at rather than let Burrow create (ADR-0063 §4).
func BackupPath ¶
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 BackupVolumeName ¶
BackupVolumeName is the PersistentVolumeClaim holding add-on type t's dumps for environment env — ONE CLAIM PER ENVIRONMENT, the same shape ADR-0067 §1 gives the instance those dumps come from.
A dump is only ever taken from, and only ever restored into, one environment's instance. Sharing one claim across environments would have put staging's and production's dumps for an app of the same name on one disk, which the backup and restore Jobs of EITHER environment mount whole: the registry rows would say which environment each dump came from while nothing on the volume did. Isolation comes from the claim, not from a naming convention inside a shared one — the sentence ADR-0067 §1 uses about the instance, one level down.
THE NAMES CANNOT COLLIDE, and that is by construction rather than by convention:
- Across environments, because AddonInstanceName is already injective over (type, environment) and appending a fixed token preserves that. Two environments have no name they both resolve to.
- Against the INSTANCE names sharing the add-on namespace, because a named environment's claim is separated by a DOT, and an instance name can never contain one: an add-on type has no dot and an environment name is a DNS-1123 *label*. Without that, an environment called `staging-backups` would name its instance, its Deployment, its Service and its data claim exactly what `staging`'s backup claim is called — the same class of collision this fixes.
The DEFAULT environment keeps the unqualified name its claim already carries (PostgresBackupVolume), so no dump moves and no existing claim is renamed — the same exemption ADR-0067 §3 gives the default environment's instance, for the same reason. That is the one name in the family sitting inside the instance family's shape, which is why an environment called `backups` is reserved (ReservedEnvironmentNames).
env is REQUIRED, for AddonInstanceName's reason: a signature that can omit it is a signature that will omit it, and the value it would default to is another environment's dumps.
func BuildFitsState ¶ added in v0.13.0
func BuildFitsState(state ClusterResourceState) (fits bool, verdict string)
BuildFitsState reports whether a typical in-cluster build (buildRequestCPUMillis / buildRequestMemBytes — a quarter CPU and 512 MB) would currently schedule on the given resource state, and returns the short plain-language verdict to surface when it would not. It runs the same pure headroom math as ClusterCapacity, so the build pre-flight (issue #274) and the capacity report (issue #275) can never disagree. The kube build adapter calls it before creating a build Job to fail fast with an actionable message instead of leaving a Job stuck Pending.
func ContextWithClientVersion ¶ added in v0.10.0
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 DependencyReasons ¶
func DependencyReasons() []string
DependencyReasons returns the closed reason vocabulary a dependency-check result may carry.
func DiscrepancyReasons ¶
func DiscrepancyReasons() []string
DiscrepancyReasons returns every §6 intent-versus-cluster reason.
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), which is a property the guardrail declares (ADR-0068 §5). An unknown code is not scopable.
func EnvScopableLimit ¶
EnvScopableLimit reports whether a limit can be set for one named environment (ADR-0068 §5), which is a property the limit declares. An unknown code is not scopable.
func HumanCPU ¶ added in v0.13.0
HumanCPU renders a milli-CPU amount in plain language — "½ a CPU", "¼ of a CPU", "a tenth of a CPU", "2 CPUs" — never machine units like "500m" (issue #275/#277). Sub-CPU amounts snap to the nearest familiar fraction (a tenth, ¼, ⅓, ½, ⅔, ¾); amounts of one CPU or more show the whole count with a fractional glyph (e.g. "1½ CPUs"). Amounts that do not land on a named fraction are prefixed "about", so an arbitrary reading (e.g. a DOKS node's ~620m of system overhead) still reads as "about ⅔ of a CPU". This is the plain-language convention the README uses.
func HumanMemory ¶ added in v0.13.0
HumanMemory renders a byte count in MB or GB (issue #275/#277). It divides by the binary MiB/GiB (so a 512Mi request reads as "512 MB" and 2Gi as "2 GB", matching how the manifests and README state these) and trims a trailing ".0". Amounts below a gigabyte show as MB; a gigabyte or more as GB with up to one decimal.
func ImagePullIssue ¶ added in v0.8.0
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 the agent control channel (ADR-0017), so the message points at the user's terminal.
func IsBackupFailureReason ¶
IsBackupFailureReason reports whether reason is a member of the closed backup set — which is also how a caller tells a backup reason from an ADR-0074 §2 IssueReason arriving on the same field.
func IsDependencyReason ¶
IsDependencyReason reports whether reason is a member of the closed vocabulary. The engine checks it before a result reaches a caller or an audit row, so a reason nobody decided on — including one a future probe build invented — cannot escape into the surface an agent branches on.
func IsImagePullReason ¶ added in v0.8.0
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 IsIssueReason ¶
IsIssueReason reports whether reason is a member of the closed set. A reason outside it is not an Issue: that is the criterion above, enforced rather than merely documented, so a new Kubernetes waiting reason cannot leak into the surface without a decision being made about it here.
func IsLedgerReason ¶
IsLedgerReason reports whether reason is a member of the closed ledger vocabulary. The observer checks it before writing, so a reason nobody decided to record cannot reach a row by accident.
func IssueReasons ¶
func IssueReasons() []string
IssueReasons returns every member of the closed IssueReason set, so a caller (or a test, or a generated agent schema) can enumerate the vocabulary instead of hard-coding it.
func KnownGuardrail ¶
func KnownGuardrail(code GuardrailCode) bool
KnownGuardrail reports whether code names a configurable guardrail.
func KnownHookPhase ¶
KnownHookPhase reports whether p names a phase hooks may be set on today.
func KnownLimit ¶
KnownLimit reports whether code names a configurable operational limit.
func LedgerDetail ¶
LedgerDetail is the single gate every ledger detail passes through, and the enforcement point for ADR-0074 §9's rule that no secret value ever enters a ledger row.
It takes the FIRST LINE only. That is not cosmetic. The live status surface's crash-loop Issue carries a bounded tail of the application's PREVIOUS log after a newline (issues.go), and application output may contain anything at all — a connection string it printed on startup, a token in a stack frame. Bounded and labelled, that is an acceptable trade for a value read live and discarded; it is a different trade for a row that persists for the retention window in the control plane's own database. So the ledger keeps the part Burrow authored — the reason, the exit code, the limit, the missing key — and leaves the application's own output to `burrow app logs` and the live status surface, which still carry it.
func LedgerReasons ¶
func LedgerReasons() []string
LedgerReasons returns the closed vocabulary a ledger row may carry: ADR-0074 §2's IssueReason set as merged for the live status surface, plus §6's intent-versus-cluster reasons. It is enumerated rather than left implicit in a switch for the same reason IssueReasons() is — the ledger's most important consumer is an agent branching on the reason, not a person reading prose (ADR-0074 §5).
func MarshalProbePlan ¶
MarshalProbePlan renders a plan for the check container's environment.
func PgBackRestLabelFromManifestKey ¶
PgBackRestLabelFromManifestKey reads pgBackRest's backup LABEL back out of a manifest key PgBackRestManifestKey composed, and returns "" for anything that is not one.
It exists because a physical restore has to NAME the base backup it recovers, and the name the repository knows it by is pgBackRest's label rather than Burrow's backup id. The label is already on the row — inside the object key, which is recorded before the row is allowed to say completed — so this reads it back rather than adding a column that would have to be backfilled for every physical backup taken before restore existed. The key is one Burrow composed from the label itself, so the derivation is exact rather than a guess at somebody else's layout.
It is the INVERSE of PgBackRestManifestKey and lives beside it so the two cannot drift: a change to the layout that did not change this would leave a restore naming a backup the repository has never heard of, which fails at the one moment there is nothing to fall back on.
func PgBackRestManifestKey ¶
PgBackRestManifestKey is the object key of the backup manifest pgBackRest writes for the backup labelled label, in stanza, in the repository at repoPath.
It exists so a completed physical backup can be READ BACK before its row is allowed to say completed (ADR-0063 §7). CloudNativePG reporting a `Backup` as completed is the operator's word that pgBackRest returned zero; that the object store will serve the result back, at the key the repository says it is at, is a separate fact, and it is the one a restore depends on. The manifest is the object pgBackRest itself reads first when it restores that backup, so its absence is not a cosmetic discrepancy.
It takes the repository PATH rather than the environment, and that is load-bearing: the path is read off the instance's OWN `Stanza`, so the key is where that instance actually writes rather than where Burrow would have configured it to. An instance whose stanza says something else is a mismatch to refuse, not a backup to go looking for in the wrong place.
func PgBackRestRepoPath ¶
PgBackRestRepoPath is the pgBackRest repository path holding environment env's physical backups and archived write-ahead log (ADR-0066 §3). It is derived here, beside BackupObjectKey, so the engine and the `Stanza` the adapter writes cannot disagree about where an environment's repository is.
THE ENVIRONMENT IS IN THE PATH, and that is the isolation. Each environment has its own instance (ADR-0067 §1) and therefore its own pgBackRest stanza; two stanzas sharing a repository path would have each one's `create-stanza` looking at the other's backups, and a restore could reach an instance it was never taken from. One path per environment makes that unreachable rather than merely unlikely.
It is returned with the leading slash pgBackRest's `repo-path` expects. The object KEYS underneath it carry no leading slash, which is what PgBackRestManifestKey accounts for.
func RegistryHost ¶ added in v0.8.0
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".
func ReservedEnvironmentNames ¶
func ReservedEnvironmentNames() []string
ReservedEnvironmentNames returns every environment name this package refuses outright — `prod`, which install already created, the retired `default` it replaced (ADR-0067 §2), and `backups`, which would name its Postgres instance what the default environment's backup claim is already called (backupsEnvironment) — as a fresh slice a caller cannot mutate. It is the same table validateEnvironmentName refuses from, so the answer is the refusal itself and not a description of it that can fall behind.
IT IS EXPORTED FOR THE CALLER THAT REFUSES BEFORE IT FORWARDS. A front end that provisions with its own credentials and only then hands the request to the control plane has to make this refusal EARLIER than validateEnvironmentName does: `burrow env add` creates the environment's namespace and RBAC first (cmd/burrow/env.go), and an operator embedding the engine behind their own provisioning is in the same position. burrowd's refusal is the boundary, but it arrives after the namespace exists. Without an accessor the only way to spell the earlier check is a literal copy of this list, and a literal copy drifts silently the moment a name is added here: the new name passes the front end's stale check, the namespace and RBAC are created, burrowd then refuses, and the cluster is left holding state for a request that was never going to be accepted. Nothing errors where the state is created, so the leftover surfaces later, as an empty namespace nobody can account for.
The set is flat on purpose. `prod` is reserved because it exists and `default` because it must never come back, but both answers are "refuse", and a caller that needs to know which name is the default already has DefaultEnvironment. Exporting "retired but reserved" as its own category would publish a migration artifact as API and invite a branch on a distinction with no behavior behind it.
func ResolveAutoDeploy ¶ added in v0.13.0
func ResolveAutoDeploy(current string, available []string, level AutoDeployLevel) (target string, upgrade string)
ResolveAutoDeploy decides what an app on `current` should auto-deploy to, given the tags `available` in the registry and the auto-deploy `level`. It returns the tag to deploy (target, "" if none) and the highest version that exists ABOVE the level's cap (upgrade, "" if none) for surfacing as an available upgrade (ADR-0052 §2/§3). Upgrades only; only semver tags count.
The returned strings are the ORIGINAL tags as they appeared in `available` (e.g. "v1.2.7" stays "v1.2.7"), so the caller deploys the exact tag rather than a normalized form.
Non-semver tags in `available` (latest, a git SHA, a date) are ignored — they cannot be classified patch/minor/major, so the watcher never chases them (ADR-0052 §4). Prerelease versions (e.g. 1.3.0-rc1) are excluded: only stable releases are eligible for auto-deploy or for surfacing as an available upgrade. If `current` does not parse as semver there is no basis to compare, so the result is ("", "").
func ValidateHealthPath ¶
ValidateHealthPath checks a declared health path at the boundary, before it is stored.
It is the second half of §2's enforcement. ReadinessCheck cannot HOLD a host, and this refuses to accept one: "http://db:5432/health" does not begin with "/", and "//db/health" is a protocol-relative URL, so both are rejected here rather than silently turned into a probe that leaves the pod. The remaining rules keep a path a path — one line, bounded, no whitespace or control characters that would land in a Kubernetes object.
func ValidateHealthPort ¶
ValidateHealthPort checks a declared health port. Zero is allowed and means "the port the app is published on", resolved at apply time.
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"`
// Endpoint is the S3-compatible API endpoint. Object storage is addressed by endpoint rather
// than by vendor (ADR-0063 §1): the vendor is whoever answers it.
Endpoint string `json:"endpoint,omitempty"`
// Region is the region the S3 request is signed for.
Region string `json:"region,omitempty"`
// Bucket names an EXISTING bucket to use. Mutually exclusive with CreateBucket: Burrow either
// creates the bucket it will own or is pointed at one, and never infers a bucket by name
// (ADR-0063 §4).
Bucket string `json:"bucket,omitempty"`
// CreateBucket asks Burrow to create its own bucket, with a readable prefix and a random
// component, and record the name it created (ADR-0063 §4). It trips the bucket.create guardrail.
CreateBucket bool `json:"create_bucket,omitempty"`
// RetentionDays is how long backups written to this destination must stay restorable — the
// window the bucket's lifecycle rules are reconciled against (ADR-0063 §3). Zero declares no
// window, under which any age-expiring lifecycle rule conflicts, because nothing prunes
// Burrow's backups today.
RetentionDays int `json:"retention_days,omitempty"`
// AccessKeyID is one half of the object-storage credential VALUE. Like Token it travels only in
// this request body, is written to burrow-credentials after the destination is verified, and is
// never logged, stored in Postgres, or echoed back.
AccessKeyID string `json:"access_key_id,omitempty"`
// SecretAccessKey is the other half of the credential VALUE, held to exactly the same rules.
SecretAccessKey string `json:"secret_access_key,omitempty"`
// Confirm acknowledges the bucket.create guardrail so a bucket creation proceeds past it.
Confirm bool `json:"confirm,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 the agent control channel — provider add is a human/CLI operation, not an agent one.
type AddonInfo ¶
type AddonInfo struct {
Name string `json:"name"`
Type AddonType `json:"type"`
// Environment is the environment this instance serves — the canonical name, with the reserved
// "default" for the implicit one (ADR-0067 §1). Each environment gets its own instance, so this
// is what says which one this row is, and it is what an attach reads to decide which server to
// provision a database on. It is recorded rather than parsed back out of Name: the name is
// derived from the environment, never the other way round.
Environment string `json:"environment,omitempty"`
// 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 instance's backing workload is available. It is probed
// from the cluster at list time and never persisted in the registry.
Ready bool `json:"ready"`
// Warning is a non-blocking note about the operation that produced this row. It is NOT persisted
// — the registry holds what an add-on IS, and this is a fact about one install — so it is empty
// on every row read back from the registry and only ever set on the value an install returns.
//
// It exists for one situation and is deliberately not a refusal: an object-storage destination is
// registered, so the operator plainly wants backups, and the cluster has no pgBackRest plugin to
// take them with (ADR-0066 §3). Refusing the install there would take the DATABASE away to
// protect a backup, on a cluster where installing the plugin may not even be possible yet; and
// installing silently would hand somebody an instance they believe is archiving. So the instance
// is created without archiving and the omission is stated.
Warning string `json:"warning,omitempty"`
}
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 AddonRemoval ¶
type AddonRemoval struct {
// Namespace is the namespace the add-on's resources — and any retained volume — live in.
Namespace string `json:"namespace,omitempty"`
// DataDeleted reports whether the add-on's data volume was destroyed. It is true only when the
// caller explicitly asked for it AND the add-on had a volume to destroy.
DataDeleted bool `json:"data_deleted"`
// RetainedDataVolume names the PersistentVolumeClaim holding the add-on's data that was left in
// place. Empty when the add-on has no volume, or when the caller asked for it to be deleted.
RetainedDataVolume string `json:"retained_data_volume,omitempty"`
// RetainedBackupVolume names the backup PersistentVolumeClaim that was left in place — the
// Postgres add-on's dumps (ADR-0032). Backups deliberately outlive the database they came from,
// so this survives even a data-deleting removal; it is empty when no backup volume exists.
RetainedBackupVolume string `json:"retained_backup_volume,omitempty"`
}
AddonRemoval is what the cluster-side teardown of an add-on actually did: which namespace it acted in, whether it destroyed the add-on's data volume, and which volumes it deliberately left behind. Removal keeps the data volume unless the caller explicitly asks for it to go, so a removal that was meant as "stop this and reinstall it" cannot silently destroy every attached app's database (ADR-0025/0031); the retained names are reported so the caller can see what survived and reclaim it later.
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 ¶
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 PostgreSQL instance the agent attaches an app to — Burrow provisions a // database and login role per app inside it and writes the app's DATABASE_URL into the app's // per-environment Secret (ADR-0031). There is one instance PER ENVIRONMENT, not one per cluster: // the environment, not a naming convention inside a shared server, is what isolates one // environment's data from another's (ADR-0067 §1). The instance is a CloudNativePG `Cluster` and // nothing else — Burrow writes one custom resource and the operator composes the workload, the // volume and the services from it (ADR-0066 §1). AddonPostgres AddonType = "postgres" )
type AddonVolume ¶
type AddonVolume struct {
// Name is the claim name, which is also what `kubectl delete pvc` takes.
Name string `json:"name"`
// Namespace is the add-on namespace the claim lives in.
Namespace string `json:"namespace,omitempty"`
// Addon is the add-on type the claim was created for, read from the claim's own Burrow labels —
// not inferred from its name.
Addon AddonType `json:"addon"`
// Environment is the environment the claim serves, read from the claim's own Burrow labels.
// Empty for a claim created before add-ons were per-environment, which is the default
// environment's by construction — it is left empty rather than filled in, because the label is
// what the cluster actually says. With one instance and one backup claim per environment
// (ADR-0067 §1) it is the difference between a listing that says which claim is which and one
// that leaves the operator to read it off a name suffix.
Environment string `json:"environment,omitempty"`
// Role is what the claim holds: AddonVolumeData or AddonVolumeBackup.
Role string `json:"role"`
// Size is the claim's provisioned capacity where the cluster reports it, falling back to the
// requested size (e.g. "10Gi"). Empty when neither is known. Size, not cost: cost needs the
// provider's per-GiB price and can be wrong, and a wrong number about money is worse than an
// honest one about bytes (ADR-0064 §Deliberately left open).
Size string `json:"size,omitempty"`
// ReinstallAdopts reports whether reinstalling the add-on picks this claim back up with its data
// intact (ADR-0064 §1). True for a data claim — the reinstall lands on the same claim name — and
// false for a backup claim, which is read by the backup/restore Jobs rather than adopted.
ReinstallAdopts bool `json:"reinstall_adopts"`
// CreatedAt is the claim's creation time as the cluster records it.
CreatedAt time.Time `json:"created_at,omitempty"`
}
AddonVolume is one PersistentVolumeClaim in the add-on namespace that belongs to an add-on: which add-on it serves, what it holds, and how big it is. It is the unit `addon list` reports a RETAINED volume in — a claim an earlier removal deliberately left behind (ADR-0064 §6). Keeping data by default is only defensible while the leftovers are visible: an invisible claim is a silent bill, and a bill is a worse way to find out than a listing.
type AmbiguousEnvironmentError ¶ added in v0.11.0
type AmbiguousEnvironmentError struct {
// Environments are the registered environments the caller must choose among (`prod` first,
// then the ones added later in name order, as ListEnvironments returns them).
Environments []Environment
}
AmbiguousEnvironmentError reports that a mutating operation arrived with no environment named while more than one environment is registered, so burrowd refuses to pick one rather than silently defaulting to `prod`, the environment install created (ADR-0047 §1). It is a structured outcome, not a system failure: the request was understood, but its target is ambiguous — an unanswered question, not a held operation. It lists the registered environments (`prod` first, then the ones added later) with their namespaces so the caller re-issues the operation naming an explicit environment, rather than letting a state change land on the default by accident. The check is on registration, not reachability: ambiguity is a static fact about how many environments exist, with no network probe (ADR-0047 §1). Callers distinguish it with AsAmbiguousEnvironment; the HTTP API maps it to a 4xx with the machine-readable "ambiguous_environment" code.
func AsAmbiguousEnvironment ¶ added in v0.11.0
func AsAmbiguousEnvironment(err error) (*AmbiguousEnvironmentError, bool)
AsAmbiguousEnvironment reports whether err is (or wraps) an AmbiguousEnvironmentError and returns it, mirroring AsGuardrail and AsMissingPrerequisites so a front end (the HTTP API, and through it the `burrow` CLI and `burrow-agent`) can surface the structured refusal without parsing prose.
func (*AmbiguousEnvironmentError) Error ¶ added in v0.11.0
func (e *AmbiguousEnvironmentError) Error() string
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.
type AppDatabaseLister ¶
type AppDatabaseLister interface {
// ListAppDatabases returns the app names with a Burrow-provisioned database on environment env's
// instance, sorted. None yields an empty slice and no error. env is required for the same reason
// it is on DatabaseProvisioner: the answer to "who is attached?" is only true of one instance,
// and a removal names the apps it is about to affect (ADR-0067 §1).
ListAppDatabases(ctx context.Context, env string) ([]string, error)
}
AppDatabaseLister enumerates the apps that hold a Burrow-provisioned database on one environment's Postgres instance — the concrete answer to "who is attached?", and therefore to "whose data does removing this add-on destroy?". The engine reads it so a removal that would destroy the volume can name the affected apps in its confirmation message rather than warning generically (ADR-0006: a gate is only as good as the reason it hands back).
It is a SEPARATE optional interface rather than a method on DatabaseProvisioner, for the same reason MetricsRangeQuerier is separate from MetricsQuerier: an existing provisioner implementation stays valid unchanged. The engine type-asserts its provisioner to it and degrades — reporting no attached apps rather than failing — when it is absent or the instance cannot be reached. That degradation is deliberate: an add-on is often removed precisely because it is wedged, and a removal must not become impossible when the thing being removed will not answer.
type AppEnvRef ¶ added in v0.13.0
AppEnvRef names one (app, environment) pair — a unit the pull-based watcher reconciles (ADR-0052 Phase 4b). Env is the canonical environment name ("prod" for the default environment, ADR-0067 §2).
type ArchiveDestination ¶
type ArchiveDestination struct {
// Provider is the registry name of the object-storage provider. Not secret.
Provider string
// Config is the provider's non-secret configuration — endpoint, region, bucket, retention.
Config ObjectStoreConfig
// Credential is the pair pgBackRest signs its S3 requests with. Secret; see the type comment.
Credential ObjectStoreCredential
// RepoPath is the repository path within the bucket, from PgBackRestRepoPath — one per
// environment, so one environment's repository is not addressable from another's stanza.
RepoPath string
}
ArchiveDestination is everything one environment's pgBackRest repository needs: where the bucket is, the credential to reach it with, the repository path inside it, and how long a backup written there must stay restorable (ADR-0063 §3, ADR-0066 §3).
Like BackupDestination it carries a SECRET VALUE, so it is an internal in-memory shape only: no JSON tags, never returned over the control-plane API, never audited, never logged, never placed in an error. The engine assembles it from the provider row and the credential store at call time, and the adapter puts the pair into a Secret in the add-on namespace that the plugin's sidecar reads by reference — the pair never appears in the `Stanza`, in a container's environment, or in an argv.
It is separate from BackupDestination rather than an extra field on it because the two address different things. A BackupDestination names ONE OBJECT — the dump this backup is writing — and is resolved per backup. An ArchiveDestination names a REPOSITORY that outlives every individual backup in it, and is resolved when an instance is created, because that is when the write-ahead-log path has to exist.
func (ArchiveDestination) RetentionDays ¶
func (d ArchiveDestination) RetentionDays() int
RetentionDays is the window backups in this repository must stay restorable for, in days.
IT IS BURROW'S NUMBER, AND IT IS THE ONLY ONE. pgBackRest has its own retention and CloudNativePG has another; ADR-0063 §3 exists because two retention policies that disagree delete a backup somebody was relying on, and the resolution is not to reconcile them afterwards but to have one. The window declared on the provider — the same one the bucket's lifecycle rules are refused for contradicting — is written INTO the repository's retention, so pgBackRest expires against Burrow's window rather than beside it. Zero declares no window, and no retention is configured at all: an unbounded repository is a cost problem, and a repository that quietly expires what a lifecycle rule was reconciled against is a recovery problem.
type AttachResult ¶
type AttachResult struct {
App string `json:"app"`
Addon AddonType `json:"addon"`
// Environment is the environment whose instance the database was provisioned on (the reserved
// "default" for the implicit one). It is reported because it is the thing that decides WHICH
// database the app just got (ADR-0067 §1) — an attach result that named only the app would read
// identically for two environments holding entirely separate data.
Environment string `json:"environment,omitempty"`
// 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 (the `burrow` CLI or `burrow-agent`) 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 AutoDeployConfig ¶ added in v0.13.0
type AutoDeployConfig struct {
// Interval is the base poll cadence. Zero or negative applies DefaultAutoDeployInterval.
Interval time.Duration
// Backoff is how long a failed tag (or a hint-less rate-limit) is held off. Zero applies
// DefaultAutoDeployBackoff.
Backoff time.Duration
// Jitter returns the actual wait before the next cycle, given the base interval, so many
// pollers do not stampede the registry in lockstep (ADR-0052 §7). Nil applies a default of
// the base interval +/- up to 10%. It is injected so a test drives the cadence deterministically.
Jitter func(base time.Duration) time.Duration
// After returns a channel that fires after d — the seam the run loop waits on between cycles.
// Nil applies time.After. It is injected (rather than reading the wall clock) so a test drives
// the loop with no real sleeping and no ambient time (ADR-0010).
After func(d time.Duration) <-chan time.Time
}
AutoDeployConfig configures the pull-based watcher (ADR-0052 Phase 4b). The zero value is valid: every field falls back to a safe default, so cmd/burrowd can start the poller with an empty config, and a test can drive it deterministically by supplying Interval, Jitter, and After.
type AutoDeployLevel ¶ added in v0.13.0
type AutoDeployLevel string
AutoDeployLevel is how far the pull-based passive watcher may move an app on its own (ADR-0052): it caps auto-deploy to patch, minor, or major upgrades, or turns it off. The level is set per app and per environment; the watcher only ever moves an app forward, to the highest semver version within the level's cap that is greater than the running release.
const ( // AutoDeployOff disables auto-deploy: only an explicit CLI or agent deploy ships a release // (ADR-0052 §2). Setting off is also how a version is pinned. It is the default: auto-deploy // is opt-in, so an app is never polled until an operator sets a level (ADR-0058). AutoDeployOff AutoDeployLevel = "off" // AutoDeployPatch auto-deploys patches within the current minor only (e.g. 1.2.6, 1.2.7 for // an app on 1.2.5); crossing to a new minor is a manual step (ADR-0052 §2). AutoDeployPatch AutoDeployLevel = "patch" // AutoDeployMinor auto-deploys any patch or minor upgrade within the current major (e.g. // 1.2.6, 1.3.0, 1.4.0 for an app on 1.2.5), never a major (ADR-0052 §2). AutoDeployMinor AutoDeployLevel = "minor" // AutoDeployMajor auto-deploys anything newer, including a breaking major (e.g. 2.0.0) — // fully hands-off updates for the operator who accepts the risk (ADR-0052 §2). AutoDeployMajor AutoDeployLevel = "major" )
func ParseAutoDeployLevel ¶ added in v0.13.0
func ParseAutoDeployLevel(s string) (AutoDeployLevel, error)
ParseAutoDeployLevel parses an auto-deploy level from a CLI argument, rejecting unknown values with a message that lists the valid ones (ADR-0052 §6). It is case-insensitive and tolerates surrounding whitespace.
func (AutoDeployLevel) Valid ¶ added in v0.13.0
func (l AutoDeployLevel) Valid() bool
Valid reports whether l is a known auto-deploy level.
type AutoDeployPoller ¶ added in v0.13.0
type AutoDeployPoller struct {
// contains filtered or unexported fields
}
AutoDeployPoller is the pull-based passive-deploy watcher (ADR-0052): on a bounded, jittered cadence it lists each app's image tags and, when a newer tag within the app's auto-deploy level exists, drives the SAME guarded deploy an explicit call would — same rollout, deploy record, rollback handle, and audit entry, distinguished only by its auto provenance. It is outbound-only (it never accepts an inbound connection) and fail-soft: one app's registry error or deploy failure is logged and never stops the loop or affects another app (ADR-0040). It reads no ambient time or randomness — the cadence comes from the injected After/Jitter seams and every backoff deadline from the engine's injected clock (ADR-0010).
func (*AutoDeployPoller) Run ¶ added in v0.13.0
func (p *AutoDeployPoller) Run(ctx context.Context)
Run polls until ctx is cancelled, reconciling every candidate (app, env) on each cycle. It runs a reconcile pass immediately, then waits a jittered interval before the next, honoring cancellation promptly (ADR-0052 §7). With no registry seam wired it logs and returns rather than spinning — the watcher is optional and degrades to off when the outbound registry client is absent (ADR-0040).
type AutoDeployStatus ¶ added in v0.13.0
type AutoDeployStatus struct {
App string `json:"app"`
Env string `json:"env"`
Level AutoDeployLevel `json:"level"`
Repository string `json:"repository,omitempty"` // current image reference with the tag stripped (e.g. "ghcr.io/user/app"), for building the deploy hint
Current string `json:"current,omitempty"` // current running semver tag, when known
Target string `json:"target,omitempty"` // tag auto-deploy would move to within the level, "" if none / already current
Upgrade string `json:"upgrade,omitempty"` // highest version above the level's cap, surfaced as an available upgrade, "" if none
Checked bool `json:"checked"` // whether the registry upgrade check actually ran
Note string `json:"note,omitempty"` // when Checked is false, a short human reason
// DisabledReason is why auto-deploy is off, when it was turned off by the safety stop rather than
// by a human (ADR-0052 §5): "disabled by rollback" or "disabled by downgrade". Empty when the
// level was human-set or is not off.
DisabledReason string `json:"disabled_reason,omitempty"`
}
AutoDeployStatus is the enriched, read-only view of an app's auto-deploy configuration in one environment (ADR-0052 §2/§3): the level plus, when the registry could be listed, the current running version, the tag auto-deploy would move to within the level, and the highest version above the level's cap surfaced as an available upgrade. When the upgrade check could not run — the registry is unreachable or needs credentials, the current tag is not semver, there is no running release, or the check is not wired — Checked is false and Note carries a short human reason; the level is still reported. The check is deliberately best-effort: a registry failure never errors the show, keeping this path independent of registry reachability (ADR-0040).
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, an operational limit an operator sets (ADR-0068 §6).
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"`
// Kind says which mechanism produced this row: a per-app logical dump, or a physical base backup
// of the whole instance (ADR-0066 §4). Empty on a row written before the two coexisted, which is
// logical by construction — nothing else existed to write it.
Kind BackupKind `json:"kind,omitempty"`
// App is the application whose database was dumped. EMPTY on a physical row: that backup covers
// every database on the instance, and naming one of them would make a listing claim a per-app
// backup exists where none does.
App string `json:"app"`
// Environment is the environment whose instance the dump was taken from ("prod" for the default
// one). Each environment has its own Postgres instance (ADR-0067 §1), so a dump
// is only meaningful against the one it came from: restore requires the two to agree rather than
// letting one environment's contents be written over another's.
Environment string `json:"environment,omitempty"`
// CreatedAt is when the backup was recorded, read from the injected clock.
CreatedAt time.Time `json:"created_at"`
// Path is the location of the dump WITHIN its claim (e.g. /backups/<app>/<id>.dump). It is a
// path on a volume burrowd does not mount — never a credential. It does not identify a dump on
// its own: the same path exists on every environment's claim, and Volume says which one.
Path string `json:"path,omitempty"`
// Volume is the PersistentVolumeClaim the dump was written to — this environment's backup claim
// (BackupVolumeName). It is recorded rather than derived on read because the derivation changed:
// backups written before backups were per-environment are all on the one claim that existed then
// (PostgresBackupVolume), and a row that only said which environment it came from could not tell
// the two eras apart. Recording it is what lets a restore refuse a dump that is not on the claim
// this environment mounts, instead of running a Job that finds no such file.
//
// Empty only on a row written by an older burrowd against a newer schema; readers treat that as
// the pre-migration claim, which is what it can only be.
Volume string `json:"volume,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"`
// Destination is where this backup's bytes ended up. It is written when the row is created, from
// what was registered AT THAT MOMENT, so a listing says what each backup actually did rather
// than what the current configuration would do — registering a destination today does not make
// last month's in-cluster dumps durable, and the rows must not imply that it did.
Destination BackupDestinationKind `json:"destination,omitempty"`
// Provider is the name of the object-storage provider this backup was written to, empty for an
// in-cluster one. It is a registry name, never a credential or an endpoint secret.
Provider string `json:"provider,omitempty"`
// ObjectKey is the key the dump occupies in the provider's bucket, empty for an in-cluster
// backup. It is the address a restore reads from and the thing an operator checks at the vendor;
// the bucket and endpoint it hangs off live on the provider row, not repeated here.
ObjectKey string `json:"object_key,omitempty"`
// FailureReason is why a failed backup failed — a member of the closed BackupFailureReason set,
// or of ADR-0074 §2's IssueReason set when the Job never started. Empty on a pending or
// completed row.
FailureReason string `json:"failure_reason,omitempty"`
// FailureDetail is one Burrow-authored line elaborating the reason, safe to print: an HTTP
// status, an attempt count, a length mismatch. Never a vendor response body and never a
// credential.
FailureDetail string `json:"failure_detail,omitempty"`
}
Backup is one recorded per-app database backup (ADR-0032): the control-plane index row for a logical dump written to the backup PVC and, when one is registered, on to an object store (ADR-0063). It names the app, the on-PVC path, the destination, 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.
func (Backup) Durable ¶
Durable reports whether this row is a backup that SURVIVES LOSING THE CLUSTER: it completed, and it completed at an object-store destination.
Both halves are required and neither is redundant. A row only says `completed` for an object-store destination once the object was written AND read back (ADR-0063 §7), so the pair is the strongest fact Burrow holds about a backup — while a `cluster` destination says `completed` on a Job exiting zero, and its bytes are on a volume in the same failure domain as the database they came from. Checking the destination as well as the status also covers the row written before a provider was deregistered, which is `completed` for a dump that never left the cluster.
It is a method rather than a comparison written out at each call site because two decisions stand on it and they must not drift: ADR-0064 §5 refuses to destroy a data volume unless the final backup is durable, and the backup-age signal of ADR-0063 §7 / ADR-0066 §5 reports the age of the last durable success. If those two ever disagreed, `--delete-data` would be refusing on one definition of "safe" while the status surface reported another.
type BackupDestination ¶
type BackupDestination struct {
// Provider is the registry name of the object-storage provider. It is not secret, and it is what
// gets recorded on the Backup row.
Provider string
// Config is the provider's non-secret configuration — endpoint, region, bucket.
Config ObjectStoreConfig
// Credential is the pair the S3 request is signed with. Secret; see the type's own comment.
Credential ObjectStoreCredential
// Key is the object key this backup occupies, from BackupObjectKey.
Key string
}
BackupDestination is everything the in-cluster backup Job needs to write one dump to the object store: which provider it is, where the bucket is, and the credential to sign with (ADR-0063 §7).
It carries a SECRET VALUE and is therefore an internal, in-memory shape only: it has no JSON tags and is never returned over the control-plane API, never audited, never logged, and never placed in an error. The engine assembles it at call time from the provider row and the credential store, hands it to the Kubernetes seam, and the adapter puts the pair into a Job-owned Secret the pod mounts — the same shape ADR-0057 §4 established for a build's source token, and the same reason: a credential in a Job's env or command line is visible to anything that can read the Job.
type BackupDestinationHealth ¶
type BackupDestinationHealth struct {
// Provider is the registry name of the object-storage provider.
Provider string `json:"provider"`
// Endpoint is the S3-compatible API endpoint that answers for it. Non-secret configuration.
Endpoint string `json:"endpoint,omitempty"`
// Bucket is the bucket Burrow writes backups to.
Bucket string `json:"bucket,omitempty"`
// Reachable reports whether the bucket answered for this credential just now.
Reachable bool `json:"reachable"`
// Detail says what was observed, in one Burrow-authored line. It never carries a vendor response
// body and never carries a credential: an unreachable destination is described by what Burrow
// could not do, not by quoting what the vendor said.
Detail string `json:"detail,omitempty"`
}
BackupDestinationHealth is one registered object-storage destination, probed AT THE MOMENT OF THE CALL. It is not persisted and not cached: a stored reachability verdict goes stale while continuing to read as current, which is the failure this surface exists to avoid rather than reproduce (the same reasoning that keeps ProviderVerification unpersisted).
type BackupDestinationKind ¶
type BackupDestinationKind string
BackupDestinationKind names where a backup's bytes ended up, so a listing can distinguish a dump that survives losing the cluster from one that does not (ADR-0063).
The two are a TIER, not alternatives. The PVC is where pg_dump writes and where pg_restore reads: a same-cluster staging copy that keeps the single-app logical restore ADR-0066 §4 deliberately retains both cheap and available when the vendor is not. The object store is the durable destination that takes the backup out of the database's failure domain, which is the entire point of ADR-0063. Recording which one a row reached is what stops "we have backups" from meaning two different things on the same listing.
const ( // BackupDestinationCluster is a backup that only ever reached the in-cluster PVC, because no // object-storage provider is registered. It is recorded rather than left blank: a backup sharing // a failure domain with the database it came from is a fact an operator should be able to read // off the row, not one they have to infer from an absence. BackupDestinationCluster BackupDestinationKind = "cluster" // BackupDestinationObjectStore is a backup that reached the registered object store. On a // completed row it means the object was written AND read back at ObjectKey. BackupDestinationObjectStore BackupDestinationKind = "object-store" )
type BackupHealth ¶
type BackupHealth struct {
// Addon is the add-on type the health is reported for. Only postgres has backups today.
Addon AddonType `json:"addon"`
// App is the app the report was narrowed to, empty when it spans every app.
App string `json:"app,omitempty"`
// Environment is the environment the report was narrowed to, empty when it spans every one.
Environment string `json:"environment,omitempty"`
// ObservedAt is when this report was assembled, from the injected clock. Every age below is
// measured against it, so the numbers in one report are consistent with each other.
ObservedAt time.Time `json:"observed_at"`
// State says what kind of backup coverage exists.
State BackupHealthState `json:"state"`
// LastSuccess is the newest COMPLETED backup, wherever its bytes went. Nil when none exists.
LastSuccess *BackupObservation `json:"last_success,omitempty"`
// LastDurableSuccess is the newest backup that completed at an object-store destination — the
// one ADR-0063 §7's age is about. Nil when no backup has left the cluster. It is the same row as
// LastSuccess whenever the newest success was durable.
LastDurableSuccess *BackupObservation `json:"last_durable_success,omitempty"`
// LastFailure is the newest FAILED backup, with the closed reason it recorded. Nil when none
// exists. It is reported alongside the successes rather than instead of them: a recent failure
// after an older success is a different situation from never having succeeded, and an operator
// deciding whether to act needs both.
LastFailure *BackupObservation `json:"last_failure,omitempty"`
// Pending counts rows still recorded as pending in this scope. A pending row is NEVER read as a
// success — the ages above count completed rows only — so a burrowd that died mid-Job leaves the
// age growing rather than resetting it. The count is surfaced because a growing pile of pending
// rows is itself a symptom.
Pending int `json:"pending,omitempty"`
// Destinations are the registered object-storage destinations and whether each answered just
// now, sorted by provider name. Empty when none is registered, which is not an error: an install
// with no object storage still takes in-cluster dumps, and the state says so.
Destinations []BackupDestinationHealth `json:"destinations,omitempty"`
// Summary is one line a human can read or an agent can relay, stating the fact rather than a
// verdict against a threshold Burrow has not been given.
Summary string `json:"summary"`
}
BackupHealth is the answer to "are my backups working?" for one add-on, optionally narrowed to one app or one environment. It carries names, times, sizes and reasons — never a credential.
type BackupHealthState ¶
type BackupHealthState string
BackupHealthState is the shape of what Burrow has observed about a scope's backups. It is deliberately a statement of FACT rather than a verdict against a threshold: "durable" does not mean "recent enough", it means a backup is known to exist outside the cluster, and the age alongside it is what says how long ago.
const ( // BackupHealthNever means no backup has ever completed in this scope. It is distinct from a // stale one: nothing has been lost yet, and nothing has ever been preserved either. BackupHealthNever BackupHealthState = "never" // BackupHealthClusterOnly means backups have completed but none of them left the cluster, so // every copy shares a failure domain with the database it came from. It is reported as its own // state rather than folded into "never" because the dumps genuinely exist and genuinely restore // an app whose data is wrong — they just do not survive losing the cluster. BackupHealthClusterOnly BackupHealthState = "cluster-only" // BackupHealthDurable means at least one backup completed at an object-store destination, which // ADR-0063 §7 only permits once the object was written and read back. BackupHealthDurable BackupHealthState = "durable" )
type BackupJobOutcome ¶
BackupJobOutcome is what the backup Job reported: the dump's size, and on a failure the closed reason and a Burrow-authored detail (ADR-0063 §7, ADR-0074 §5).
It exists so a failed backup is recorded with WHY rather than with a wrapped error string a caller would have to parse. Reason is a member of the closed BackupFailureReason set, or of ADR-0074 §2's IssueReason set when the Job never started at all. Detail never carries a vendor response body and never carries a credential.
type BackupKind ¶
type BackupKind string
BackupKind says which of the two backup mechanisms produced a row (ADR-0066 §4). They are not interchangeable and the row has to say which it is, because the two answer different questions and a reader under pressure reaches for whichever one the listing makes look applicable.
logical — one app's database, as it was at the moment of the dump. `pg_dump -Fc` through a Job
Burrow runs, restorable into one app without touching any other (ADR-0032).
physical — the whole instance, and every database on it, as a base backup CloudNativePG asked
pgBackRest for. It is the only kind with a write-ahead-log window behind it, and it
is the only kind that cannot be restored per app.
const ( // BackupKindLogical is a per-app `pg_dump`. It is the default a row carries when nothing says // otherwise, which is what every backup recorded before physical backups existed is. BackupKindLogical BackupKind = "logical" // BackupKindPhysical is a CloudNativePG `Backup` object over the whole instance. Its App is // empty — an instance-wide backup belongs to no single app, and attributing it to one would make // a listing claim a coverage that app does not have. BackupKindPhysical BackupKind = "physical" )
func (BackupKind) Valid ¶
func (k BackupKind) Valid() bool
Valid reports whether k is a known BackupKind.
type BackupObservation ¶
type BackupObservation struct {
// ID is the backup identifier.
ID string `json:"id"`
// Kind is which mechanism took it — a per-app logical dump or a physical base backup of the whole
// instance (ADR-0066 §4). It is reported because the two are not interchangeable: a durable
// physical backup means every database on that instance is recoverable, and a durable logical one
// means exactly one app's is.
Kind BackupKind `json:"kind,omitempty"`
// App is the application whose database was dumped. Empty on a physical backup, which covers
// every database on the instance and belongs to no one app.
App string `json:"app"`
// Environment is the environment whose instance it was taken from.
Environment string `json:"environment,omitempty"`
// At is when the backup was recorded.
At time.Time `json:"at"`
// AgeSeconds is how long ago that was, measured against the injected clock at the moment of the
// call. It is reported in seconds rather than as a rendered duration so a caller — an agent, an
// alert rule — compares a number instead of parsing prose.
AgeSeconds int64 `json:"age_seconds"`
// Status is the row's lifecycle state.
Status BackupStatus `json:"status"`
// Destination is where the bytes ended up: the cluster volume, or the object store.
Destination BackupDestinationKind `json:"destination,omitempty"`
// Provider names the object-storage provider, for an object-store destination. A registry name,
// never a credential or an endpoint secret.
Provider string `json:"provider,omitempty"`
// SizeBytes is the dump's size, or 0 when unknown.
SizeBytes int64 `json:"size_bytes,omitempty"`
// Reason is why a failed backup failed — a member of the closed BackupFailureReason set, or of
// ADR-0074 §2's IssueReason set when the Job never started. Empty on a completed row.
Reason string `json:"reason,omitempty"`
// Detail is one Burrow-authored line elaborating the reason. Never a vendor response body and
// never a credential.
Detail string `json:"detail,omitempty"`
}
BackupObservation is one `Backup` row as the health surface reports it: what it was, when, and how long ago. It carries the app, environment, destination, provider and size — all names and numbers, never a credential — and on a failure the closed reason and the Burrow-authored detail.
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 destination, 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.
Completed means THE BYTES REACHED THE BACKUP'S DESTINATION, and it means nothing weaker (ADR-0063 §7). Where the destination is an object store, the Job has written the object and read it back before this row is allowed to say completed: a row claiming success for bytes that never left the cluster is worse than no row at all, because it converts a missing backup into a false assurance, discovered at restore time by somebody who cannot restore a database.
const ( // BackupPending is a backup whose Job has been started but has not yet completed. It covers the // whole of the dump AND the retried write to the destination: a retry in flight is a backup // still in progress, not a failure and certainly not a success. Nothing reads pending as // successful — the age of the last SUCCESSFUL backup counts completed rows only — so a row left // pending by a burrowd that died mid-Job keeps the backup age growing, which is the safe // direction for it to fail in. BackupPending BackupStatus = "pending" // BackupCompleted is a backup that reached its destination: the dump is on the PVC and, when an // object-storage destination is configured, in the store and verified readable there. BackupCompleted BackupStatus = "completed" // BackupFailed is a backup that did not reach its destination. FailureReason says why in // machine-readable terms. BackupFailed BackupStatus = "failed" )
func (BackupStatus) Valid ¶
func (s BackupStatus) Valid() bool
Valid reports whether s is a known BackupStatus.
type BuildRequest ¶ added in v0.13.0
type BuildRequest struct {
App string `json:"app"`
// Env is the environment to deploy the built image into (ADR-0035): empty or "prod" targets the
// default environment, a name added later targets that environment. An unregistered name is an
// error, surfaced by the deploy the build hands off to.
Env string `json:"env,omitempty"`
// Source is the git reference the builder clones and checks out inside the cluster.
Source SourceRef `json:"source"`
// TargetImage is the pullable repo:tag reference the built image is pushed to; the resulting deploy
// pins the digest the builder returns. In this phase it is supplied explicitly; the optional
// in-cluster registry as the zero-config default push target (ADR-0053 §5) is a later phase.
TargetImage string `json:"target_image"`
// Confirm acknowledges the app.deploy guardrail whose disposition is confirm, letting the deploy
// the build hands off to proceed past it (ADR-0020). It has no effect on a guardrail set to deny.
Confirm bool `json:"confirm,omitempty"`
}
BuildRequest is the code-free description of an in-cluster build-then-deploy (ADR-0053): the app, the git source to clone and build inside the cluster, and the target image reference the built image is pushed to. No code travels here (ADR-0004) — only the git reference and the target reference; the builder clones the source from git inside the cluster. On success the built image rejoins the existing guarded deploy path (ADR-0053 §4), so a build is a front-end that ends where deploy begins.
type BuildResult ¶ added in v0.13.0
type BuildResult struct {
// Digest is the content digest of the built image (e.g. "sha256:..."), the immutable identity the
// deployed release pins.
Digest string `json:"digest"`
// Deploy is the guarded deploy the built image flowed into (ADR-0053 §4).
Deploy DeployResult `json:"deploy"`
}
BuildResult reports the outcome of a successful build-then-deploy (ADR-0053 §4): the digest of the image the builder produced and the deploy that shipped it. Because the build ends where deploy begins, Deploy carries the same new release, rollback handle, and hints an explicit deploy returns — downstream cannot tell a built image apart from an externally-built one except in provenance.
type Builder ¶ added in v0.13.0
type Builder interface {
// Build clones source inside the cluster, builds an image, pushes it to targetImage (a pullable
// repo:tag reference), and returns the resulting image content digest (e.g. "sha256:..."). Only
// the git reference and the target reference cross into the builder — never source bytes; the
// builder clones the actual code from git inside the cluster (ADR-0004/0053 §3). It returns an
// error on any clone, build, or push failure; the caller surfaces that structurally and does NOT
// touch the deploy path. On success the returned digest is the immutable identity the resulting
// guarded deploy pins (ADR-0053 §4).
//
// insecure marks the push target as a plain-HTTP registry the push must not verify TLS against —
// set only for the in-cluster registry, which serves plain HTTP in-cluster (ADR-0054 §5). The
// engine is the single place that knows this: it sets insecure when it defaults the target to the
// in-cluster registry, and leaves it false for a caller-supplied external target, which is pushed
// over TLS. The base-image pull during the build always uses TLS regardless — insecure applies
// only to the push to targetImage.
//
// cred is the resolved source-provider credential (ADR-0057). When it carries a token the builder
// authenticates the clone to a PRIVATE git source and the buildah push/pull to the provider's
// registry with it, by mounting it into the build Job — never as a Job env var or command-line
// argument. The zero value (IsZero) is the public-source, credential-free path. The token is a
// secret: an implementation must not log it, echo it, or place it in an error.
Build(ctx context.Context, source SourceRef, targetImage string, insecure bool, cred SourceCredential) (digest string, err error)
}
Builder builds a container image from a git source reference inside the user's own cluster and pushes it to a target registry (ADR-0053). It is a seam — a real adapter (a Kubernetes build Job) and a fake — like every other Burrow dependency that touches the cluster, the registry, the clock, or the database. The interface is deliberately MINIMAL (ADR-0053 §6): it takes a source reference and a target image reference and returns the resulting image digest or an error — nothing more. Isolation and sandboxing are expressed INSIDE an implementation, never as interface knobs, so the separate commercial multi-tenant product can supply a hardened, sandboxed executor behind this same seam without the OSS interface having to anticipate its needs (ADR-0053 §6/§7). Building is code execution; in the single-tenant OSS path the user owns both the cluster and the source, so no sandbox is required (ADR-0053 §7). It is an OPTIONAL seam: nil is allowed, and the build path errors cleanly (ErrNotImplemented) when it is not wired.
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" // CapabilitySource is authenticating to a source provider (GitHub, GitLab, …): the in-cluster // build uses the provider's token to clone a PRIVATE git source and — where the same provider // hosts a registry — to authenticate the build's image push/pull (ADR-0057). One provider token // covers both, so a single credential serves the clone and the provider registry. CapabilitySource Capability = "source" // CapabilityObjectStorage is durable storage OUTSIDE the cluster, reached over the S3-compatible // API (ADR-0063). It exists so a backup can leave the failure domain of the database it came // from; it is a backup DESTINATION and not an object-storage product, and ADR-0063 §2 fixes that // scope deliberately — no browser, no cp/sync/ls of arbitrary prefixes, no policy or IAM surface. CapabilityObjectStorage Capability = "object-storage" )
type CapacityProber ¶ added in v0.13.0
type CapacityProber interface {
// ReadResourceState reads node allocatable and pod requests read-only. It never writes.
ReadResourceState(ctx context.Context) (ClusterResourceState, error)
}
CapacityProber reads the cluster's scheduling-capacity facts read-only over the Kubernetes API: each node's allocatable and every pod's summed requests, with no metrics-server required (issue #275). It is the seam over those reads so the engine's headroom/verdict logic stays pure and unit-testable against a fake. The production adapter (controlplane/kube) wraps a client-go clientset. It is an OPTIONAL seam — nil is allowed, and a capacity read errors cleanly (ErrNotImplemented) when it is not wired. It never writes.
type CapacityReport ¶ added in v0.13.0
type CapacityReport struct {
// Nodes is the per-node breakdown, one entry per schedulable node.
Nodes []NodeCapacity `json:"nodes"`
// Cluster is the cluster-wide total (its Name is empty).
Cluster NodeCapacity `json:"cluster"`
// TopCPU lists the pods requesting the most CPU, most first; TopMemory the most memory.
TopCPU []Consumer `json:"top_cpu"`
TopMemory []Consumer `json:"top_memory"`
// BuildCPUMillis / BuildMemBytes are the resource request the verdict pre-flights (a typical
// in-cluster build: a quarter of a CPU, 512 MB).
BuildCPUMillis int64 `json:"build_cpu_millis"`
BuildMemBytes int64 `json:"build_mem_bytes"`
// BuildFits is true when at least one node has enough free CPU AND memory at once to schedule
// that build. A build pod lands on a single node, so headroom split across nodes does not count.
BuildFits bool `json:"build_fits"`
// BuildFitsNode names the node the build would schedule on when BuildFits is true.
BuildFitsNode string `json:"build_fits_node,omitempty"`
// Verdict is the short, plain-language recommendation (plain CPU units, memory in MB/GB).
Verdict string `json:"verdict"`
// UtilizationNote records that this is scheduling headroom only and what metrics-server would add.
UtilizationNote string `json:"utilization_note"`
}
CapacityReport answers "is my cluster at capacity, do I need to scale, and what is using the most CPU/memory?" from the Kubernetes API alone (issue #275). It reports, per node and cluster-total, allocatable / committed (sum of requests) / free headroom; the top consumers by CPU and by memory request; and a short plain-language verdict — whether a typical in-cluster build would currently schedule and whether another node is needed. It is SCHEDULING headroom (requests vs allocatable), which is what determines whether a pod schedules and needs no metrics-server; live utilization is a separate layer (see UtilizationNote, issue #276).
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 ChecksReport ¶
type ChecksReport struct {
App string `json:"app"`
Environment string `json:"environment,omitempty"`
// Enabled reports whether the default deploy-time check runs. True unless it was turned off for
// this app and environment.
Enabled bool `json:"enabled"`
// Dependencies are what Burrow derived from what it provisioned. Empty means there is nothing to
// check, and then no check pod runs at all — an app with no database and no exposure costs
// nothing.
Dependencies []Dependency `json:"dependencies"`
// Note explains an empty or partial list in one line, so "nothing to check" and "could not tell"
// do not read the same.
Note string `json:"note,omitempty"`
}
ChecksReport is what `burrow app checks` answers and what the health surface carries: whether the default check runs for this app, and what Burrow would check if it did.
Both halves are needed. ADR-0072 described `post-deploy` as user-configured, and this adds a Burrow-SUPPLIED default on that path, so a hook the user never configured can still run — which ADR-0076's consequences say must be visible and disableable rather than silent. Listing the derived dependencies is the "visible" half: an operator can see exactly what runs in their app's image after a deploy, without reading this file.
type Clock ¶
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 CloudNativePGCapability ¶
type CloudNativePGCapability struct {
Present bool `json:"present"`
Ready bool `json:"ready"`
Version string `json:"version,omitempty"`
Pinned string `json:"pinned,omitempty"`
}
CloudNativePGCapability reports the CloudNativePG operator, the cluster prerequisite ADR-0066 §1 puts the Postgres add-on on — `addon install postgres` is refused without it. It is three facts rather than one because they fail apart, and each failure looks like the others from the outside:
- Present is whether the postgresql.cnpg.io API group is served — the CRDs are installed, so a `Cluster` object can be written. Detected via API-group discovery, which needs no RBAC.
- Ready is whether a controller is actually RUNNING. CRDs are cluster-scoped and outlive the operator that installed them, so a cluster can accept a `Cluster` object that nothing will ever reconcile. Present without Ready is precisely that state, and it must not read as installed.
- Version is the running operator's release, read from its image tag, empty when unknown. Pinned is the release Burrow targets — a constant, not a cluster read. They are reported side by side because the placement translation Burrow writes into a `Cluster` is a claim about a specific release's schema (ADR-0077 §3), so a cluster running an older operator is a fact the operator of it should be able to see rather than discover from a pruned field.
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"`
// MetricsServer is whether metrics-server is serving the Kubernetes Metrics API (metrics.k8s.io),
// detected via API-group discovery. It powers `kubectl top`, HPA CPU/memory autoscaling, and the
// utilization layer of capacity reporting; Burrow auto-ensures it as a baseline (ADR-0054 §1).
MetricsServer MetricsServerCapability `json:"metrics_server"`
// CloudNativePG is the CloudNativePG operator's situation: whether its CRDs are served, whether
// a controller is actually running, and which release it is (ADR-0066 §1). It is the cluster
// prerequisite the Postgres add-on runs on, installed by an operator-CLI setup
// step because CRDs need cluster-admin.
CloudNativePG CloudNativePGCapability `json:"cloudnative_pg"`
// PgBackRest is the CloudNativePG pgBackRest plugin's situation (ADR-0066 §3): the component that
// archives a Postgres instance's write-ahead log and takes its base backups. It is reported
// separately from CloudNativePG because the two fail apart — an operator with no plugin runs
// databases that cannot be backed up off-cluster — and installed by the same operator-CLI step,
// for the same reason: its CRDs need cluster-admin.
PgBackRest PgBackRestCapability `json:"pgbackrest"`
// 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 ClusterConfigFunc ¶
type ClusterConfigFunc func(ctx context.Context) OperationalConfig
ClusterConfigFunc supplies the operator-set operational configuration to an ADAPTER — a piece of the system that reads a cluster-tier limit at the moment it acts, rather than being handed the value by the engine (ADR-0068 §6).
It exists because §6's remaining occupants are read where no engine call can reach: the unschedulable grace is applied inside the pod inspection that produces a WorkloadStatus, the build Job's retention inside the Job the build adapter constructs, and the metrics add-on's retention inside its container args. Threading each through the narrow seams those sit behind would widen three interfaces to carry three numbers; a supplier the adapter calls keeps the seams as they are and keeps the READ where the value is used, which is what makes `cluster config set` take effect without restarting burrowd.
A NIL ClusterConfigFunc IS VALID and yields the built-in defaults. That is the point: an adapter nobody wired — every test, and any embedder that has not opted in — behaves exactly as it did when these were constants.
func ClusterConfigFrom ¶
func ClusterConfigFrom(read func(context.Context) (OperationalConfig, error)) ClusterConfigFunc
ClusterConfigFrom adapts a Database read into a ClusterConfigFunc. A read that FAILS yields the empty configuration, which resolves every limit to its built-in default, and says so once in the log rather than propagating: these values are read on the deploy and status paths, where the database being briefly unavailable must not become a failed deploy or a status call that errors instead of answering. Values are validated on the way IN, where an operator is present to be told (see resolve) — this is the same principle applied one level up.
func (ClusterConfigFunc) ClusterDuration ¶
ClusterDuration returns the effective CLUSTER-tier value of a duration limit. It is the only accessor an adapter needs, because every limit an adapter reads is cluster-scoped: an adapter acts on a cluster, and the environment-scoped tier belongs to the engine, which knows which environment an operation named.
A nil receiver, an unreadable configuration, and an unset limit all give the built-in default, which are three different reasons for the same correct answer.
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 ClusterResourceState ¶ added in v0.13.0
type ClusterResourceState struct {
Nodes []NodeAllocatable
Pods []PodRequest
}
ClusterResourceState is the raw scheduling-capacity facts read from the Kubernetes API alone (node allocatable and the per-pod sum of resource requests) — no metrics-server involved. The engine turns it into a CapacityReport; keeping the seam's output raw keeps the headroom, top consumers, and verdict math pure and unit-testable against a fake.
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 Consumer ¶ added in v0.13.0
type Consumer struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Node string `json:"node,omitempty"`
CPUMillis int64 `json:"cpu_millis"`
MemBytes int64 `json:"mem_bytes"`
}
Consumer is one pod's contribution to the committed total, for the top-consumers lists. CPUMillis is milli-CPU; MemBytes is bytes. It is a request figure, not live usage.
type Coverage ¶
type Coverage struct {
// Since and Until bound the period this describes.
Since time.Time `json:"since"`
Until time.Time `json:"until"`
// Windows are the observer runs overlapping that period, oldest first.
Windows []ObservationWindow `json:"windows"`
// Gaps are the stretches inside it that no window covers, oldest first. Non-empty means the
// ledger is INCOMPLETE for this period and an empty failure list says nothing about it.
Gaps []CoverageGap `json:"gaps"`
// DegradedSweeps is how many sweeps across those windows could not read every object they set
// out to. It is partial coverage rather than none: rows for the objects a degraded sweep could
// not read may be missing or stale, and their absence is not evidence of health either.
DegradedSweeps int64 `json:"degraded_sweeps,omitempty"`
// Detail is the most recent degradation note across those windows, empty when none degraded.
Detail string `json:"detail,omitempty"`
}
Coverage is what the observer was doing over the period an answer describes, reported ALONGSIDE every answer rather than on request.
ADR-0074's consequences name the failure this exists to prevent: "if the observer was down from 02:00 to 03:00, an empty ledger for that hour reads as 'nothing broke'". A caller that has to ask a second question to learn the first answer was hollow will not ask it at 3am, so the answer carries its own caveat.
type CoverageGap ¶
type CoverageGap struct {
// From is when coverage stopped.
From time.Time `json:"from"`
// To is when it resumed — or the moment the answer was assembled, for a gap that is still open.
To time.Time `json:"to"`
}
CoverageGap is a stretch of time no observer was recording. It is a LITERAL gap, not an inference: each observation window is one continuous run of the observer, so the time between two windows is time in which burrowd was down, restarting, or wedged — and the failures that began and ended inside it were not seen and are not in the ledger.
func (CoverageGap) Duration ¶
func (g CoverageGap) Duration() time.Duration
Duration is how long the gap lasted.
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 the agent control channel — 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 in env, or ErrNotFound if
// the app has no releases there. Releases are keyed per (app, environment) (ADR-0052 Phase 4a):
// env is the canonical environment name ("prod" for the default environment, ADR-0067 §2).
LatestRelease(ctx context.Context, app, env string) (Release, error)
// Releases returns all releases for app in env, oldest first, keyed per (app, environment).
// An app with no releases there yields an empty slice and no error.
Releases(ctx context.Context, app, env string) ([]Release, error)
// ListReleases returns all releases for app in env, NEWEST first — the deploy timeline the
// history surface reads (the same rows deploys already write, read the other way round from
// Releases). Releases are keyed per (app, environment) (ADR-0052 Phase 4a). An app with no
// releases there yields an empty slice and no error.
ListReleases(ctx context.Context, app, env string) ([]Release, error)
// DeleteReleases removes all release records for app across every environment — the durable
// side of an app teardown, which removes the whole app. 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
// AppHook returns the command app runs at phase in env — the lifecycle hook configured beside the
// app's config (ADR-0072 §1). A phase with no hook yields a nil command and no error: unset means
// no hook and today's behaviour exactly, so absence is the ordinary answer and not ErrNotFound.
// The command is an argv, stored so an argument boundary survives rather than depending on a
// shell to re-split it. env is the canonical environment name ("prod" for the default one).
AppHook(ctx context.Context, app, env string, phase HookPhase) ([]string, error)
// AppHooks returns every hook configured for app in env. None yields an empty slice and no error.
AppHooks(ctx context.Context, app, env string) ([]Hook, error)
// SetAppHook upserts the command app runs at phase in env, replacing any command already set
// there — the write behind `burrow app hook set`. The command arrives validated (non-empty argv).
SetAppHook(ctx context.Context, app, env string, phase HookPhase, command []string) error
// UnsetAppHook removes app's hook at phase in env. Removing a hook that is not set is a no-op,
// not an error: afterwards that phase runs nothing, which is what the caller asked for.
UnsetAppHook(ctx context.Context, app, env string, phase HookPhase) error
// DeleteAppHooks removes every hook for app across every environment — the durable side of an app
// teardown, beside DeleteReleases. Deleting the hooks of an app that has none is a no-op.
DeleteAppHooks(ctx context.Context, app 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
// OperationalConfig returns the operator-set operational configuration: every stored limit
// value, keyed by the code it was set under (ADR-0068 §1). A limit with no stored value
// resolves to its built-in default, so a store with nothing set yields the defaults.
OperationalConfig(ctx context.Context) (OperationalConfig, error)
// SetLimit persists the value of one operational limit — the write behind `cluster config
// set`. code is the key the value is stored under, which is the bare limit code for a cluster
// value and `<env>.<code>` for an environment one; the engine composes it, exactly as it does
// for SetGuardrail. The value arrives already validated and in its canonical text form.
SetLimit(ctx context.Context, code LimitCode, value string) error
// AutoDeployLevel returns the auto-deploy level configured for app in the named environment
// (ADR-0052 §2). A missing configuration resolves to DefaultAutoDeployLevel (off): auto-deploy is
// opt-in, so an app with no stored row is off and is never polled (ADR-0058). env is the canonical
// environment name ("prod" for the default environment, ADR-0067 §2).
AutoDeployLevel(ctx context.Context, app, env string) (AutoDeployLevel, error)
// SetAutoDeployLevel upserts the auto-deploy level for app in the named environment — the write
// behind `burrow app auto-deploy <app> <level>`. It rejects an invalid level. It CLEARS any
// stored disable reason: a human setting the level is the deliberate re-enable action that
// removes a rollback or downgrade note (ADR-0052 §5).
SetAutoDeployLevel(ctx context.Context, app, env string, level AutoDeployLevel) error
// DisableAutoDeploy sets app's level to off in the named environment AND records why (e.g.
// "disabled by rollback") — the safety stop of ADR-0052 §5, so the watcher does not fight a
// deliberate downgrade. It upserts, overwriting any prior level and reason.
DisableAutoDeploy(ctx context.Context, app, env, reason string) error
// AutoDeployCandidates returns the distinct (app, environment) pairs the pull-based watcher may
// reconcile: every app that has a recorded release, paired with the environment it was released
// into (ADR-0052 Phase 4b). Candidacy is "has a running release" — the set the poller can compare
// a registry tag against — not "has a stored level row"; the poller reads each pair's level and
// skips those that are off, which is the default (ADR-0058), so an app that never opted in is read
// and skipped before any registry call. None yields an empty slice and no error.
AutoDeployCandidates(ctx context.Context) ([]AppEnvRef, error)
// AutoDeployReason returns the stored disable reason for app in the named environment, or ""
// when the level was human-set or is the default (no stored override) — the reason surfaced
// next to an off level (ADR-0052 §5).
AutoDeployReason(ctx context.Context, app, env string) (string, 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, the destination it is
// being written to, 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 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
// FailBackup marks a recorded backup failed AND records why: reason is a member of the closed
// BackupFailureReason set, or of ADR-0074 §2's IssueReason set when the Job never started, and
// detail is one Burrow-authored line. It is a separate method from SetBackupStatus because a
// failure without a reason is the thing ADR-0063 §7 exists to stop — "it did not work" sends the
// reader to the logs, which is where the destination's silence was hiding in the first place.
//
// Neither argument may carry a secret value or a vendor response body. Failing an unknown backup
// id returns ErrNotFound.
FailBackup(ctx context.Context, id, reason, detail string) error
// ListBackups returns recorded backups, newest first. An empty app lists every app's backups and
// an empty env every environment's; a non-empty value restricts to that app or that environment.
// A listing is a read, not a target, so an unfiltered call spans environments deliberately —
// unlike backup and restore, which act on exactly one instance and take the environment as a
// required argument (ADR-0067 §1). No matches yields an empty slice and no error.
ListBackups(ctx context.Context, app, env string) ([]Backup, error)
// GetBackup returns the backup with the given id, or ErrNotFound.
GetBackup(ctx context.Context, id string) (Backup, error)
// PendingBackups returns the recorded backups still in `pending` that were created before
// `before`, oldest first — the candidates for ADR-0074 §6's "a backup row still pending whose Job
// no longer exists". The cutoff is the caller's, not the store's: it is what separates a backup
// that is simply still running from one nothing is going to finish, and the observer sets it from
// the injected clock. None yields an empty slice and no error.
PendingBackups(ctx context.Context, before time.Time) ([]Backup, error)
// ManagedApps returns the distinct (app, environment) pairs the registry says Burrow OWNS A
// WORKLOAD FOR: an app whose release history shows a rollout that actually reached the cluster.
// It is the intent side of ADR-0074 §6's first comparison, so it is deliberately not "every app
// with a release row" — an app whose only deploy failed before a workload was ever applied has no
// Deployment to be missing, and reporting one would be a false positive on the day someone is
// already debugging a failed deploy. None yields an empty slice and no error.
ManagedApps(ctx context.Context) ([]AppEnvRef, error)
// RecordExposure upserts the intent behind a successful expose, keyed by (app, environment)
// (ADR-0074 §6). It records what was ASKED FOR — the host, the port, whether TLS was requested —
// and never whether it currently works, which stays a live read.
RecordExposure(ctx context.Context, ex Exposure) error
// DeleteExposure removes the recorded exposure for app in env. Removing one that is not recorded
// is a no-op, not an error: an unexpose must succeed whether or not the intent row survived.
DeleteExposure(ctx context.Context, app, env string) error
// Exposures returns every recorded exposure, ordered by app then environment. None yields an
// empty slice and no error.
Exposures(ctx context.Context) ([]Exposure, error)
// Exposure returns the recorded exposure for app in env, or ErrNotFound when the app is not
// published there. It is the read behind ADR-0076 §3's conservative default: the container port
// an exposure routes to is the ONLY port Burrow knows for an app, so it is what a readiness
// probe can honestly check. Not being published is the ordinary case, not a failure — the caller
// treats ErrNotFound as "no port known" and authors no probe.
Exposure(ctx context.Context, app, env string) (Exposure, error)
// HealthEndpoint returns the health endpoint declared for app in env (ADR-0076 §5), or the zero
// value when none was declared — which is where every app starts, since the endpoint is opt-in.
// A missing row is not an error.
HealthEndpoint(ctx context.Context, app, env string) (HealthEndpoint, error)
// SetHealthEndpoint upserts the declared health endpoint for app in env — the write behind
// `burrow app health set`. The path and port arrive already validated.
SetHealthEndpoint(ctx context.Context, ep HealthEndpoint) error
// UnsetHealthEndpoint removes the declared endpoint for app in env, returning the app to
// ADR-0076 §3's default. Unsetting one that was never declared is a no-op, not an error.
UnsetHealthEndpoint(ctx context.Context, app, env string) error
// DeleteHealthEndpoints removes every declared endpoint for app across all environments — the
// durable side of an app teardown, alongside DeleteReleases. Deleting for an app that declared
// none is a no-op.
DeleteHealthEndpoints(ctx context.Context, app string) error
// DependencyChecksEnabled reports whether the deploy-time dependency check runs for app in env
// (ADR-0076 §4). It answers TRUE when nothing was recorded: the check is a Burrow-supplied
// default, so a row exists only where someone made a decision about it, and a missing row is the
// default rather than an error.
DependencyChecksEnabled(ctx context.Context, app, env string) (bool, error)
// SetDependencyChecks records whether the deploy-time dependency check runs for app in env — the
// write behind `burrow app checks enable|disable`, and the "disableable rather than silent" half
// of adding a Burrow-supplied default to a path ADR-0072 described as user-configured.
SetDependencyChecks(ctx context.Context, app, env string, enabled bool, at time.Time) error
// DeleteDependencyCheckSettings removes app's recorded setting across all environments — the
// durable side of an app teardown, alongside DeleteHealthEndpoints. Deleting for an app that
// never recorded one is a no-op.
DeleteDependencyCheckSettings(ctx context.Context, app string) error
// RecordFailure opens or extends the ledger row for one observed failure (ADR-0074 §4). The row
// is keyed by (object, reason) so concurrent reasons on one object coexist as separate rows with
// independent lifetimes (§5). A first sighting writes first_seen, last_seen and a count of one; a
// later sighting of the same still-active failure advances last_seen and increments the count,
// leaving first_seen — the answer to "when did it start" — alone. A sighting of a failure that
// had been RESOLVED opens a new row rather than reviving the old one, so a thing that broke,
// recovered and broke again reads as two episodes instead of one long outage.
//
// It is the ledger's only write path for failures, and it is not the audit log: they are separate
// tables deliberately (§7).
RecordFailure(ctx context.Context, obs FailureObservation) error
// ResolveFailures closes, at time at, every active ledger row that is not in keep and whose
// object is not in skip — the "did it recover on its own" half of §4, decided by absence.
//
// The two lists are what keep that decision honest. keep is every failure the sweep observed
// still happening. skip is every object the sweep could NOT read: those rows are left exactly as
// they are, because a failure Burrow could not check is not a failure that recovered. A row for
// an object that is in neither — one the sweep read and found healthy, or one that has left the
// registry entirely — is resolved.
ResolveFailures(ctx context.Context, at time.Time, keep []FailureKey, skip []ObjectRef) error
// Failures returns ledger rows matching filter, oldest first. Oldest-first is the order ADR-0074
// §5 asks for: when one cause has produced many rows, the earliest first_seen in the cascade is
// the likeliest thing to actually fix. No matches yields an empty slice and no error.
Failures(ctx context.Context, filter FailureFilter) ([]Failure, error)
// StartObservationWindow opens a new observation window beginning at `at` and returns its id.
// One is opened per RUN of the observer, never reused across a restart: a gap between windows is
// time nobody was watching, and it is the only thing that stops an empty ledger from reading as
// "nothing broke" (ADR-0074's consequences).
StartObservationWindow(ctx context.Context, at time.Time) (int64, error)
// ExtendObservationWindow advances window id's coverage to `until` and counts one more sweep. A
// non-empty degraded note counts the sweep as incomplete and is stored as the window's most
// recent degradation, so a stretch during which some objects could not be read is visible as
// partial coverage rather than as clean coverage. Extending a window that no longer exists
// returns ErrNotFound, which tells the caller to open a fresh one rather than stop recording.
ExtendObservationWindow(ctx context.Context, id int64, until time.Time, degraded string) error
// ObservationWindows returns the windows whose coverage ends at or after since, oldest first,
// capped by limit (a store default when unset). The gaps BETWEEN the returned windows are the
// answer the caller is really after. None yields an empty slice and no error.
ObservationWindows(ctx context.Context, since time.Time, limit int) ([]ObservationWindow, error)
// PruneLedger enforces ADR-0074 §4's retention bound: it removes failure rows RESOLVED before
// `before` and observation windows whose coverage ended before it. An ACTIVE failure is never
// removed, however old — a thing that is still broken is not history. Unbounded growth here is
// not untidiness; it is an outage in the control plane's own database, which is why the bound is
// a requirement rather than a setting.
PruneLedger(ctx context.Context, before time.Time) (LedgerPruneResult, 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 default environment `prod` IS stored here, written once by Engine.EnsureDefaultEnvironment
// at burrowd startup (ADR-0067 §2) rather than synthesized on every read.
CreateEnvironment(ctx context.Context, name, namespace string) error
// ListEnvironments returns the registered environments ordered by name, INCLUDING the default
// environment `prod`; the engine hoists that one first and marks it. None yields an empty slice
// and no error.
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)
// DeleteEnvironment removes the registered environment with the given name, or ErrNotFound when
// no such environment is registered. The default environment `prod` is stored here but is never
// removed: the engine rejects it before this call (ADR-0067 §2).
DeleteEnvironment(ctx context.Context, name string) 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
// environment env's own 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. env and app are both
// validated against a strict identifier pattern and every SQL identifier is quoted BEFORE any SQL
// runs, so neither can carry SQL; an empty env is ErrInvalid, never the default environment. 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, env string) (databaseURL string, err error)
// DropAppDatabase removes app's database and login role from environment env's instance — the
// destructive side of detach. Dropping a database/role that is already absent is a no-op, not an
// error. env and app are validated before any SQL, exactly as in EnsureAppDatabase, so a detach
// can no more reach another environment's server than an attach can.
DropAppDatabase(ctx context.Context, app, env string) error
}
DatabaseProvisioner is the seam over an installed Postgres add-on instance's admin surface (ADR-0031). burrowd connects to the environment's 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 the agent control channel (ADR-0029/0031).
EVERY METHOD TAKES THE ENVIRONMENT, AND IT IS NOT OPTIONAL (ADR-0067 §1). Databases keep their simple names — an app called web has a database called web — so the environment is the only thing that distinguishes web-in-staging from web-in-production. Without it, provisioning web a second time found the first one, and because provisioning is IDEMPOTENT it did not fail: it rotated the role password and handed back a URL pointing at the other environment's data (issue #339). The environment selects the INSTANCE, so an implementation cannot reach another environment's server at all; an empty environment is rejected before any SQL, because "a signature that can omit it is a signature that will omit it".
type Dependency ¶
type Dependency struct {
// Kind is the class of dependency.
Kind DependencyKind `json:"kind"`
// Provisioned is the one-line, non-secret statement of WHAT Burrow gave the app, so a reader can
// see that the check was derived rather than configured.
Provisioned string `json:"provisioned"`
// EnvKey is the environment variable the app reads the dependency's credential from, for a
// dependency reached with one. Never a value.
EnvKey string `json:"env_key,omitempty"`
// Endpoint is the in-cluster address the check dials, for a dependency reached without a
// credential. Never a value.
Endpoint string `json:"endpoint,omitempty"`
}
Dependency is one thing Burrow gave an app and can therefore check: what it is, what Burrow provisioned that makes it a fact rather than a guess, and the non-secret handle the check uses.
It carries no credential and no value. EnvKey is a KEY NAME — the env var the app already reads — and the value behind it is resolved by Kubernetes inside the check pod and never travels here.
type DependencyKind ¶
type DependencyKind string
DependencyKind names a class of dependency Burrow provisioned for an app and can therefore check. It is a closed set, and it is closed for the same reason ADR-0074 §2's IssueReason set is: the consumer that matters is an agent branching on the value, not a person reading prose.
const ( // DependencyPostgres is a database and login role Burrow provisioned for the app on its // environment's Postgres instance, reachable through the DATABASE_URL Burrow wrote into the // app's Secret (ADR-0031). DependencyPostgres DependencyKind = "postgres" // DependencyExposure is the app's published port, reachable through the Service the exposure // created in front of it (ADR-0018). DependencyExposure DependencyKind = "exposure" )
type DependencyOutcome ¶
type DependencyOutcome string
DependencyOutcome is what one check found. Three values rather than a boolean, because "the dependency answered", "the dependency did not answer" and "Burrow could not ask" are three different facts and collapsing the third into the second reports a healthy database as broken.
const ( // DependencyPassed means the dependency answered. DependencyPassed DependencyOutcome = "passed" // DependencyFailed means the dependency did not answer, or answered with a refusal. DependencyFailed DependencyOutcome = "failed" // DependencySkipped means the check did not run: Burrow could not stand the check up, or could // not read what it needed to derive it. It is NOT a failure of the dependency. DependencySkipped DependencyOutcome = "skipped" )
type DependencyResult ¶
type DependencyResult struct {
// Kind is the dependency this result is about.
Kind DependencyKind `json:"kind"`
// Outcome is passed, failed, or skipped.
Outcome DependencyOutcome `json:"outcome"`
// Reason is a member of DependencyReasons, empty on a passed check.
Reason string `json:"reason,omitempty"`
// Detail is one bounded, Burrow-authored line: what was tried and what came back. It never
// carries a credential, a driver message, or any part of a connection string beyond the host and
// port that were dialled.
Detail string `json:"detail,omitempty"`
// Status is the HTTP status code an exposure check received. Zero when none was.
Status int `json:"status,omitempty"`
}
DependencyResult is what one check found. It is the unit reported on a deploy and by `burrow app checks`.
func (DependencyResult) Failed ¶
func (r DependencyResult) Failed() bool
Failed reports whether this result is a dependency that did not answer — as distinct from a check that did not run.
type DeployRequest ¶
type DeployRequest struct {
App string `json:"app"`
// Env is the environment to deploy into (ADR-0035 phase 2b): empty or "prod" targets the default
// environment's namespace — the one install created (ADR-0067 §2) — and a name added later 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"`
// Hints are non-blocking notes about the deploy the agent can reason over (ADR-0052 §8): today,
// a nudge toward semver when the deployed tag cannot be classified for auto-update. They never
// gate or fail the deploy — any reference deploys (ADR-0007). Empty when there is nothing to note.
Hints []string `json:"hints,omitempty"`
// Dependencies is what the deploy-time dependency check found (ADR-0076 §4): for each thing
// Burrow provisioned for this app — a database it attached, a port it published — whether the app
// could actually reach it from inside its own container after the deploy.
//
// IT IS A REPORT AND NEVER A VERDICT ON THE DEPLOY. A failed entry here sits on a DeployResult
// that succeeded, because it does: the release is deployed, the rollout happened, and Burrow does
// not roll back by itself (ADR-0072 §6). Empty when Burrow provisioned nothing it can check, or
// when the check is turned off for this app.
Dependencies []DependencyResult `json:"dependencies,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
// ObjectStore builds an ObjectStore for an S3-compatible endpoint and credential pair, so a
// backup can be written outside the cluster it came from (ADR-0063). Optional — nil is allowed,
// and registering an object-storage provider errors cleanly (ErrNotImplemented) when it is not
// wired, exactly as the build path does without a Builder.
ObjectStore ObjectStoreFactory
// 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
// CapacityProber reads the cluster's scheduling-capacity facts (node allocatable, pod requests)
// for the headroom surface (issue #275). Optional — nil is allowed, and the engine errors cleanly
// (ErrNotImplemented) on a capacity read when it is not wired.
CapacityProber CapacityProber
// RegistryClient lists an image repository's tags for the auto-deploy read/watch (ADR-0052).
// Optional — nil is allowed, and the auto-deploy show degrades to reporting the level alone
// when it is not wired. It is OUTBOUND-only and never used on the core deploy path (ADR-0040).
RegistryClient RegistryClient
// Builder builds an image from a git source reference inside the cluster for the optional
// in-cluster build path (ADR-0053). Optional — nil is allowed, and the engine errors cleanly
// (ErrNotImplemented) on a build when it is not wired.
Builder Builder
// BuildRegistry is the in-cluster registry reference host:port the in-cluster build defaults its
// push target to when the caller supplies none — the zero-config default push target for a build
// (ADR-0053 §5). Optional — an empty value means a build with no explicit target errors, and a
// caller-supplied target always overrides it (external registries stay fully supported). burrowd
// sets it from BURROW_BUILD_REGISTRY, which `burrow cluster registry install` wires to the
// in-cluster registry it deploys.
BuildRegistry string
// BuildPublicRegistry is the PUBLIC registry host the in-cluster build's resulting deploy
// references so the node pulls through the ingress over TLS, distinct from BuildRegistry (the
// internal push endpoint) — ADR-0054 §5. Optional — an empty value falls back to referencing the
// internal push endpoint. burrowd sets it from BURROW_BUILD_PUBLIC_REGISTRY, which
// `burrow cluster registry install --host` wires to the registry's public ingress hostname.
BuildPublicRegistry string
// AppNamespace is the namespace burrowd deploys apps into (BURROW_NAMESPACE) — the namespace the
// default environment `prod` maps to (ADR-0067 §3). Optional — an empty value defaults to the
// Kubernetes namespace "default", matching the kube Adapter. That is a NAMESPACE name and has
// nothing to do with the retired environment name of the same spelling.
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 ¶
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
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 `prod` (install already created it — ADR-0067 §2) and the retired `default`, 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 ¶
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) AppChecks ¶
AppChecks reports what Burrow checks after a deploy of app, and whether it checks at all (ADR-0076 §4). It is a read: nothing is changed and no check is run.
func (*Engine) AppHealth ¶
AppHealth reports the health endpoint declared for an app and the probe Burrow authors as a result (ADR-0076 §3, §5). It is a read: nothing is changed and nothing is rolled.
func (*Engine) AttachAddon ¶
func (e *Engine) AttachAddon(ctx context.Context, t AddonType, app, env string) (AttachResult, error)
AttachAddon gives app its own database on ENVIRONMENT env's Postgres instance and wires it into the app (ADR-0031). burrowd provisions an isolated database + login role on that environment's instance, generates the DATABASE_URL server-side, writes it into the app's per-app Secret in that environment's namespace 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 the agent control channel: no secret value crosses it — the agent supplies only the app name; burrowd generates the value and never returns it. The audit row records {addon, app, env} only — never the URL.
The environment is not optional and is resolved before anything is provisioned (ADR-0067 §1). It is the whole of the fix for issue #339: databases keep their simple names, so with one instance per cluster an attach of `web` in staging found production's `web`, and because provisioning is idempotent it did not fail — it rotated the role password and handed staging a URL pointing at production's data. Resolving the environment first sends the provisioning at staging's own instance, and sends the Secret to staging's own namespace, so the two attaches have nothing in common to collide over.
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) AutoDeploy ¶ added in v0.13.0
AutoDeploy returns the auto-deploy level configured for app in env (ADR-0052 §2). A missing configuration resolves to the built-in default (DefaultAutoDeployLevel, off): auto-deploy is opt-in, so an app with no stored level is off and is never polled (ADR-0058). The environment is resolved so an unknown name is a clear error, and the level is keyed by the canonical environment name. This is a read: the agent may observe it over burrow-agent, but only a human sets it (§6).
func (*Engine) AutoDeployStatus ¶ added in v0.13.0
AutoDeployStatus returns the enriched, read-only auto-deploy view for app in env (ADR-0052 §2/§3). It reads the level exactly as AutoDeploy does, then — when the registry seam is wired — lists the tags of the app's current image and computes what auto-deploy would take within the level and any higher upgrade above its cap. The registry listing is anonymous in this phase (RegistryAuth{}): public GHCR (the reference registry), public Docker Hub, DO, and GCR-token registries all list anonymously. Authenticated private-repo listing — reading the client-side burrow-registry pull secret — needs a deliberate burrowd RBAC grant withheld today under the least-privilege boundary (ADR-0017/ADR-0040); it lands with the Phase 4 poller, for which the seam is already ready via RegistryAuth. Any registry failure (unreachable, a private-repo 401, a non-semver current tag, or an absent seam) degrades to Checked=false with a Note and no error; only a validation or environment error returns a non-nil error.
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 bounded twice — the replica ceiling bounds the requested max the same way it bounds a manual scale, so a max above the ceiling is refused exactly like scaling above it (ADR-0068 §2), and the app.autoscale guardrail gates the operation itself (allow by default). 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, env, destination string) (BackupResult, error)
BackupAddon backs up app's database on the installed Postgres add-on (ADR-0032, ADR-0063 §7): burrowd resolves where the backup is to go, records a pending backup, runs an in-cluster Job that pg_dumps the database to the backup PVC and — when an object-storage destination is registered — writes it to the store and reads it back, and only then marks the backup completed. 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.
THE ROW IS NOT ALLOWED TO SAY SUCCEEDED FOR BYTES THAT DID NOT ARRIVE. Every path off the Job that is not "the Job succeeded" writes BackupFailed with the closed reason the Job reported, because a backup recorded as completed when the store never got it is worse than no row at all: it converts a missing backup into a false assurance, and the assurance is only tested at restore time. Nothing in this function has a path that leaves the row pending on a known failure, and pending is never read as success — the age of the last SUCCESSFUL backup counts completed rows only, so a row stranded pending by a burrowd that died mid-Job keeps the age growing rather than resetting it.
destination names which object-storage provider holds this backup; empty resolves it, which succeeds when exactly one is registered and refuses when several are (ADR-0063 §6). No object storage at all is not a failure — the dump still lands on the PVC, and the row records that it only ever reached the cluster.
It moves no secret value: the Job reads the superuser password only via secretKeyRef, the object-storage credential reaches the pod only through a Job-owned Secret, and the audit row and the returned result name the add-on, app, backup id, path, destination, and size — never a credential. Backup is allowed by default (it destroys nothing) and safe over the agent control channel.
func (*Engine) BackupHealth ¶
func (e *Engine) BackupHealth(ctx context.Context, t AddonType, app, env string) (BackupHealth, error)
BackupHealth reports what Burrow has observed about an add-on's backups: the age of the last successful backup, the age of the last one that left the cluster, the last failure, and whether each registered object-storage destination answers right now (ADR-0063 §7, ADR-0066 §5).
An empty app spans every app and an empty env every environment, exactly as ListBackups does — the same latitude, for the same reason: this answers "what coverage do I have", and each observation says which app and environment it came from. Unlike backup and restore, which act on one instance and take the environment as a required target, a health question that had to name an environment would be unable to answer the question most worth asking.
It is READ-ONLY and moves no secret: it reads its own rows, and the reachability probe reads the provider credential into memory to sign one request and returns names, not values. Nothing here is audited, because nothing here changes anything.
A destination that cannot be probed is reported as unreachable WITH the reason, never omitted: a destination silently missing from the listing is indistinguishable from no destination at all, and "there is nowhere to back up to" is exactly the wrong conclusion to draw from "the probe failed".
func (*Engine) BackupInstance ¶
func (e *Engine) BackupInstance(ctx context.Context, t AddonType, env, destination string) (BackupResult, error)
BackupInstance takes a PHYSICAL base backup of one environment's whole Postgres instance (ADR-0066 §2): Burrow creates a `postgresql.cnpg.io/v1 Backup`, CloudNativePG hands it to the pgBackRest plugin, and Burrow reads the result. No backup tool runs here, no Job is constructed, and no superuser credential is handled on this path — that is the change ADR-0066 §2 exists for.
THE ROW IS NOT ALLOWED TO SAY COMPLETED FOR BYTES THAT ARE NOT READABLE. A completed `Backup` object is the operator's word that pgBackRest exited zero; that the object store will serve the result back is a separate fact, and it is the one a restore depends on. So the backup's manifest is read back from the store at the key the repository says it is at, and only then does the row say completed (ADR-0063 §7). A store that will not answer is retried by the layers underneath; a store that answered and could not serve the object back is recorded as such and not retried.
destination names which registered object-storage provider holds the repository; empty resolves it, which succeeds when exactly one is registered and refuses when several are (ADR-0063 §6). Unlike a logical dump, no object storage at all is a REFUSAL rather than a cluster-destination fallback: there is no in-cluster tier for a physical backup, and an instance that archives nowhere has nothing to base one on.
It moves no secret value: the credential is read at call time to sign one read-back request, and the audit row and the returned result name the add-on, environment, backup id, provider and object key — never a credential. Backup destroys nothing and is allowed by default.
func (*Engine) Build ¶ added in v0.13.0
func (e *Engine) Build(ctx context.Context, req BuildRequest) (BuildResult, error)
Build builds an app's image from a git source reference inside the user's own cluster and, on success, hands the resulting image into the existing guarded deploy path (ADR-0053). It is the optional in-cluster build path, never the deploy spine: Burrow stays client-build-first and deploy stays by image reference (ADR-0007), so a build is a front-end that ends where deploy begins.
Only metadata crosses into the builder — the git reference and the target image reference; the builder clones the actual source from git inside the cluster, so no code travels over the control channel (ADR-0004, ADR-0053 §3). On a successful build the resulting digest-pinned reference flows into the same rollout, deploy record (the rollback handle), and audit entry an explicit deploy runs (ADR-0053 §4), so guardrails and the deploy history are never bypassed. On a builder error nothing is deployed: the error is surfaced structurally and the deploy path is not touched.
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) ClusterCapacity ¶ added in v0.13.0
func (e *Engine) ClusterCapacity(ctx context.Context) (CapacityReport, error)
ClusterCapacity reports the cluster's scheduling capacity and headroom (issue #275): per node and cluster-total allocatable / committed / free, the top CPU and memory consumers, and a verdict on whether a typical in-cluster build fits and whether another node is needed — all from the Kubernetes API alone, no metrics-server. It runs the read through the CapacityProber seam and computes the report purely, so it changes nothing in the cluster.
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 the agent control channel — 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 ¶
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).
Deploy is the explicit, human- or agent-triggered path (ADR-0007): it records a manual provenance. The Phase 4b pull-based watcher (ADR-0052) will drive the same rollout through the unexported deploy with an auto provenance.
func (*Engine) DetachAddon ¶
DetachAddon removes app's DATABASE_URL and, behind the addon.detach confirm guardrail (it destroys data), drops app's database and role from ENVIRONMENT env's Postgres instance (ADR-0031/0067 §1). The audit row records {addon, app, env} only. The environment is required for the same reason attach requires it, and the stakes are higher: without it, detaching `web` in staging would have dropped production's `web` database — the same collision as issue #339, with the destructive verb.
func (*Engine) DisableAutoscale ¶ added in v0.8.0
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) EnsureDefaultEnvironment ¶
func (e *Engine) EnsureDefaultEnvironment(ctx context.Context) (Environment, error)
EnsureDefaultEnvironment registers the environment install creates — `prod`, mapped to the app namespace burrowd runs against — and returns it (ADR-0067 §2–§3). burrowd calls it once at startup, after the migrations, so the registry row exists on a FRESH install and is BACKFILLED on an existing one by the same code path: an install predating ADR-0067 gains an environment named `prod` pointing at the namespace its apps are already in, and nothing in the cluster moves or is renamed. Registering it here rather than from `burrow cluster install` is what makes the upgrade case free — a re-run, a restart, and an upgrade all arrive at the same one row.
It is idempotent, and deliberately not a plain CreateEnvironment: an existing row is read back rather than treated as a duplicate error, so a second burrowd replica racing the first is a no-op rather than a failed startup.
A row that already maps `prod` to a DIFFERENT namespace is refused rather than repointed. It can only exist on an install that ran `burrow env add prod --namespace …` before this change — an install where `prod` already means a namespace of its own, and silently redefining it would send every unqualified operation into the wrong namespace. Migration 00018 refuses the same case up front, so this is the second net rather than the first.
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) Failures ¶
func (e *Engine) Failures(ctx context.Context, q FailureQuery) (FailureReport, error)
Failures answers the cluster-wide question ADR-0074 §8 asks: what, across everything Burrow manages, is broken — with the coverage behind that answer attached.
It reads only the ledger and the coverage record. It does NOT consult the cluster: current state is a live read that belongs to Status, and a listing that re-derived thirty objects' state from the API server during the incident that made them all fail is the wrong thing to be doing at that moment. The rows are what the observer saw at the time, which is the fact the reader is after.
func (*Engine) Guardrails ¶
Guardrails returns the guardrail policy as a list for inspection (ADR-0020). With an empty env, or with `prod` — the environment install created, whose policy IS the global policy (ADR-0067 §2) — it returns the global policy; with an environment added later 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). That environment must be registered; an unknown one is a clear ErrNotFound.
func (*Engine) History ¶ added in v0.13.0
History returns an app's deploy timeline: the releases recorded for it, newest first — what versions the app has been rolled to, when, and whether each landed (the Release.Status conveys success or failure). It is a read-only view over the deploy records the deploy engine already writes (ADR-0007); it records nothing and touches no cluster state, so it is safe over any channel. It resolves and validates env exactly as Status does — an unknown environment is a clean error — and reads the releases for that environment: releases are keyed per (app, environment) (ADR-0052 Phase 4a), so history is the app's timeline in the selected environment.
Each entry carries what the deploy record holds: the image reference (the version), the creation timestamp, the lifecycle status, and the deploy's provenance (ADR-0052 §5) — whether it was a manual or an unattended auto-update, and the level and tag an auto-update ran under. The record carries those fields, so history renders whatever they hold with no per-field logic here.
func (*Engine) Hooks ¶
Hooks returns the lifecycle hooks configured for app in env, in phase order (ADR-0072 §1). A phase with no hook is absent from the result rather than present and empty: unset means no hook and today's behaviour exactly.
func (*Engine) InstallAddon ¶
func (e *Engine) InstallAddon(ctx context.Context, t AddonType, env string, opts InstallAddonOptions) (AddonInfo, error)
InstallAddon deploys the vetted backing service for the named add-on type INTO ONE ENVIRONMENT and registers it as a queryable capability (ADR-0025/0026). It is guarded by addon.install.
Each environment gets its own instance (ADR-0067 §1): the registry key and the cluster resource names come from AddonInstanceName, so installing into the default environment lands on exactly the names an existing install already has — nothing migrates — and installing into `staging` stands a separate instance up beside it. The environment is resolved the same way a deploy's is, so an install that names none while several environments are registered is refused rather than landing on whichever one happens to be first (ADR-0047 §1).
func (*Engine) Limits ¶
Limits returns every operational limit with its effective value for env, each marking the tier that value came from (ADR-0068 §3). With an empty env, or with `prod` — the environment install created, whose configuration IS the cluster configuration (ADR-0067 §2) — it reports the cluster tier and the built-in defaults; an environment added later reports its own values where it has them. That environment must be registered; an unknown one is a clear ErrNotFound.
This is a read of what the operator set, and reading a limit is harmless. SETTING one is the operator's alone (ADR-0068 §4).
func (*Engine) ListAddons ¶
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 ¶
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 ¶
ListBackups returns recorded backups, newest first, from the control-plane database (ADR-0032). An empty app lists every app's backups and an empty env every environment's; a non-empty value restricts to that app or environment. Read-only and safe over the agent control channel — it names the app, environment, size, time, and on-PVC path, never a credential.
An unfiltered listing deliberately spans environments rather than defaulting to one: a listing answers "what dumps exist", and each row now says which environment it came from, so the answer is legible without a filter (ADR-0067 §1). Backup and restore, which act on exactly one instance, do not get that latitude — they take the environment as a required target.
func (*Engine) ListConfig ¶ added in v0.7.0
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, ADR-0067 §2): the default environment `prod` first, followed by the environments added later in name order. Unlike before ADR-0067 the default is a REGISTERED row like any other — install creates it (EnsureDefaultEnvironment) — so a fresh install lists exactly one environment and a single-environment user never meets the ambiguity refusal.
It is synthesized only when its row is missing, which is a burrowd that has not yet run its startup ensure. That fallback is not cosmetic: without it a cluster with `staging` registered and no `prod` row would list ONE environment, and ADR-0047's forcing function — which counts environments — would let an unqualified mutating operation through instead of refusing it.
func (*Engine) ListSecrets ¶
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 the agent control channel, so this read returns keys only. An app with no secrets yields an empty slice.
func (*Engine) NewAutoDeployPoller ¶ added in v0.13.0
func (e *Engine) NewAutoDeployPoller(cfg AutoDeployConfig) *AutoDeployPoller
NewAutoDeployPoller builds the watcher bound to this engine. It reads the engine's registry, database, and clock seams, and drives the same unexported guarded deploy path an explicit call uses, stamped with an auto provenance (ADR-0052 §5). Constructing it does not start it — call Run.
func (*Engine) NewObserver ¶
func (e *Engine) NewObserver(cfg ObserverConfig) *Observer
NewObserver builds the observer bound to this engine. It reads the engine's Kubernetes, database and clock seams and writes only to the database. Constructing it does not start it — call Run.
func (*Engine) NextTag ¶ added in v0.13.0
NextTag suggests the next semver release tags for app in env, read from its current running tag (ADR-0052 §8). It reads the current tag exactly as AutoDeployStatus does (the latest release's image), and when that tag parses as stable semver returns the next patch, minor, and major tags. A missing release or a non-semver current tag degrades to a Note with no suggestion and no error, so the guidance is best-effort and never blocks the agent (ADR-0040). It changes nothing.
func (*Engine) ObjectStoreCredentialFor ¶
func (e *Engine) ObjectStoreCredentialFor(ctx context.Context, p Provider) (ObjectStoreCredential, error)
ObjectStoreCredentialFor reads an object-storage provider's credential PAIR from burrow-credentials at call time, so a rotated key is picked up with no restart (ADR-0023, ADR-0063 §1). The values are returned to the caller that makes the vendor call and are never logged, audited, or returned over any API.
func (*Engine) Providers ¶
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) QueryMetricsRange ¶
func (e *Engine) QueryMetricsRange(ctx context.Context, query string, backend string, start, end time.Time, step time.Duration) ([]MetricSeries, error)
QueryMetricsRange runs a PromQL range query against the connected metrics add-on and returns the matching time series over [start, end] sampled every step. It is the time-series sibling of QueryMetrics — the read path behind a sparkline or area chart — and resolves and authenticates the add-on identically (ADR-0026): it locates the "metrics" add-on, reads its token when the backend is authenticated, and dispatches through the querier keyed by that backend. The window is caller-supplied (no ambient time in the engine, ADR-0010). The selected querier must additionally implement the optional MetricsRangeQuerier seam; a backend that offers only instant queries returns a clean ErrNotImplemented-wrapped error. 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).
func (*Engine) Reachability ¶
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, opts RemoveAddonOptions) (RemoveAddonResult, error)
RemoveAddon removes the named add-on instance. It tears the add-on's WORKLOAD down and, by default, LEAVES ITS DATA VOLUME IN PLACE: for Postgres that volume holds every attached app's database (ADR-0031), so a removal meant as "stop this and reinstall it cleanly" must not destroy it. Passing DeleteData is the explicit, separate ask that destroys the volume.
It is guarded by addon.remove (ADR-0025), held for confirmation by default. The guardrail's message carries the CONCRETE consequence — which volume goes or stays, which apps are affected by name, and whether a final backup will be taken first — because a confirmation prompt that does not say what is destroyed is not informed consent (ADR-0006: the gate hands back a reason the human and the agent can act on). A hard refusal is deliberately NOT used for the attached-apps case: this system expresses "never" as a policy disposition (`guard set addon.remove deny`, ADR-0020), not as a bespoke override flag, and the destructive path already requires the caller to have typed DeleteData.
THE MECHANISM DOES NOT CHANGE THE CONTRACT. A Postgres instance is a CloudNativePG `Cluster` (ADR-0066 §1) and keeps its data on removal exactly as a Deployment-backed add-on does; the difference is entirely below this line, because the operator owns those claims and keeping them means disowning them before its `Cluster` is deleted rather than simply not deleting them. What that costs here is a single argument — the add-on's TYPE is read from the registry row and handed to the seam, because a removal that inferred the shape from the cluster would read a refused probe as an absent add-on.
WHERE AN OBJECT STORE IS REGISTERED, DeleteData TAKES A FINAL BACKUP FIRST AND ABORTS IF IT FAILS (ADR-0064 §5). The ordering is the whole of the safety: nothing is destroyed until a copy is known to exist off the cluster. planFinalBackup decides that before the guardrail is evaluated, so the human confirming the removal is told whether a copy will be made; finalBackupBeforeDataDeletion takes it, and any failure returns before the first destructive call.
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) RemoveEnvironment ¶
RemoveEnvironment unregisters a named environment (ADR-0035 phase 2), the inverse of AddEnvironment. It rejects `prod` — the environment install creates, which every unqualified operation resolves to (ADR-0067 §2) — and an empty name, and returns ErrNotFound for an unknown name (from the store). Like AddEnvironment it only touches the registry entry: the environment's namespace and any apps in it are managed out of band — kubeconfig-side by the operator in the single-tenant install, or by the managed control plane's own teardown in the cloud — so removing the mapping does not delete the namespace. A removed environment can be re-added later.
func (*Engine) RestoreAddon ¶
func (e *Engine) RestoreAddon(ctx context.Context, t AddonType, app, backupID, env 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, env, backup} only — never a credential.
The backup must belong to the app AND to the environment being restored into (ADR-0067 §1). Environments have separate instances holding separate data, so a dump taken from one is not a valid source for another: restoring staging's dump into production would overwrite live production data with staging's, which is the same class of incident as issue #339 pointed the other way.
func (*Engine) RestoreInstance ¶
func (e *Engine) RestoreInstance(ctx context.Context, t AddonType, env string, opts RestoreInstanceOptions) (RestoreInstanceResult, error)
RestoreInstance rewinds one environment's WHOLE Postgres instance to a point in its object-storage repository (ADR-0066 §4).
THE SEQUENCE IS THE FEATURE, and each step exists because the one before it can fail:
- The recovery target is resolved and, for a named backup, checked to be a PHYSICAL backup of THIS environment that actually reached the store. A logical dump is refused by name — it is the other command's input, and the two refusals now point at each other.
- Every app on the instance is enumerated BY NAME, from the apps' own Secrets rather than from the database, so the blast radius is known even when the instance is the thing that is gone. An unreadable answer aborts: a rewind must not be confirmed against an unknown blast radius.
- The guardrail is evaluated with those names in the message. "This is destructive" is not consent; "this rewinds the databases of api, web and worker" is (ADR-0064 §3).
- A PHYSICAL BACKUP OF THE CURRENT STATE IS TAKEN FIRST, and a failure aborts the restore with nothing touched. This is ADR-0064 §5's ordering — nothing is destroyed until a copy is known to exist — applied to the one operation that destroys a database on purpose. It is also the answer to "what is the fate of the old one": its bytes go into the same repository as a backup this restore names, so a rewind to the wrong point is itself recoverable.
- The seam replaces the instance and waits for it to serve.
- Every app is re-provisioned onto the recovered instance and restarted, through the same seam `addon attach` writes. The roles in a recovered instance are as they were at the recovery target, so an app's old password may simply not exist any more; re-provisioning rotates it and hands the app a URL that works.
It moves no secret value: the repository credential is assembled at call time and handed only to the seam, the regenerated connection strings go only to SetSecretValue, and the audit row records the add-on, environment, instance, target and affected app NAMES — never a credential and never a URL.
func (*Engine) RetainedAddonVolumes ¶
func (e *Engine) RetainedAddonVolumes(ctx context.Context) ([]AddonVolume, error)
RetainedAddonVolumes returns the add-on volumes no installed add-on owns — the claims an earlier `addon remove` deliberately left behind (ADR-0064 §1), reported so reclaiming them is a decision a user can make rather than an archaeology exercise in `kubectl get pvc` (ADR-0064 §6).
The registry says what is installed; the cluster says what claims exist. A retained volume is the difference: a claim Burrow created for an add-on that is no longer installed. Deriving it from the cluster rather than from registry rows is the load-bearing part — a removed add-on has no registry row at all, which is precisely why its volume was invisible.
Nothing here reclaims anything. An automatic reaper for the data ADR-0064 exists to protect would reintroduce the failure it prevents, by a slower route (ADR-0064 §Consequences).
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.
A rollback disables auto-deploy in the target environment (ADR-0052 §5): once landed it sets the app's level to off with the reason "disabled by rollback", so the pull-based watcher does not fight the deliberate downgrade by re-applying the version just backed away from. Re-enabling is a deliberate human action (`burrow app auto-deploy <app> <level>`).
func (*Engine) Run ¶ added in v0.12.0
Run executes a caller-provided one-off command inside the app's OWN current image and environment as a short-lived Kubernetes Job, waits for it to finish, and returns the captured output and exit code as a structured result (ADR-0048). It is the primitive behind migrations, seeds, backfills, and console-style tasks.
The command runs in the app's currently-deployed release image — resolved the same way deploy and rollback resolve it — with the app's config env and per-app Secret injected via envFrom, so the command sees exactly the runtime, dependencies, config, and secrets the running app sees (ADR-0048 §2). No secret value crosses the API or the agent control channel: the caller supplies only the command (ADR-0029). The operation is synchronous — launch, wait, capture, return (ADR-0048 §3): a non-zero exit is a normal structured outcome the agent reasons over, not a transport error.
It is gated by the app.run guardrail (confirm by default), which gates WHETHER the command runs, not what it does — the opaque-command limitation is honest and stated (ADR-0048 §5). A held run returns a *GuardrailError needing confirmation; the agent surfaces the exact command to the human and re-invokes with Confirm only on explicit approval — it never self-confirms (ADR-0020). Every run — held, denied, or executed — is recorded in the audit log with the command (ADR-0027).
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. It is bounded by the replica ceiling — an operational limit whose breach is a validation failure (ADR-0068 §2) — and guarded against scale-to-zero (ADR-0006). It does not create a new release: scaling adjusts the running workload, while a release records a deploy.
func (*Engine) SetAppChecks ¶
func (e *Engine) SetAppChecks(ctx context.Context, app, env string, enabled bool) (ChecksReport, error)
SetAppChecks turns the deploy-time dependency check on or off for one app and environment (ADR-0076's consequence that a Burrow-supplied default on the post-deploy path must be visible and DISABLEABLE rather than silent).
There is no guardrail on it, for the reason `health set` has none: it changes what Burrow observes, not what the app can reach, and it is reversible with the opposite call. Disabling it cannot break anything — the check never blocked a deploy — so the failure direction of the setting matches §6's posture as well.
func (*Engine) SetAppHealth ¶
func (e *Engine) SetAppHealth(ctx context.Context, app, env, path string, port int32) (HealthReport, error)
SetAppHealth records the health endpoint an app serves and re-applies the running workload so the probe reaches its pods (ADR-0076 §5). port may be zero, meaning "the port the app is published on", resolved on every apply.
There is no guardrail on it, for the reason `config set` has none: it changes what Burrow checks, not what the app can reach, and the change is reversible with `health unset`. Its blast radius is one app's pods being gated on one more condition — and §6's whole posture is that the condition fails toward deployed.
func (*Engine) SetAutoDeploy ¶ added in v0.13.0
SetAutoDeploy sets the auto-deploy level for app in env (ADR-0052 §2, §6). Choosing the level is a governance decision, so it is a human operator action exposed only through the `burrow` CLI and never to the agent — what deploys unattended stays a human decision (ADR-0038). It rejects an invalid level as ErrInvalid and an unknown or ambiguous environment like every other per-app mutation, and stores the level under the canonical environment name.
func (*Engine) SetConfig ¶ added in v0.7.0
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 `burrow` CLI's `guard set`; `burrow-agent` can only list guardrails, so the agent cannot change its own.
With an empty env, or with `prod`, it sets the GLOBAL disposition for code: `prod` is the environment install created and the baseline every other environment inherits, so `guard set --env prod app.delete deny` and `guard set app.delete deny` are deliberately the same write (ADR-0067 §2). An environment added later stores the env-prefixed code (e.g. staging.app.delete) so its policy can diverge from that baseline (ADR-0035 phase 2c) — which is the gradient ADR-0065 §3 expects, written as an opt-OUT of production's setting rather than a set of unrelated ones. Such an 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) SetHook ¶
func (e *Engine) SetHook(ctx context.Context, app, env string, phase HookPhase, command []string) (Hook, error)
SetHook configures the command app runs at phase in env, replacing any command already set there (ADR-0072 §1). It is one mechanism with the phase NAMED rather than a command per phase, so a fourth phase is a new value and not a new verb.
The blast radius is worth stating where it is set: a pre-deploy hook is configuration set once and forgotten, and a command that starts failing blocks every deploy of that app until someone notices (ADR-0072's consequences). That is the correct failure direction and it is still a new way for an app to become undeployable.
func (*Engine) SetLimit ¶
SetLimit sets one operational limit's value (ADR-0068). It rejects an unknown limit, or a value that is not of the limit's kind or lies outside its permitted range, as ErrInvalid. This is the operator's lever and it is on the operator CLI only — `burrow-agent` carries no `cluster config`, because a bound the agent can raise is not a bound (ADR-0068 §4).
With an empty env, or with `prod`, it sets the CLUSTER value, for the reason SetGuardrail does the same: `prod` is the environment install created and the baseline every other environment inherits (ADR-0067 §2). An environment added later stores the env-prefixed code (e.g. staging.app.replica_ceiling) so its bound can diverge from that baseline; such an environment must be registered, and only a limit that DECLARES itself environment-scoped may be set that way (ADR-0068 §5).
The value is normalized to the limit's canonical text form before it is stored, so `72h0m0s` and `72h` do not read back as two different settings.
func (*Engine) SetSecret ¶
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 the agent control channel — only its KEY name appears in any error (the value is never formatted into one). Setting a value stays a human operation: `burrow-agent` has no secret-set command, so the agent cannot supply a value. An app with no running workload just writes the Secret; the change lands on the next deploy.
func (*Engine) Status ¶
recorded release and the live workload state. It returns ErrNotFound only when the app is unknown to both.
func (*Engine) Unexpose ¶
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) UnsetAppHealth ¶
UnsetAppHealth removes an app's declared endpoint, returning it to ADR-0076 §3's default — a TCP check on the published port, or no probe at all when the app is not published — and re-applies the running workload so the change reaches its pods. Unsetting an endpoint that was never declared is not an error: it is the state the app is already in.
func (*Engine) UnsetConfig ¶ added in v0.7.0
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) UnsetHook ¶
UnsetHook removes app's hook at phase in env. Unsetting a phase with no hook is a no-op, not an error — the state it asks for is the state that already holds — and afterwards that phase runs nothing, which is today's behaviour exactly (ADR-0072 §1).
func (*Engine) UnsetSecret ¶
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, so it is on the agent surface (`burrow-agent secret unset`). 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. "prod", "staging").
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 default environment — `prod`, created at install and
// mapped to the app namespace burrowd runs against (ADR-0067 §2–§3). Environments added later
// are never default.
Default bool `json:"default"`
// CreatedAt is when the environment was registered, read from the injected clock. It is the
// zero time when the default environment is synthesized because its registry row is missing.
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 environment install created (`prod`), the one an operation naming none resolves to (ADR-0067 §2).
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
// "prod" targets the default environment, a name added later 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 Exposure ¶
type Exposure struct {
// App is the exposed application.
App string `json:"app"`
// Environment is the canonical environment the exposure was made in ("prod" for the default
// one), because the same app in two environments is exposed at two different hosts.
Environment string `json:"environment,omitempty"`
// Host is the external hostname routed to the app.
Host string `json:"host"`
// Port is the app's container port the Service forwards to.
Port int32 `json:"port"`
// TLS records whether a certificate was requested for Host, which is what makes "the Ingress is
// there but the certificate never issued" a failure Burrow can name rather than guess at.
TLS bool `json:"tls,omitempty"`
// CreatedAt is when the exposure was recorded, read from the injected clock.
CreatedAt time.Time `json:"created_at,omitempty"`
}
Exposure is the registry's record that an app was made reachable at a host: what Burrow was told to route, kept alongside the releases and add-ons it already records.
It exists because ADR-0074 §6 asks a question that cannot be answered from the cluster: an exposure whose Ingress is gone looks, from the cluster side, like an app that was never exposed. The Ingress itself carried the whole of that intent until now, so deleting it deleted the evidence that it was ever supposed to be there. Whether the exposure is currently WORKING stays a live read (Reachability, ExposureStatus) — this row records only what was asked for, in keeping with the split ADR-0074 §1 draws between intent and observed state.
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 Failure ¶
type Failure struct {
// ID is the store-assigned row identity (0 before it is persisted).
ID int64 `json:"id,omitempty"`
// Object is what failed.
Object ObjectRef `json:"object"`
// Reason is the member of the closed ledger vocabulary this row is about.
Reason string `json:"reason"`
// Detail is the most recent bounded, Burrow-authored line for this failure. It is overwritten
// on each sighting rather than accumulated: the row is a transition, not a log.
Detail string `json:"detail,omitempty"`
// FirstSeen is when this failure began — the answer to "when did it start", and the reason the
// ledger exists at all.
FirstSeen time.Time `json:"first_seen"`
// LastSeen is when it was last observed — the answer to "is it still happening".
LastSeen time.Time `json:"last_seen"`
// ResolvedAt is when the failure stopped being observed, nil while it is still active — the
// answer to "did it recover on its own". It is a pointer so an active row serialises without a
// zero timestamp that reads as a date in the year 1.
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
// Occurrences is how many OBSERVATIONS found this failure present, counting the one that opened
// the row. It is deliberately a count of sightings rather than of restarts: a restart count
// exists for a crash loop and for nothing else in the vocabulary, and a field that meant
// different things for different reasons could not be compared across rows. Read with the
// observation cadence, it is what says whether a failure is a blip or a standing condition.
Occurrences int64 `json:"occurrences"`
}
Failure is one ledger row: a failure with a lifetime rather than an event (ADR-0074 §4). An event stream has higher fidelity and is unreadable at the moment it matters — an incident produces thousands of rows describing one problem — so the shape here answers "when did it start" and "is it still happening" in a single row.
type FailureFilter ¶
type FailureFilter struct {
// Kind restricts to one class of object; empty matches any.
Kind FailureKind
// Name restricts to one object name; empty matches any.
Name string
// Environment restricts to one environment; empty matches any.
Environment string
// Reason restricts to one reason; empty matches any. It is the filter that answers "what else
// hit this in the same window", which is §5's grouping seen from the query side.
Reason string
// Since restricts to rows last seen at or after this instant; the zero value applies no bound.
Since time.Time
// IncludeResolved widens the result from active failures to the history as well.
IncludeResolved bool
// Limit caps the number of rows returned. Zero or negative applies the store's default cap.
Limit int
}
FailureFilter narrows a ledger query. A zero value matches every ACTIVE row (subject to Limit) — the default a reader wants during an incident, which is "what is broken", not "what ever broke".
type FailureKey ¶
FailureKey is one ledger row's identity: the object AND the reason (ADR-0074 §5). Keying on both is what lets one pod be OOM-killed and unschedulable at once without either being lost, and what makes a pod alternating between two reasons legible as the one bug it is. A single status field per object would silently drop the second, which is the failure this record exists to stop.
type FailureKind ¶
type FailureKind string
FailureKind names the class of managed object a ledger row is about. It is part of the row's identity, not decoration: an app and an add-on may share a name, and a reason on one says nothing about the other.
const ( // FailureApp is a deployed application — the workload behind a release the registry records. FailureApp FailureKind = "app" // FailureAddon is an installed building-block add-on (ADR-0025). FailureAddon FailureKind = "addon" // FailureBackup is a recorded backup (ADR-0032), identified by its backup id. FailureBackup FailureKind = "backup" // FailureExposure is an app's recorded exposure — the Ingress and certificate behind a host // (ADR-0018). FailureExposure FailureKind = "exposure" )
func (FailureKind) Valid ¶
func (k FailureKind) Valid() bool
Valid reports whether k is a known FailureKind.
type FailureObservation ¶
type FailureObservation struct {
// Object is what failed.
Object ObjectRef
// Reason is a member of the closed ledger vocabulary (LedgerReasons).
Reason string
// Detail is one bounded, Burrow-authored line — already through LedgerDetail.
Detail string
// At is the observation time, from the injected clock (ADR-0010).
At time.Time
}
FailureObservation is one sighting of one failure, handed to the store to open or extend a row. It carries no lifetime of its own: whether this is a new failure or the four-hundredth sighting of a standing one is the store's decision, made against what is already there.
type FailureQuery ¶
type FailureQuery struct {
// Kind restricts to one class of managed object; empty matches any.
Kind FailureKind
// Name restricts to one object name; empty matches any.
Name string
// Environment restricts to one environment; empty matches any.
Environment string
// Reason restricts to one reason from the closed ledger vocabulary; empty matches any. It is
// ADR-0074 §5's grouping asked from the query side: "what else hit this in the same window".
Reason string
// Since bounds the answer to failures last seen within this much of now, and widens it from the
// active failures to the history — a window over the past is not a question about the present.
// Zero leaves the query on the active rows and reads coverage over DefaultFailureLookback.
Since time.Duration
// IncludeResolved widens the answer to failures that have since recovered, with no time bound
// beyond the ledger's retention.
IncludeResolved bool
// Limit caps the rows returned. Zero or negative applies the store's default cap.
Limit int
}
FailureQuery is the read side's request. It differs from FailureFilter in one way that matters: Since is a DURATION rather than an instant, resolved against the control plane's own injected clock. The ledger's timestamps were written by that clock, so a client resolving "the last hour" against its own would query a window skewed by however wrong its clock is — and the caller most likely to be wrong about the time is an agent on a laptop that just woke from sleep.
type FailureReport ¶
type FailureReport struct {
// Failures are the ledger rows, oldest first — the order ADR-0074 §5 asks for, because the
// earliest first_seen in a cascade is the likeliest thing to actually fix. They are rows and not
// groups on purpose: see the file comment.
Failures []Failure `json:"failures"`
// Coverage is what was watching over the period described. It is never omitted.
Coverage Coverage `json:"coverage"`
}
FailureReport is one answer from the ledger: the rows, and what the observer was doing while they were (or were not) being recorded.
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" // 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). Denied by default // (ADR-0065 §3): removing the record takes an application off the internet, and the record may // not be one Burrow created, so a confirmation the caller can satisfy itself is too weak a // control. An operator who wants the agent tidying up DNS sets it to confirm or allow with // `guard set dns.delete ...` — cluster-wide, because a DNS operation carries no environment to // scope the disposition to: AddDomain and RemoveDomain act on a hostname at a vendor, and // nothing in the request says which environment it belongs to. Its declaration below says so // (ADR-0068 §5): scoping it is a matter of the operation carrying an environment, not of the // code's name. 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. // (Backup and list are not guarded: they destroy nothing.) GuardrailAddonRestore GuardrailCode = "addon.restore" // GuardrailAddonRestoreInstance: the operation would rewind a whole Postgres instance to a point // in its object-storage repository, taking every app's database on it back together (ADR-0066 // §4). Held for confirmation by default. // // IT IS A SEPARATE CODE FROM addon.restore, and the separation is the point ADR-0064's // "deliberately left open" section raised about `--delete-data` sharing `addon.remove`'s // disposition. The two restores have materially different blast radii — one app's database against // every app's — so an operator who wants per-app restore available and instance-wide rewinds // denied has to be able to say so. Sharing one disposition would have made that unexpressable. // // The disposition is not the boundary here, which is worth stating so it is never mistaken for // one: `addon restore-instance` is not compiled into `burrow-agent` at all (ADR-0065 §2 tier 1), // and this code exists so the hold is legible and configurable for the human who can run it. GuardrailAddonRestoreInstance GuardrailCode = "addon.restore_instance" // 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. Denied by default (ADR-0065 §3): destroying the release history // leaves nothing to roll back to, so a confirmation protects only an attentive reader. The // deny is a floor, not a fixed setting — app.delete is env-scopable, and the expected shape is // a gradient set with `guard set --env <env> app.delete ...`: allow where the agent should tidy // up after itself, confirm in staging, deny in production. 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 // (LimitReplicaCeiling), which is an operational limit rather than a guardrail (ADR-0068 §2). // 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" // GuardrailAppRun: the operation would run a caller-provided one-off command inside the app's own // current image and environment (ADR-0048) — a migration, seed, backfill, or maintenance script. // Held for confirmation by default: a command runs opaquely and may make destructive changes, so // the human sees and approves the exact command before it runs, and prod is the environment to // keep gated. The guardrail gates whether the command runs, not what it does (ADR-0048 §5). GuardrailAppRun GuardrailCode = "app.run" // GuardrailBucketCreate: the operation would create a bucket at an object-storage provider // (ADR-0063 §5). Held for confirmation by default, which is ADR-0065's THIRD tier: creating a // bucket is additive, reversible, and part of a legitimate workflow, but it costs money at a // vendor, so a human approves it. // // Its counterpart is deliberately absent rather than guarded. Bucket DELETION is not a // guardrail code, because it is not an operation Burrow performs at all: its blast radius is // every backup the platform holds, and a bucket name lives in a GLOBAL namespace, so a mistaken // argument could reach outside the cluster entirely — ADR-0065's tier 1, where the worst case is // unbounded rather than merely bad. Deleting a bucket happens at the vendor, by a human. GuardrailBucketCreate GuardrailCode = "bucket.create" )
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 `burrow-agent`'s read-only `guard` command (ADR-0020).
type HealthEndpoint ¶
type HealthEndpoint struct {
// App is the application the endpoint belongs to.
App string `json:"app"`
// Environment is the canonical environment name ("prod" for the default one).
Environment string `json:"environment,omitempty"`
// Path is the HTTP path the app serves its readiness answer on, e.g. "/healthz". Empty means no
// endpoint was declared and §3's default applies.
Path string `json:"path,omitempty"`
// Port is the container port the path is served on. Zero means "the port the app is published
// on", resolved at apply time from the recorded exposure — so an app that moves its exposure
// does not need its health endpoint re-declared.
Port int32 `json:"port,omitempty"`
// UpdatedAt is when the endpoint was recorded, read from the injected clock.
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
HealthEndpoint is the health endpoint a user or their agent DECLARED for an app in one environment (ADR-0076 §5) — the opt-in that turns Burrow's conservative TCP default into an HTTP check against a real endpoint. It is intent, recorded by `burrow app health set`, and the zero value means nothing was declared, which is the state every app starts in.
It is keyed per (app, environment) like an exposure and an auto-deploy level are: the same app in two environments can serve on two different ports, and an operator may want the production probe to differ from the development one.
func (HealthEndpoint) Declared ¶
func (h HealthEndpoint) Declared() bool
Declared reports whether an endpoint was actually declared.
type HealthReport ¶
type HealthReport struct {
App string `json:"app"`
Environment string `json:"environment,omitempty"`
// Path and Port are the DECLARED endpoint, empty and zero when none was declared.
Path string `json:"path,omitempty"`
Port int32 `json:"port,omitempty"`
// Probe is what Burrow authors: "http", "tcp", or "none".
Probe string `json:"probe"`
// ProbePort is the port the probe checks; zero when Probe is "none".
ProbePort int32 `json:"probe_port,omitempty"`
// ProbePath is the path an HTTP probe requests; empty for a TCP or absent probe.
ProbePath string `json:"probe_path,omitempty"`
// Source is where the probe came from: "endpoint" (declared), "exposure" (§3's TCP default off
// the published port), or "none".
Source string `json:"source"`
// Liveness is always false and is reported anyway, because "does Burrow restart my container
// when this fails?" is the first question a reader has and the answer is load-bearing (§1).
Liveness bool `json:"liveness"`
// Hint is the §5 guidance, present whenever no endpoint has been declared. It is what turns this
// from a status read into the place an agent learns that adding one is worth doing and what a
// good one checks.
Hint string `json:"hint,omitempty"`
// AppliesOn says when the reported probe reaches the running pods, when it is not already there.
AppliesOn string `json:"applies_on,omitempty"`
}
HealthReport is what the health surface answers on a read or a write: what the user declared, what Burrow actually authors on the container as a result, and where that came from.
The resolved half is there because the declared half is not the question an agent is asking. "I set a path" and "my app is being probed" are different facts — an endpoint declared before the app was published resolves to no probe — and a surface that reported only the intent would let that gap sit unnoticed.
func NewHealthReport ¶
func NewHealthReport(app, env string, ep HealthEndpoint, exposedPort int32, applied bool) HealthReport
NewHealthReport builds the answer for one app from its declared endpoint and its published port. applied says whether the running workload already carries the resolved probe; when it does not, the report names what makes it land, because a user who just set a path and saw nothing change would otherwise assume the setting did not take.
type Hook ¶
type Hook struct {
App string `json:"app"`
Environment string `json:"environment"`
Phase HookPhase `json:"phase"`
Command []string `json:"command"`
}
Hook is one configured lifecycle command: the phase it fires at and the command it runs, for one app in one environment. Command is an argv, so a command's argument boundaries survive storage rather than depending on a shell to re-split them.
type HookError ¶
type HookError struct {
App string
Env string
Phase HookPhase
Command []string
// Image is the image the hook ran from: the one being deployed for a pre-deploy hook, the one
// being left for a pre-rollback hook (ADR-0072 §2, §8).
Image string
// ExitCode is the command's exit code when it ran and failed. Zero when the command never
// produced one (Cause is set instead).
ExitCode int
// Output is the command's captured combined output, bounded by hookOutputLimit.
Output string
// TimedOut reports the hook did not finish inside the run window and Burrow stopped waiting.
TimedOut bool
// Cause is the launch, poll, or timeout failure when the hook never ran to an exit code. Nil
// when the command ran and exited non-zero, which is the ordinary failure.
Cause error
}
HookError reports that a lifecycle hook did not succeed, and therefore that the operation it ran ahead of did not happen (ADR-0072 §3). It is a structured outcome, not a system failure: the request was understood, the guardrail allowed it, and the user's own command said no. Callers distinguish it with AsHook.
It carries the command's captured output so the failure is diagnosable from the response rather than from a hunt through the cluster — the point of §3's "reported as the deploy's failure, with the command's output". Summary is the same failure WITHOUT the output, and it is what reaches the audit log: a stored row must carry the phase, the command, and the exit code, and never whatever the command happened to print.
func AsHook ¶
AsHook reports whether err is (or wraps) a HookError and returns it, mirroring AsGuardrail so a front end (the HTTP API, and through it the `burrow` CLI and `burrow-agent`) can surface the structured failure without parsing prose.
func (*HookError) Error ¶
Error is the summary plus the command's captured output and what Burrow did about it — the whole answer the caller who asked for the deploy needs.
What Burrow did about it depends on the phase, and the two cases are opposite. A `pre` hook's failure ABORTED the operation, so the message says the operation did not happen. A `post` hook's failure aborted nothing — the deploy it was reporting on was already live and recorded — so the message says exactly that, because a reader who saw "the deploy did not happen" after a deploy that plainly did would trust the surface less, not more.
type HookPhase ¶
type HookPhase string
HookPhase names when a hook runs. The value is the phase's name exactly as an operator types it (`--on pre-deploy`), so the configured value and the documented moment are one string.
const ( // HookPreDeploy runs from the image BEING DEPLOYED, before traffic moves, on every deploy path — // an explicit `burrow app deploy`, a build that ends in a deploy, and an unattended auto-deploy // alike (ADR-0072 §2). Its failure aborts the deploy (§3). HookPreDeploy HookPhase = "pre-deploy" // HookPostDeploy runs AFTER THE ROLLOUT SETTLES, from the image that was deployed, and it runs // whether the rollout succeeded or failed (ADR-0072 §4). A post hook that fired only on success // could not report the case it exists for: the failure an unattended 3am push actually produces // is a crashloop, not a failed migration. // // It fires after a ROLLBACK too, told that the deploy it is reporting on was one. "Did this // settle and is it serving?" is the same question whichever direction the image moved, and a // separate `post-rollback` phase would be a fourth name for an identical answer. // // Its failure changes nothing. The deploy already happened, so there is nothing to abort — see // runPostDeployHook. HookPostDeploy HookPhase = "post-deploy" // HookPreRollback runs from the image being rolled back FROM, before traffic moves back // (ADR-0072 §8). Unset means nothing runs, which is the safe default: a team practising // expand/contract migrates forward only and would be harmed by anything running on a rollback. HookPreRollback HookPhase = "pre-rollback" )
func HookPhases ¶
func HookPhases() []HookPhase
HookPhases returns every phase a hook may be set on, so a caller can name the set without reaching into the catalogue.
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 InstallAddonOptions ¶
type InstallAddonOptions struct {
// Confirm satisfies the addon.install guardrail's confirmation hold (ADR-0020).
Confirm bool
// ArchiveDestination names which registered object-storage provider a Postgres instance archives
// its write-ahead log and takes its base backups to (ADR-0066 §3). It is only needed when several
// are registered — ADR-0063 §6 allows that on purpose — and Burrow refuses to guess rather than
// tying an instance to a repository nobody is watching, because an instance keeps the repository
// it was created against. Empty with exactly one registered provider means that one; empty with
// none means the instance does not archive at all, which is not an error.
ArchiveDestination string
}
InstallAddonOptions is everything `addon install` needs beyond the add-on's type and its environment.
The environment stays a positional argument rather than moving in here, deliberately. ADR-0067 §1 requires every add-on operation to name the environment it acts on — "a signature that can omit it is a signature that will omit it" — and a field on an options struct is exactly the kind of thing a caller omits.
type IssueEvidence ¶
type IssueEvidence struct {
// Reason is the member of the closed set this evidence is for. Empty means no issue.
Reason string
// Container is the container the evidence came from, when the reason is a container's.
Container string
// Image is the container image, for an image-pull reason.
Image string
// Detail is the reason-specific evidence: the scheduler's message for a scheduling or volume
// failure, the kubelet's message for a config error, the memory limit for an OOM kill.
Detail string
// ExitCode is the code the previous run of a crash-looping container exited with. A crash loop
// with code 0 is a real and confusing case (a container that keeps exiting cleanly under
// restartPolicy Always), so it is reported as-is rather than treated as "unknown".
ExitCode int32
// LogTail is a bounded tail of a crash-looping container's PREVIOUS log — application output,
// captured verbatim. Message labels it as such; Burrow does not inspect or sanitise it.
LogTail string
}
IssueEvidence is the raw, cluster-supplied evidence behind one Issue, in a form the core package can turn into prose. It is the seam that keeps the real adapter and the fake honest with each other: the adapter fills it from a pod (or a Deployment condition) and the fake takes it directly from a test, and both call Message, so there is exactly one place the wording lives.
Fields other than Reason are optional and reason-specific; a missing one degrades the message rather than breaking it, because status enrichment is best-effort by contract and must never be the thing that fails a status call.
func (IssueEvidence) Message ¶
func (e IssueEvidence) Message() string
Message renders the actionable explanation for this evidence. An unknown or empty Reason returns "", so a caller that forgot to check IsIssueReason produces no Issue rather than a misleading one.
type JobBlockedError ¶
type JobBlockedError struct {
// Job is the Job's name, so a reader can go straight to it (a blocked Job is deliberately left
// undeleted for diagnosis).
Job string
// Reason is the member of the closed set. ReasonDeadlineExceeded is the backstop: the waiter ran
// out of time without any pod reporting a blocking condition.
Reason string
// Issue is the rendered, actionable explanation — the same prose the status surface shows.
Issue string
}
JobBlockedError is what a Job waiter returns instead of waiting out its deadline, when the Job's pod reports a blocking, human-fixable condition (issue #352). Before it, a Job whose pod could not start — a missing Secret, an unpullable image, no node with room — left Job.Status.Failed and .Succeeded both at zero, so a waiter polling only those two counters could not tell "still working" from "will never start" and reported a bare timeout minutes later. ADR-0074 names that shape: a waiter that burns its full deadline when the cluster was saying exactly what was wrong has converted a diagnosis into a shrug.
Reason is a member of the closed IssueReason set so a CALLER CAN BRANCH ON IT rather than parse the prose (ADR-0074 §5) — an agent that sees ReasonCreateContainerConfigError knows retrying is pointless, where a timeout string invites exactly that retry. Issue is IssueEvidence.Message()'s output, so the wording a user reads for a blocked Job is the wording they read for a blocked Deployment; there is one renderer, not two that can drift apart.
func (*JobBlockedError) Error ¶
func (e *JobBlockedError) Error() string
func (*JobBlockedError) TimedOut ¶
func (e *JobBlockedError) TimedOut() bool
TimedOut reports whether this error is the deadline backstop rather than an observed blocking condition. It is the one distinction a caller makes on the reason without knowing the whole set.
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)
// AwaitRollout blocks until app's newest revision has SETTLED — the completion test
// `kubectl rollout status` uses, not merely that the write was accepted — or until it can say why
// it did not, bounded by timeout (ADR-0072 §4-§5). It is what makes a `post-deploy` hook possible:
// a hook cannot be told how a deploy went until something waits for it to go.
//
// EVERY OBSERVABLE CONDITION IS AN OUTCOME, NOT AN ERROR, exactly as a non-zero exit is a result
// rather than an error on RunJob (ADR-0048 §3). A crash loop, an unschedulable pod, a missing
// workload and an expired deadline all come back as a RolloutOutcome with a reason from the closed
// vocabulary, because the caller's next move is the same in every case: tell the hook. The error
// return is reserved for a call that could not be made at all (an invalid app identifier).
//
// It returns as soon as it has a verdict rather than sleeping out its bound: a pod reporting a
// blocking condition ends the wait immediately, which is the difference between a hook told
// "CrashLoopBackOff" in fifteen seconds and one told "timed out" in five minutes (issue #352).
// An expired bound is the BACKSTOP, and its outcome carries what was observed rather than the
// elapsed time (ADR-0072 §5).
//
// It mutates nothing. A rollout it reports as failed is left exactly as it is, for the hook to
// decide about — Burrow does not roll back by itself (ADR-0072 §6).
AwaitRollout(ctx context.Context, app string, timeout time.Duration) (RolloutOutcome, error)
// DeployAddon installs a building-block backing service per spec for environment env — 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.
//
// env is required and names which environment's instance this is (ADR-0067 §1): the resources are
// named by AddonInstanceName, so the default environment lands on exactly the names an existing
// install already has and any other environment gets its own instance beside it — its own pod,
// its own volume, and for Postgres its own superuser credential.
//
// The add-on's TYPE decides what is written. Postgres is a CloudNativePG `Cluster` — one custom
// resource, from which the operator composes the workload, the volume and the services (ADR-0066
// §1) — and every other add-on is a Deployment Burrow authors. Which one it is follows from the
// spec; there is no mechanism to select.
//
// archive, when non-nil, is the pgBackRest repository this instance archives its write-ahead log
// and takes its base backups to (ADR-0066 §3). It applies to Postgres and is ignored by every
// other add-on. Passing it is what wires the plugin into the instance, so an instance created
// with a nil archive has no archiving at all and physical backups of it are refused by name; a
// later install with one wires it, because a destination registered after the instance was
// created is a normal sequence and not a reason to rebuild the database. Like BackupDestination
// it carries a credential PAIR and therefore never crosses an API boundary — the adapter puts it
// in a Secret the plugin's sidecar reads by reference and nowhere else.
DeployAddon(ctx context.Context, spec AddonSpec, env string, archive *ArchiveDestination) (AddonInfo, error)
// AddonReady reports whether the named add-on's backing workload is available. It is a cheap
// single-object readiness probe — readiness is a live property, not stored in the registry — and
// a missing workload is reported as not ready (false, nil), not an error.
//
// WHICH object it reads is resolved from the cluster rather than from the name, because an add-on
// instance is not always a Deployment: a Postgres instance has none at all, and its readiness is
// its CloudNativePG `Cluster`'s ready instance count (ADR-0066 §1). Reading only the Deployment
// would report every Postgres instance as not running, and the failure observer would then open
// an ADR-0074 §6 discrepancy row against a database that is serving perfectly well — a false
// absence, which is precisely the diagnosis §6 exists to make correctly.
AddonReady(ctx context.Context, name string) (bool, error)
// DeleteAddon tears down the named add-on's WORKLOAD — its Deployment, Service, collector, and
// generated config — and, only when deleteData is true, destroys its data volume as well.
// Keeping the volume by default is the load-bearing part: for the Postgres add-on that volume
// holds every attached app's database (ADR-0031), and a removal meant as "stop this and put it
// back" must not destroy it. A retained volume is left with whatever the add-on needs to be
// usable again after a reinstall. It returns what was torn down and what was deliberately kept,
// so the caller can report it. Removing an add-on that is not installed returns ErrNotFound.
//
// t is the add-on's TYPE, taken from the registry row rather than probed for, and it decides
// which teardown runs: Postgres is a CloudNativePG `Cluster` and everything else is a Deployment
// (ADR-0066 §1). A removal is the operation that must never guess — a Postgres instance has no
// Deployment, and probing for one and finding nothing is indistinguishable from a probe that was
// refused. Under CloudNativePG the claims belong to the OPERATOR and carry the `Cluster` as their
// owner, so keeping the data means disowning them before the `Cluster` is deleted rather than
// simply not deleting them — the same promise, a different act.
DeleteAddon(ctx context.Context, name string, t AddonType, deleteData bool) (AddonRemoval, error)
// AddonVolumes returns every PersistentVolumeClaim in the add-on namespace that Burrow created
// for an add-on, whether or not the add-on that owns it still exists. It reads the CLUSTER, not
// the registry, because that is the only place a volume outliving its add-on can be seen at all
// (ADR-0064 §6) — a removed add-on is no longer a registry row, but its claim is still there and
// still costing money. Deciding which of them are RETAINED is the engine's job. No claims is an
// empty slice, not an error.
AddonVolumes(ctx context.Context) ([]AddonVolume, 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 the agent control channel. 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
// BackupJobPresent reports whether the one-shot Job for the given backup id still exists in the
// add-on namespace. It is a READ, and it exists for one question the registry cannot answer on
// its own (ADR-0074 §6): a backup row left `pending` — by a burrowd that restarted mid-backup, or
// by a Job the cluster garbage-collected — is indistinguishable from a backup still running, and
// a backup that never completed is the sort of thing discovered at restore time unless something
// looks. A missing Job is reported as absent (false, nil), not an error.
BackupJobPresent(ctx context.Context, backupID string) (bool, error)
// RunBackupJob runs a one-shot Job in the add-on namespace that pg_dumps app's database on
// environment env's 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 that instance's 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. env and app are validated before any Job is built: a dump names the server
// it came from, so it is as environment-scoped as the database it reads (ADR-0067 §1).
//
// dest, when non-nil, adds the SECOND half of the Job: after the dump lands on the PVC, the same
// pod writes it to the object store and reads it back, retrying a write that did not complete
// (ADR-0063 §7). The Job then succeeds only if the object is there, which is what lets the
// caller record a completed backup without lying. dest carries a credential PAIR and therefore
// never crosses an API boundary — the adapter puts it in a Job-owned Secret and nowhere else.
//
// The returned BackupJobOutcome carries the dump's size in bytes (0 when unknown) and, on the
// error path, the closed reason the Job reported for its failure, so the caller can record WHY a
// backup failed rather than that it did. A non-nil error always accompanies a failure reason;
// the outcome is still returned so the caller does not have to parse the error to record it.
RunBackupJob(ctx context.Context, app, env, backupID string, dest *BackupDestination) (BackupJobOutcome, error)
// RunRestoreJob runs a one-shot Job in the add-on namespace that pg_restores
// /<backup-pvc>/<app>/<backupID>.dump into app's database on environment env's instance (--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. env and app are validated before any Job is built — restoring is the one
// operation where reaching the wrong environment's server overwrites live data with another
// environment's.
RunRestoreJob(ctx context.Context, app, env, backupID string) error
// RunPhysicalBackup asks CloudNativePG for a base backup of environment env's whole instance and
// waits for the answer (ADR-0066 §2). It CREATES A CUSTOM RESOURCE and reads `.status`: a
// `postgresql.cnpg.io/v1 Backup` with method `plugin`, handled by the pgBackRest plugin. Burrow
// runs no backup tool, handles no superuser credential on this path, and constructs no Job — the
// operator does the work and this seam reports what it said.
//
// It is refused, before any object is written, on an instance whose `Cluster` carries no
// pgBackRest plugin: there is nowhere for a base backup to go, and a `Backup` object created
// against it would sit in `pending` until the timeout rather than saying so.
//
// archive is the destination the caller resolved, and it is CHECKED against the instance's own
// `Stanza` rather than trusted: the two can differ, and a backup verified against the wrong bucket
// would be recorded as unreadable when it succeeded. The returned ObjectKey is derived from the
// stanza, so it addresses where this instance actually writes.
//
// The returned outcome carries pgBackRest's own backup LABEL on success — the name the repository
// knows this backup by, and the only handle a later restore has — and on failure the closed
// reason. A `Backup` object that failed and an archive that is not reaching the store are
// different reasons, because they are different problems (ADR-0063 §7). A non-nil error always
// accompanies a failure reason; the outcome is returned either way so the caller records WHY
// without parsing the error.
RunPhysicalBackup(ctx context.Context, env, backupID string, archive *ArchiveDestination) (PhysicalBackupOutcome, error)
// PhysicalBackupPresent reports whether the `Backup` object for the given backup id still exists
// in the add-on namespace — BackupJobPresent's question for a physical backup, and it exists for
// the same reason (ADR-0074 §6): a row left `pending` by a burrowd that restarted mid-backup is
// otherwise indistinguishable from a backup still running. A missing object is absent (false,
// nil), not an error.
PhysicalBackupPresent(ctx context.Context, backupID string) (bool, error)
// RestoreInstance rewinds environment req.Environment's whole Postgres instance to a point in its
// pgBackRest repository and waits until the recovered instance is serving (ADR-0066 §4).
//
// IT REPLACES THE INSTANCE UNDER ITS OWN NAME. CloudNativePG never recovers in place — a recovery
// bootstraps a new `Cluster` — but the name every consumer of a Postgres instance resolves
// (AddonInstanceName: the Service, the superuser Secret, every DATABASE_URL) is not a detail a
// restore may move. So the pre-restore `Cluster` and its data claims are removed and the recovered
// one comes up under the same name, rather than a differently-named instance being stood up beside
// it and every derivation in Burrow being asked to follow.
//
// THE CALLER HAS ALREADY TAKEN A BACKUP OF WHAT THIS DESTROYS, and this seam does not check that —
// it is the engine's ordering (ADR-0064 §5) and the seam's job is the replacement. What the seam
// does refuse is a recovery it cannot address: an instance with no `Stanza`, or one whose stanza
// describes a repository other than req.Archive, because recovering from a bucket the instance
// never wrote to produces an empty database rather than an error.
//
// req carries a credential PAIR and therefore never crosses an API boundary. The archived
// write-ahead log and every base backup in the repository are left untouched: this reads from the
// repository and writes nothing to it.
RestoreInstance(ctx context.Context, req RestoreInstanceRequest) (RestoreInstanceOutcome, error)
// RunJob runs spec.Command as a one-shot Job in the app namespace (this seam view's namespace),
// built from the app's own current image (spec.Image) and its config env plus per-app Secret via
// envFrom, so DATABASE_URL and every secret resolve as the app sees them (ADR-0048 §2). It blocks
// until the Job finishes, then captures the pod's output and the container's exit code into a
// RunResult and returns it. A non-zero exit is a NORMAL structured outcome, not an error: the
// error return is reserved for a launch, poll, or timeout failure (ADR-0048 §3). The finished Job
// is garbage-collected by Kubernetes' native ttlSecondsAfterFinished, set from spec.TTLSeconds, so
// there is no imperative reap (ADR-0048 §7). spec.App is validated before any Job is built.
RunJob(ctx context.Context, spec RunSpec) (RunResult, 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 LedgerPruneResult ¶
type LedgerPruneResult struct {
// Failures is how many resolved failure rows were removed.
Failures int64 `json:"failures"`
// Windows is how many observation windows were removed.
Windows int64 `json:"windows"`
}
LedgerPruneResult is what a retention pass removed. It is returned rather than logged inside the store so the caller decides whether a prune is worth a line, and a test can assert the bound is actually enforced rather than merely configured.
type LifecycleCheck ¶
type LifecycleCheck struct {
Status LifecycleStatus `json:"status"`
// Detail explains the verdict in one line. For an unknown status it says WHY the configuration
// could not be read, so nothing reads as "checked and fine".
Detail string `json:"detail"`
// Rule is the identifier of the rule that conflicts, when one does.
Rule string `json:"rule,omitempty"`
// Backup is the identifier of a retained backup the rule would already have expired, when
// there is one. It is the concrete half of the refusal: not "this could be a problem" but
// "this backup would be gone".
Backup string `json:"backup,omitempty"`
}
LifecycleCheck is what the reconciliation of ADR-0063 §3 observed: the verdict, the offending rule and backup when there is one, and a sentence a human can act on.
type LifecycleRule ¶
type LifecycleRule struct {
// ID is the rule's identifier at the vendor, quoted back in a refusal so the operator can find
// the rule they have to change.
ID string `json:"id,omitempty"`
// Prefix is the key prefix the rule applies to; empty means the whole bucket.
Prefix string `json:"prefix,omitempty"`
// Enabled reports whether the rule is in force. A disabled rule deletes nothing and so cannot
// conflict with retention.
Enabled bool `json:"enabled"`
// ExpireAfterDays is how many days after creation the rule deletes an object, or 0 when the
// rule expires nothing by age (e.g. it only aborts incomplete multipart uploads).
ExpireAfterDays int `json:"expire_after_days,omitempty"`
}
LifecycleRule is one rule read from a bucket's lifecycle configuration — the vendor-side policy that decides when objects in the bucket are deleted (ADR-0063 §3). It carries only what Burrow reconciles against backup retention: whether the rule is in force, what it applies to, and how long an object survives under it.
type LifecycleStatus ¶
type LifecycleStatus string
LifecycleStatus is the outcome of reconciling a bucket's lifecycle rules against backup retention (ADR-0063 §3).
const ( // LifecycleOK means the rules were read and none of them expires an object a retained backup // still needs. LifecycleOK LifecycleStatus = "ok" // LifecycleConflict means a rule would expire objects sooner than retention needs them. It is // refused at configuration time, which is the only point where the failure is cheap: the // alternative is a backup set that lists fine and cannot be restored. LifecycleConflict LifecycleStatus = "conflict" // LifecycleUnknown means the lifecycle configuration could not be read, so the invariant is // NOT verified and is not reported as if it were. LifecycleUnknown LifecycleStatus = "unknown" )
type LimitCode ¶
type LimitCode string
LimitCode identifies an operational limit: it is both the key an operator sets the limit under and the machine-readable reason it appears in a refusal, so an agent can branch on the cause rather than parse prose (ADR-0068 §2).
const ( // LimitReplicaCeiling is the largest replica count a deploy, a scale, or an autoscaler's // maximum may ask for. Environment-scoped: a production ceiling and a development ceiling are // exactly the case that motivates limits at all (ADR-0068 §6). LimitReplicaCeiling LimitCode = "app.replica_ceiling" // LimitBuildJobRetention is how long a finished in-cluster build Job lingers before Kubernetes' // TTL-after-finished controller reaps it and its pods. Cluster-scoped: how long a build's logs // are worth keeping is a property of the cluster, not of the environment an image was built for // — and a build Job runs in one namespace of its own regardless of which environment the // resulting image is deployed to, so there is no environment to scope it by (ADR-0068 §6). LimitBuildJobRetention LimitCode = "build.job_retention" // LimitAddonMetricRetention is how long the metrics add-on keeps samples. Cluster-scoped: the // add-on is one instance per environment, but the retention is a storage-cost decision about // the cluster's disk, and ADR-0068 §1's test — whether a sensible answer can differ per // environment — does not obviously pull the other way for it (ADR-0068 §6). LimitAddonMetricRetention LimitCode = "addon.metric_retention" // LimitUnschedulableGrace is how long a pod must have been unschedulable before Burrow calls it // an Issue. Cluster-scoped, and it is the clearest case for that tier: how long a pod may sit // unschedulable before something is actually wrong is a property of the cluster's scheduler and // whether it has an autoscaler — a cluster that provisions a node in ninety seconds needs a // longer grace than one with fixed capacity — not of whether the workload is staging or // production. An operator who tuned it per environment would be encoding the same cluster fact // twice and would eventually disagree with themselves (ADR-0068 §6). // // It carries an obligation the other occupants do not: WHATEVER VALUE APPLIES HAS TO APPLY // EVERYWHERE THE SAME FACT IS JUDGED. The live status surface, the Job waiters, and ADR-0074's // failure ledger are all consumers of exactly this threshold, and a status surface that stayed // silent for thirty seconds while the ledger recorded a row at twenty would not merely disagree // on tuning — the two would hold different definitions of "failure", and an agent reading both // would get contradictory answers about one pod at one moment. LimitUnschedulableGrace LimitCode = "status.unschedulable_grace" // LimitDeploySettleTimeout is how long Burrow waits for a rollout to settle before it reports // what it observed to a `post-deploy` hook. ADR-0072 §5 puts it here by name rather than leaving // it a constant, and the reason is the one §5 gives: a rollout does not finish, it either // completes, fails, or hangs, so the wait needs a bound — and a bound is a decision a human makes // about their own cluster, not a number a program picks. // // Environment-scoped, on ADR-0068 §1's test — whether a sensible answer can differ per // environment. It plainly can: a production environment rolling twenty replicas over a // PodDisruptionBudget takes longer to settle than a development one rolling one, and an operator // who wants a quick verdict in staging should not have to accept it in production. // // A deploy waits on it whenever it has something to wait FOR: a `post-deploy` hook to tell // (ADR-0072 §4), or a derived dependency to check once the rollout is ready (ADR-0076 §4) — an // attached database or a published port. A deploy with neither returns exactly when it did before // hooks existed, and a deploy with BOTH waits once and hands the same observation to each, so this // bound is spent at most once per deploy however many things want the outcome (issue #407). // Raising this bound therefore does reach ordinary apps, and a caller's own bound has to outlast // it: see MaxDeployWait in apiwait.go, which is what `client` sizes itself from. LimitDeploySettleTimeout LimitCode = "deploy.settle_timeout" )
func LimitCodes ¶
func LimitCodes() []LimitCode
LimitCodes returns every known limit code in listing order, so a caller can name the set without reaching into the catalogue.
type LimitError ¶
type LimitError struct {
// Operation is the operation that was refused (e.g. "deploy", "scale").
Operation string
// Code is the machine-readable limit that was exceeded.
Code LimitCode
// Requested is the value the caller asked for.
Requested int32
// Limit is the effective bound the request exceeded.
Limit int32
// Scope is the tier the effective bound came from: "environment", "cluster", or "default".
Scope string
// Env is the environment the request targeted, when it named one.
Env string
// Message is a human-readable explanation, naming the limit, the tier it came from, and the
// operator command that changes it.
Message string
}
LimitError is returned when a request exceeds an operational limit (ADR-0068 §2). It is a structured VALIDATION failure, not a policy decision: unlike a GuardrailError there is no disposition behind it, nothing to relax, and no confirmation that opens it — the only way past it is a human raising the bound with the operator CLI. Callers distinguish it with AsLimit.
func AsLimit ¶
func AsLimit(err error) (*LimitError, bool)
AsLimit reports whether err is (or wraps) a LimitError and returns it.
func (*LimitError) Error ¶
func (e *LimitError) Error() string
type LimitInfo ¶
type LimitInfo struct {
Code LimitCode `json:"code"`
Value string `json:"value"`
Description string `json:"description"`
Kind LimitKind `json:"kind"`
// Scope reports which tier the effective value came from: "environment", "cluster", or
// "default" (the built-in). It is what tells an operator whether the value they are reading is
// one someone set or one nobody has.
Scope string `json:"scope"`
// EnvScoped reports whether a value may be set for one environment at all (ADR-0068 §5).
EnvScoped bool `json:"env_scoped"`
// Default is the built-in value, so a listing shows what the limit reverts to.
Default string `json:"default"`
}
LimitInfo describes an operational limit and its effective value, for inspection through `burrow cluster config list` (ADR-0068 §4).
type LimitKind ¶
type LimitKind string
LimitKind is the shape of a limit's value, which decides how it is parsed, formatted, and bounds-checked. Two kinds because ADR-0068 §6's occupants need two: the replica ceiling is a count, while build-Job retention, add-on metric retention, and the status thresholds are durations. Values are stored in their canonical TEXT form ("50", "72h"), so what an operator reads out of the table is what they typed.
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 MetricPoint ¶
MetricPoint is one sample in a time series: a timestamp and the metric's value at it. Value is a string for the same reason MetricSample.Value is — PromQL's exact numeric formatting (precision, NaN/Inf) is preserved rather than lost to a float round-trip. Time is the timestamp as the Prometheus API returns it, formatted the same way MetricSample.Time is.
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 MetricSeries ¶
type MetricSeries struct {
Labels map[string]string `json:"labels,omitempty"`
Points []MetricPoint `json:"points"`
}
MetricSeries is one labelled time series returned by a range query: a label set and its ordered points over the queried window. It is the time-series sibling of MetricSample, which carries a single instant sample.
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 MetricsRangeQuerier ¶
type MetricsRangeQuerier interface {
// QueryMetricsRange runs a PromQL range query against the Prometheus-API-compatible store
// reachable at endpoint (an in-cluster host:port) over [start, end] sampled every step, and
// returns one MetricSeries per matching series. token is a bearer credential for an
// authenticated backend; an empty token means unauthenticated and no Authorization header is
// sent.
QueryMetricsRange(ctx context.Context, endpoint, query, token string, start, end time.Time, step time.Duration) ([]MetricSeries, error)
}
MetricsRangeQuerier runs a PromQL range query against a Prometheus-API-compatible metrics store and returns time series over a window — the sibling of MetricsQuerier's instant query, for sparklines and area charts rather than a point-in-time read (ADR-0026). It is kept as a SEPARATE optional interface rather than folded into MetricsQuerier so an existing instant-only implementer stays valid unchanged: the engine type-asserts a selected metrics querier to it and errors cleanly (ErrNotImplemented) when a backend does not provide the range surface.
type MetricsServerCapability ¶ added in v0.13.0
type MetricsServerCapability struct {
Present bool `json:"present"`
}
MetricsServerCapability reports whether metrics-server is serving the Kubernetes Metrics API. Present is true when the metrics.k8s.io API group is served — i.e. a metrics-server (or a vendor's equivalent on k3s, GKE, or AKS) is registered — detected via API-group discovery, which needs no RBAC.
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, and through it the `burrow` CLI and `burrow-agent`) can surface the structured checklist without parsing prose.
func (*MissingPrerequisitesError) Error ¶ added in v0.8.0
func (e *MissingPrerequisitesError) Error() string
type NextTagResult ¶ added in v0.13.0
type NextTagResult struct {
App string `json:"app"`
Env string `json:"env"`
Current string `json:"current,omitempty"`
Next *NextTags `json:"next,omitempty"`
Note string `json:"note,omitempty"`
}
NextTagResult is the read-only suggestion of an app's next semver release tags in one environment (ADR-0052 §8). Current is the app's running tag; Next carries the suggested patch/minor/major tags when that tag is stable semver. When there is no running release or the current tag is not semver, Next is nil and Note carries a short human reason — this degrades gracefully rather than erroring (ADR-0040), matching how AutoDeployStatus reports Checked=false with a Note.
type NextTags ¶ added in v0.13.0
type NextTags struct {
Patch string `json:"patch"`
Minor string `json:"minor"`
Major string `json:"major"`
}
NextTags are the suggested next release tags after a current semver tag (ADR-0052 §8): the next patch, minor, and major version, each preserving the current tag's leading "v" style. They turn "please use semver" into concrete numbers the agent applies to its build.
type NodeAllocatable ¶ added in v0.13.0
NodeAllocatable is one node's schedulable capacity, read straight from its .status.allocatable (the Kubernetes API's own scheduling budget). CPUMillis is in milli-CPU (1000 = one core); MemBytes is bytes. It carries no live-usage figure — allocatable is what the scheduler divides among pod requests, and needs no metrics-server.
type NodeCapacity ¶ added in v0.13.0
type NodeCapacity struct {
Name string `json:"name,omitempty"`
Pods int `json:"pods"`
AllocCPUMillis int64 `json:"alloc_cpu_millis"`
UsedCPUMillis int64 `json:"committed_cpu_millis"`
FreeCPUMillis int64 `json:"free_cpu_millis"`
AllocMemBytes int64 `json:"alloc_mem_bytes"`
UsedMemBytes int64 `json:"committed_mem_bytes"`
FreeMemBytes int64 `json:"free_mem_bytes"`
}
NodeCapacity is the allocatable / committed / free-headroom breakdown for one node, or — when Name is empty in CapacityReport.Cluster — the cluster-wide total. Committed is the sum of the resource requests of the pods scheduled here; Free is Allocatable minus Committed, the room the scheduler still has to place a new pod. All CPU figures are milli-CPU, memory figures bytes.
type ObjectRef ¶
type ObjectRef struct {
Kind FailureKind `json:"kind"`
Name string `json:"name"`
Environment string `json:"environment,omitempty"`
}
ObjectRef identifies one object Burrow manages, stably enough that two observations a week apart land on the same ledger row. The environment is part of the identity for the same reason it is part of a release's: the same app name in staging and in production is two objects, and a failure on one is not a failure on the other (ADR-0067 §1).
type ObjectStore ¶
type ObjectStore interface {
// BucketExists reports whether the bucket is present and reachable with this credential. A
// bucket that exists but belongs to someone else is reported as absent-or-unreachable rather
// than present, since Burrow can do nothing with it either way.
BucketExists(ctx context.Context, bucket string) (bool, error)
// CreateBucket creates the bucket. The NAME is the engine's to choose (ADR-0063 §4: a readable
// prefix plus a random component, recorded in the providers row) — an implementation creates
// exactly the name it is given and never derives or adopts one.
CreateBucket(ctx context.Context, bucket string) error
// PutObject writes body at key. It exists for the configuration-time probe of ADR-0063 §2 and
// for the backup path that follows; it is not a general upload surface.
PutObject(ctx context.Context, bucket, key string, body []byte) error
// PutObjectStream writes size bytes read from body at key, without holding them in memory. It
// is PutObject's shape for the one payload that is not small — a pg_dump, which is routinely
// gigabytes and would OOM the container that buffered it.
//
// sha256Hex is the hex SHA-256 of exactly those bytes, computed by the caller before the read
// begins. It is not an optimisation: an S3-compatible endpoint validates the body against the
// hash it was signed with and REFUSES a transfer that does not match, so passing it is what
// makes "the store accepted the write" mean "the store accepted these bytes" rather than "the
// store accepted some bytes". A caller that cannot compute it cannot use this method.
//
// It reports whether the write was REFUSED (the endpoint answered, and said no) as opposed to
// failing to complete, because ADR-0063 §7 retries the second and must not retry the first: a
// revoked credential does not become a valid one by being asked again, and spending the retry
// budget on it delays the loud failure that is the whole point.
PutObjectStream(ctx context.Context, bucket, key string, body io.Reader, size int64, sha256Hex string) (refused bool, err error)
// StatObject returns the size of the object at key, or ErrNotFound when it is not there.
//
// It exists so a backup can be VERIFIED rather than assumed (ADR-0063 §7). A 200 on a write is
// the endpoint's word that it accepted the request; reading the object back afterwards is the
// separate fact that it can serve it, at the key Burrow recorded, at the length Burrow sent.
// Those come apart in practice — an eventually-consistent listing, a bucket policy that permits
// writes and not reads, a multipart upload that completed into nothing — and the difference is
// only ever discovered at restore time unless something looks.
StatObject(ctx context.Context, bucket, key string) (int64, error)
// DeleteObject removes key. Deleting an object that is already absent is a no-op, not an error,
// so the probe's cleanup is idempotent.
DeleteObject(ctx context.Context, bucket, key string) error
// LifecycleRules returns the bucket's lifecycle rules. NO rules is an empty slice and no error.
//
// A store that cannot ANSWER — the vendor does not implement the lifecycle API, or this
// credential is not permitted to read it — returns an error wrapping ErrLifecycleUnknown, and
// the engine reports the reconciliation as unknown rather than as verified (ADR-0063 §3: an
// unverifiable invariant reported as verified is worse than one reported as unknown). Any other
// error is a genuine failure.
LifecycleRules(ctx context.Context, bucket string) ([]LifecycleRule, error)
}
ObjectStore is the seam over ONE bucket's vendor, addressed by S3-compatible endpoint rather than by vendor name (ADR-0063 §1). Its surface is deliberately the whole of what ADR-0063 §2 permits Burrow to do with object storage and no more: create the bucket it will own, verify the destination works by writing and deleting a probe object, and read the lifecycle configuration it must reconcile against backup retention. There is no cp, no sync, no listing of arbitrary prefixes, no presigned URL, and no policy/IAM/replication surface — a capability enters here only when a Burrow feature requires it, never because the S3 API offers it.
It is an OPTIONAL seam: nil is allowed, and registering an object-storage provider errors cleanly (ErrNotImplemented) when it is not wired.
type ObjectStoreConfig ¶
type ObjectStoreConfig struct {
// Endpoint is the S3-compatible API endpoint that answers for this provider. The vendor is
// whoever answers it (ADR-0063 §1): Burrow maintains no vendor list.
Endpoint string `json:"endpoint"`
// Region is the region the request is signed for. Vendors that have no meaningful region still
// require one in the signature; "auto" or "us-east-1" is the usual answer.
Region string `json:"region,omitempty"`
// Bucket is the bucket Burrow writes to. It is RECORDED, never derived (ADR-0063 §4): Burrow
// only ever writes to the bucket it knows about, because inferring one by name is how a tool
// ends up writing into something that belonged to somebody else.
Bucket string `json:"bucket"`
// Created reports whether Burrow created this bucket, as opposed to being pointed at one that
// already existed.
Created bool `json:"created,omitempty"`
// AccessKeyIDKey is the burrow-credentials key holding the access key ID.
AccessKeyIDKey string `json:"access_key_id_key"`
// SecretAccessKeyKey is the burrow-credentials key holding the secret access key.
SecretAccessKeyKey string `json:"secret_access_key_key"`
// RetentionDays is how long backups written here must stay RESTORABLE, in days — the window
// ADR-0063 §3 reconciles the bucket's lifecycle rules against. Zero means no window has been
// declared, which today is the truthful description of Burrow's backups: nothing prunes them,
// so they are meant to be restorable indefinitely and ANY expiry rule will eventually delete
// one that is still retained.
RetentionDays int `json:"retention_days,omitempty"`
}
ObjectStoreConfig is the NON-SECRET half of an object-storage provider registration (ADR-0063 §1): where the bucket is, which bucket it is, and the NAMES of the two burrow-credentials keys that hold the credential pair. It is what lives in the `providers` row, so it can be inspected without reading a Secret at all.
The two key names are recorded rather than derived so the layout can change for a NEW provider without invalidating one already registered — the same reason ADR-0023 records a provider's single secret key instead of assuming it equals the provider name.
type ObjectStoreCredential ¶
ObjectStoreCredential is one S3-compatible credential: the pair an S3 API call is signed with (ADR-0063 §1). Both halves are SECRET VALUES — they are read from the burrow-credentials Secret at call time, handed straight to an adapter, and never logged, audited, returned in a response, or placed in an error.
func (ObjectStoreCredential) IsZero ¶
func (c ObjectStoreCredential) IsZero() bool
IsZero reports whether the credential carries neither half, so a caller can tell "not configured" from "configured with an empty value".
type ObjectStoreFactory ¶
type ObjectStoreFactory interface {
ObjectStore(endpoint, region string, cred ObjectStoreCredential) (ObjectStore, error)
}
ObjectStoreFactory builds an ObjectStore for an S3-compatible endpoint and credential pair (ADR-0063 §1) — the object-storage sibling of DNSFactory, and for the same reason: the engine reaches a vendor without importing its adapter, so production wires controlplane/objectstore and a test substitutes a fake. region is the S3 region the request is signed for; endpoint is the API endpoint that answers, and the vendor is whoever that is.
type ObservationWindow ¶
type ObservationWindow struct {
// ID is the store-assigned window identity (0 before it is persisted).
ID int64 `json:"id,omitempty"`
// StartedAt is when this run of the observer began.
StartedAt time.Time `json:"started_at"`
// Until is the end of the most recent sweep that COMPLETED its enumeration of what Burrow
// manages. A sweep that could not enumerate does not advance it, so an observer that is running
// but wedged stops claiming coverage instead of claiming it falsely.
Until time.Time `json:"until"`
// Sweeps is how many sweeps have advanced this window.
Sweeps int64 `json:"sweeps"`
// DegradedSweeps is how many of those could not read every object they set out to. Coverage in
// such a sweep is partial, and a window that reports some is saying its ledger rows may be
// incomplete for that stretch.
DegradedSweeps int64 `json:"degraded_sweeps,omitempty"`
// Detail is the most recent degradation note, bounded like a failure detail. Empty on a window
// whose sweeps all completed.
Detail string `json:"detail,omitempty"`
}
ObservationWindow is one continuous run of the observer: when it started, how far its completed sweeps have carried coverage, and how many of those sweeps were incomplete.
It exists because ADR-0074's consequences name the failure mode a reliability surface must not have: if the observer was down from 02:00 to 03:00, an empty ledger for that hour reads as "nothing broke". A GAP BETWEEN WINDOWS IS THEREFORE LITERAL — it is time no observer was recording, and a reader can tell it apart from a quiet hour. A restart always begins a NEW window rather than extending the last one, so burrowd bouncing can never be papered over into an unbroken stretch of coverage.
func (ObservationWindow) Complete ¶
func (w ObservationWindow) Complete() bool
Complete reports whether every sweep in this window read everything it set out to.
type Observer ¶
type Observer struct {
// contains filtered or unexported fields
}
Observer watches the workloads the registry says Burrow owns and writes what breaks to the ledger (ADR-0074 §3–§7). It is the first thing in Burrow that runs when nobody asked it to, and the reason burrowd is no longer purely request/response.
WHAT IT WATCHES IS BOUNDED BY THE REGISTRY, NOT BY A NAMESPACE OR A LABEL (ADR-0074 §3). That is what makes §6 expressible at all: a label selector can only find things that exist, and the interesting failure is the thing that does not — a registered app with no Deployment, an add-on with no running pod, a pending backup whose Job is gone, an exposure with no Ingress. Each is an absence, visible only from the side that knows what was intended.
IT IS READ-ONLY AGAINST THE CLUSTER AND MUST STAY THAT WAY (ADR-0074 §9). Every cluster call it makes is a read — ListWorkloads, AddonReady, BackupJobPresent, ExposureStatus — and it takes no corrective action of any kind. This is stated because the next step is obvious and wrong by default: once something notices a crash loop, restarting it looks like a small addition. It is not. It is a mutation performed with nobody present, which is the exact shape every guardrail in Burrow exists to gate, and the remedies for the failures that most invite it are usually wrong — restarting an OOM-killed pod without changing its limit reproduces the OOM. Remediation, if it is ever wanted, is its own record with its own guardrail dispositions.
It does NOT infer causes. It records what it observed, completely, in a shape something else can reason over; turning twenty rows into "the node pool was tainted at 02:14" is the agent's half of the work, and an inference engine inside burrowd is explicitly out of scope (ADR-0074 §5).
It SAMPLES on an interval rather than holding an informer. What ADR-0074 §3 requires of it is that the set it looks at be bounded by the registry, and a sweep over that set satisfies it through the same read seams every other surface uses — so a ledger row and a status read cannot disagree, and the whole thing is deterministic against a fake. The cost is resolution: a failure shorter than the interval can pass unseen. That is a real limit, and it is why the coverage record below says how often it looked rather than implying it was watching continuously. Moving to a watch later changes this file and nothing about the ledger's shape.
func (*Observer) Run ¶
Run observes until ctx is cancelled. It sweeps immediately, then waits the interval before the next sweep, honoring cancellation promptly. A sweep never returns an error to the loop: an observer that stopped on the first cluster hiccup would produce exactly the silent gap the coverage record exists to expose.
type ObserverConfig ¶
type ObserverConfig struct {
// Interval is the sweep cadence. Zero or negative applies DefaultObserveInterval.
Interval time.Duration
// Retention is how long resolved failures and elapsed windows are kept. Zero applies
// DefaultLedgerRetention; negative disables pruning, which is a deliberate operator choice and
// not the default (ADR-0074 §4 requires the bound).
Retention time.Duration
// PruneInterval is how often retention is enforced. Zero applies DefaultLedgerPruneInterval.
PruneInterval time.Duration
// After returns a channel that fires after d — the seam the run loop waits on between sweeps.
// Nil applies time.After. It is injected (rather than reading the wall clock) so a test drives
// the loop with no real sleeping and no ambient time (ADR-0010).
After func(d time.Duration) <-chan time.Time
}
ObserverConfig configures the failure observer. The zero value is valid: every field falls back to a documented default, so cmd/burrowd can start it with an empty config and a test can drive it deterministically by supplying Interval and After.
type OperationalConfig ¶
type OperationalConfig struct {
// Values holds every stored limit value keyed by the code it was set under — the bare code for
// a cluster value and `<env>.<code>` for an environment one. That is the same keying guardrail
// dispositions already use, so the two configuration surfaces resolve the same way and an
// operator learns one rule (ADR-0068 §3).
Values map[LimitCode]string
}
OperationalConfig is the operator-set configuration the control plane resolves operational limits from (ADR-0068 §1). It is the middle and outer tiers together: app config and secrets (ADR-0028) belong to the developer and reach the pod, and are a different thing entirely.
func (OperationalConfig) Count ¶
func (c OperationalConfig) Count(env string, code LimitCode) (int64, string)
Count returns the effective value of a count limit for env and the tier it came from. An unknown code, or one of another kind, yields zero at the default scope: passing one is a programming error, not an operator's.
func (OperationalConfig) Duration ¶
Duration returns the effective value of a duration limit for env and the tier it came from — the sibling of Count for ADR-0068 §6's duration occupants (build-Job retention, add-on metric retention, the status thresholds). An unknown code, or one of another kind, yields zero at the default scope.
func (OperationalConfig) Limits ¶
func (c OperationalConfig) Limits(env string) []LimitInfo
Limits returns every known limit with its effective value for the named environment, each marking which tier that value came from. An empty env, or `prod`, reports the cluster tier and the built-in defaults; a named environment reports its own values where it has them.
func (OperationalConfig) ReplicaCeiling ¶
func (c OperationalConfig) ReplicaCeiling(env string) (int32, string)
ReplicaCeiling returns the effective replica ceiling for env and the tier it came from (ADR-0068 §6). It is an int32 because every replica count below it is.
func (OperationalConfig) With ¶
func (c OperationalConfig) With(code LimitCode, value string) OperationalConfig
With returns a copy of the configuration with code's value set, leaving the receiver unchanged — the basis for `cluster config set` and for composing configurations in tests.
type PgBackRestCapability ¶
type PgBackRestCapability struct {
Present bool `json:"present"`
Ready bool `json:"ready"`
Pinned string `json:"pinned,omitempty"`
}
PgBackRestCapability reports the CloudNativePG pgBackRest plugin, the component a Postgres instance archives through (ADR-0066 §3). Present is whether its CRDs are served; Ready is whether its controller is actually running, kept apart for CloudNativePGCapability's reason — a CRD outlives the controller that installed it, and a `Stanza` written against a served CRD with nothing behind it is accepted and reconciled by nothing.
It reports NO running version, unlike CloudNativePGCapability. The plugin's release artifact does not carry its version anywhere Burrow can read back, so what Burrow targets is stated (Pinned) and what is running is stated as present or not — a version guessed off an image tag would be a claim about the component holding the backups that Burrow cannot stand behind.
type PhysicalBackupOutcome ¶
type PhysicalBackupOutcome struct {
// Label is pgBackRest's backup label (for example `20260210-101333F`), read from the `Backup`
// object's status. It is the name the repository knows this backup by, the handle a restore names,
// and the path component under which the manifest that proves it arrived lives. Empty when the
// backup did not complete.
Label string
// ObjectKey is where the backup's manifest is in the bucket, derived by the adapter from the
// instance's OWN `Stanza` — its repository path and stanza name — rather than from what Burrow
// would have configured. It is the key the read-back verifies at and the address the row records.
// Empty when the backup did not complete.
ObjectKey string
// Reason is a member of the closed BackupFailureReason set, or of ADR-0074 §2's IssueReason set.
Reason string
// Detail is one Burrow-authored line elaborating it — never a vendor response body, never a
// credential. CloudNativePG's own `status.error` is a Burrow-safe field to relay: it is the
// operator's rendering of what pgBackRest exited with, and it holds no credential because the
// credential never reaches an argv.
Detail string
}
PhysicalBackupOutcome is what a CloudNativePG `Backup` object reported (ADR-0066 §2, §4).
It is BackupJobOutcome's sibling and deliberately not the same type. A Job reports a byte count it measured; a `Backup` object carries no size field of any kind, which is the gap ADR-0066 §4 makes Burrow's to close, so what comes back here is an identity — pgBackRest's own label for the backup — and the size is joined afterwards from the store rather than read off the status.
type PodRequest ¶ added in v0.13.0
PodRequest is one pod's summed resource requests and the node it is scheduled on. It is what the scheduler actually reserves against a node's allocatable — not live usage. Node is empty for a pod that has not been scheduled (e.g. Pending for lack of room); such a pod reserves nothing yet but still shows as a consumer of demand. CPUMillis is milli-CPU; MemBytes is bytes.
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
}
Policy is the guardrail configuration the control plane evaluates dangerous operations against (ADR-0006). It carries dispositions and nothing else: a guardrail answers "may this operation happen", so a Policy holds no numbers. The bounds an operator sets are OperationalConfig's (ADR-0068 §2), and the evaluation that gates, constrains, or refuses an operation is the deploy engine's job.
func DefaultPolicy ¶
func DefaultPolicy() Policy
DefaultPolicy returns the conservative starting guardrail policy (ADR-0020): 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`.
The irreversible operations — deleting an app, deleting a public DNS record — are denied rather than confirmed (ADR-0065 §3): a confirmation is a real control only for someone who reads it, and these are the cases where the reader is busy or mid-incident. Those denies are a floor, not a fixed setting: the expected use is a per-environment gradient (allow in development, confirm in staging, deny in production), so an operator who finds one too strict should scope it with `guard set --env <env> ...` rather than switch it off everywhere.
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 env, or `prod` — the environment install created (ADR-0067 §2) — reproduces the global policy exactly and leaves Source unset, as for Guardrails: with one environment there is nothing for a per-environment override to differ FROM, so the default environment's policy IS the global policy rather than a second layer over it. `guard set --env staging …` then reads as the deliberate divergence it is.
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 ProbeCheck ¶
type ProbeCheck struct {
// Kind selects what the probe does.
Kind DependencyKind `json:"kind"`
// EnvKey is the environment variable holding the connection string, for a postgres check. The
// probe reads it from ITS OWN environment inside the app's container: the value is never in the
// plan, never in the Job spec, and never crosses the control plane.
EnvKey string `json:"env_key,omitempty"`
// URL is the address an exposure check requests. It is an in-cluster Service address Burrow
// composed, not a user value.
URL string `json:"url,omitempty"`
}
ProbeCheck is one check in a plan.
type ProbePlan ¶
type ProbePlan struct {
// Checks are the checks to run, in the order they are reported.
Checks []ProbeCheck `json:"checks"`
}
ProbePlan is what the check container is told to do — the whole of its input, and non-secret by construction. It travels as JSON in one environment variable.
func ParseProbePlan ¶
ParseProbePlan reads a plan back inside the check container.
type ProbeReport ¶
type ProbeReport struct {
Results []DependencyResult `json:"results"`
}
ProbeReport is what the probe prints on its marked line and what the engine parses back.
func ParseProbeReport ¶
func ParseProbeReport(out string) (ProbeReport, error)
ParseProbeReport finds the probe's marked line in a captured output stream and reads the results from it. It takes the LAST marked line: the probe prints exactly one, and taking the last means a user image that somehow echoed an earlier one cannot displace the real answer.
Every result is passed through the same gates the engine applies to its own: an unknown outcome or a reason outside the closed vocabulary is normalised rather than trusted, so a probe build that invented a value cannot put it on the surface an agent branches on.
type ProbeSpec ¶
type ProbeSpec struct {
// Env is the probe's OWN configuration, applied to the check container after the app's config so
// it cannot be shadowed by an app that happens to set the same key. It is non-secret by
// construction — the plan carries environment variable NAMES and in-cluster addresses Burrow
// composed, never a value — which is what makes it safe in a Job spec.
Env map[string]string
}
ProbeSpec is the extra a Job needs to run Burrow's probe inside the app's image (ADR-0076 §4).
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. For a
// provider whose credential is a PAIR rather than one opaque token (object storage, ADR-0063
// §1) it names the first of the pair's two keys, and ObjectStore records both by name.
SecretKey string `json:"secret_key"`
// ObjectStore is the non-secret object-storage configuration — endpoint, region, the RECORDED
// bucket, and the names of the two burrow-credentials keys holding the credential pair
// (ADR-0063 §1). Nil for a provider that serves no object storage.
ObjectStore *ObjectStoreConfig `json:"object_store,omitempty"`
// CreatedAt is when the provider was registered, read from the injected clock.
CreatedAt time.Time `json:"created_at"`
// Verification reports what configuration-time verification OBSERVED — the probe object, the
// bucket, the lifecycle reconciliation (ADR-0063 §2-§4). It is populated by the registration
// that performed the checks and is deliberately NOT stored: it describes one moment, and a
// stored copy would go stale while still reading as current.
Verification *ProviderVerification `json:"verification,omitempty"`
}
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.
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" // ProviderGitHub is GitHub: a source provider whose one token covers both github.com private-git // clones and ghcr.io image pulls (ADR-0057). ProviderGitHub ProviderType = "github" // ProviderGitLab is GitLab: a source provider whose one token covers both gitlab.com private-git // clones and its container registry (ADR-0057). ProviderGitLab ProviderType = "gitlab" // ProviderS3 is any S3-compatible object store — AWS S3, Backblaze B2, Cloudflare R2, MinIO, // and the rest. It is deliberately NOT a vendor: ADR-0063 §1 addresses object storage by // ENDPOINT, so the vendor is whoever answers it and Burrow maintains no vendor list. S3 // compatibility is a spectrum rather than a standard, so a vendor is supported when it has been // tested, not when it claims compatibility. ProviderS3 ProviderType = "s3" )
func SourceProviderForHost ¶ added in v0.13.0
func SourceProviderForHost(host string) (ProviderType, bool)
SourceProviderForHost returns the source provider that serves the given git host (e.g. "github.com" -> ProviderGitHub) and whether one is known. It is how the in-cluster build maps a clone URL's host to the provider whose credential authenticates it (ADR-0057 §1).
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) GitHost ¶ added in v0.13.0
func (t ProviderType) GitHost() string
GitHost returns the git host a source provider clones from (e.g. "github.com"), or "" for a non-source provider.
func (ProviderType) GitUser ¶ added in v0.13.0
func (t ProviderType) GitUser() string
GitUser returns the HTTPS basic-auth username the clone presents alongside a source provider's token, or "" for a non-source provider.
func (ProviderType) RegistryHost ¶ added in v0.13.0
func (t ProviderType) RegistryHost() string
RegistryHost returns the registry host a source provider's token also authenticates (e.g. "ghcr.io"), or "" for a non-source provider (ADR-0057 §1).
func (ProviderType) Valid ¶
func (t ProviderType) Valid() bool
Valid reports whether t is a provider type Burrow supports.
type ProviderVerification ¶
type ProviderVerification struct {
// Bucket is the bucket that was verified — the recorded name, which is the only bucket Burrow
// will ever write to.
Bucket string `json:"bucket"`
// BucketCreated reports whether this registration created the bucket.
BucketCreated bool `json:"bucket_created"`
// ProbeObject reports that a probe object was written and then deleted at configuration time,
// so a wrong key or a typo'd endpoint failed here rather than at the first scheduled backup.
ProbeObject bool `json:"probe_object"`
// Lifecycle is the reconciliation of the bucket's lifecycle rules against backup retention.
Lifecycle LifecycleCheck `json:"lifecycle"`
}
ProviderVerification reports what Burrow OBSERVED while registering a provider — the probe it wrote and deleted, the bucket it created or was pointed at, and the lifecycle reconciliation (ADR-0063 §2, §3, §4).
It is deliberately NOT persisted: it describes one moment, and a stored copy would go stale while continuing to read as current. The durable answer to "is the destination healthy now?" is a status surface that observes it on demand (ADR-0063 §7), which is a separate change.
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 ReadinessCheck ¶
type ReadinessCheck struct {
// Port is the container port to check. Zero means no probe at all.
Port int32 `json:"port,omitempty"`
// Path, when set, makes this an HTTP GET against the pod's own port. Empty makes it a TCP
// connect, which proves only that the process bound the socket — strictly more than "the
// container did not exit", and all Burrow can prove without the app's cooperation.
Path string `json:"path,omitempty"`
}
ReadinessCheck is the readiness probe Burrow authors on a user application's container — the RESOLVED answer, after ResolveReadiness has combined what the user declared with what Burrow knows. The zero value means NO PROBE, which is the honest answer whenever Burrow does not know a port (ADR-0076 §3).
Its shape is the enforcement of §2. There is no Host field, no Exec command, and no URL: the probe can only ever address the pod's OWN port, so "check the database from the readiness probe" is not a thing this type can express. That is deliberate — the rule with the largest blast radius is the one most likely to be broken by someone trying to be helpful, and a comment would not have stopped them.
func ResolveReadiness ¶
func ResolveReadiness(ep HealthEndpoint, exposedPort int32) ReadinessCheck
ResolveReadiness decides the probe for one app from its declared endpoint and the container port its recorded exposure routes to (zero when it is not published). It is the whole of ADR-0076 §3 and §5, in one pure function, so the decision is testable without a cluster and cannot drift between the deploy path, the rollback path, and the config-reapply path that all call it.
- A declared path wins, on its own port when one was given and on the exposure's otherwise.
- No declared path, but a published port → a TCP connect on it.
- Neither → no probe. Burrow does not scan, assume 8080, or guess /healthz.
A declared path with no port anywhere is the one degenerate case: the endpoint was declared before the app was published, so there is nothing to reach it on yet. That returns NO PROBE rather than a guessed port, and the health surface says so; the probe starts on the first apply after the app is published.
func (ReadinessCheck) Enabled ¶
func (r ReadinessCheck) Enabled() bool
Enabled reports whether a probe is authored at all.
func (ReadinessCheck) HTTP ¶
func (r ReadinessCheck) HTTP() bool
HTTP reports whether this is an HTTP GET check rather than a TCP connect.
func (ReadinessCheck) Kind ¶
func (r ReadinessCheck) Kind() string
Kind names the probe in one word for a human or an agent reading the health surface: "http", "tcp", or "none".
type RegistryAuth ¶ added in v0.13.0
RegistryAuth carries optional basic-auth credentials for listing a private repo's tags (ADR-0052 §7). The zero value lists anonymously — the case for public GHCR/Docker Hub.
type RegistryClient ¶ added in v0.13.0
type RegistryClient interface {
// ListTags returns the tags available in the repository named by imageRef (a reference
// like "ghcr.io/user/app:1.2.3" or the bare repo). auth carries optional basic-auth
// credentials for a private repo; the zero value lists anonymously. It follows the Docker
// Registry HTTP API v2 tag-list pagination and the standard Bearer-token auth flow.
ListTags(ctx context.Context, imageRef string, auth RegistryAuth) ([]string, error)
}
RegistryClient lists the tags of a container image repository so burrowd can see which versions exist and compute what auto-deploy would take (ADR-0052). It is OUTBOUND-only and used only for the optional auto-deploy read/watch — never on the core deploy path, which stays independent of registry reachability (ADR-0040). It is an OPTIONAL seam: when it is not wired the auto-deploy show degrades to reporting the level alone.
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"`
// Environment is the canonical environment this release was deployed into (ADR-0052 §5):
// an environment added later, or "prod" for the default one (ADR-0067 §2). Releases are
// keyed per (app, environment), so an app can have an independent history in each.
Environment string `json:"environment,omitempty"`
// 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"`
// Trigger is how this deploy was triggered (ADR-0052 §5): "manual" for an explicit CLI or
// agent deploy — the default for every deploy today — or "auto" for the pull-based passive
// watcher (Phase 4b). Empty on rows written before provenance existed.
Trigger ReleaseTrigger `json:"trigger,omitempty"`
// AutoLevel is the auto-deploy level that applied when the watcher took this release
// (ADR-0052 §5). Set only for an auto trigger; empty for a manual deploy.
AutoLevel AutoDeployLevel `json:"auto_level,omitempty"`
// AutoTag is the resolved tag the watcher moved to when it took this release (ADR-0052 §5).
// Set only for an auto trigger; empty for a manual deploy.
AutoTag string `json:"auto_tag,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 the agent control channel (env, command, replica count) — never any code (ADR-0004).
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 ReleaseTrigger ¶ added in v0.13.0
type ReleaseTrigger string
ReleaseTrigger records how a deploy was triggered — its provenance (ADR-0052 §5), so the deploy record and audit log distinguish an explicit deploy from an unattended auto-update.
const ( // TriggerManual is an explicit deploy driven by a human or an agent through the CLI or the // control-plane API. It is the default for every deploy today: nothing auto-deploys until the // Phase 4b watcher lands. TriggerManual ReleaseTrigger = "manual" // TriggerAuto is an unattended deploy the pull-based passive watcher performed on its own // (ADR-0052 §1). An auto release also records the level that applied and the tag it took. TriggerAuto ReleaseTrigger = "auto" )
func (ReleaseTrigger) Valid ¶ added in v0.13.0
func (t ReleaseTrigger) Valid() bool
Valid reports whether t is a known release trigger.
type RemoveAddonOptions ¶
type RemoveAddonOptions struct {
// DeleteData destroys the add-on's data volume as well as its workload — for Postgres, every
// attached app's database. Its absence is the safe default (ADR-0064 §1).
DeleteData bool
// SkipFinalBackup destroys the data WITHOUT the final backup ADR-0064 §5 otherwise requires.
//
// It exists because §5 cannot be mandatory: an add-on is often removed precisely BECAUSE it is
// wedged, and a wedged instance cannot be dumped. With no way past the backup, a broken add-on
// would be undeletable — trading a data-loss failure for an unrecoverable cluster. It is a
// separate flag from DeleteData, not a mode of it, so asking for it is a sentence someone had to
// write; and every path that honours it says in the result that no backup was taken.
SkipFinalBackup bool
// BackupDestination names which registered object-storage provider the final backup goes to. It
// is only needed when several are registered — ADR-0063 §6 allows that on purpose, for a user
// migrating between vendors — and Burrow refuses to guess rather than writing the last copy of a
// database somewhere nobody is watching.
BackupDestination string
// Confirm satisfies the addon.remove guardrail's confirmation hold (ADR-0020). It is NOT the
// data-loss acknowledgement: that gate lives on the operator CLI (ADR-0064 §2).
Confirm bool
}
RemoveAddonOptions is everything `addon remove` needs beyond the add-on's name. It is a struct rather than a run of positional booleans because this is the most destructive operation in the product and `RemoveAddon(ctx, name, true, false, true)` is not a call anyone can review.
type RemoveAddonResult ¶
type RemoveAddonResult struct {
Name string `json:"name"`
Type AddonType `json:"type"`
// AddonRemoval is embedded so its fields (namespace, retained volumes) flatten into the JSON the
// agent reads — the removal facts are the result, not a nested detail of it.
AddonRemoval
// AttachedApps are the apps with a Burrow-provisioned database on this instance at removal time,
// sorted. Empty for an add-on type that has no per-app attachments, and empty (not an error) when
// the instance could not be reached to enumerate them — a wedged add-on must stay removable.
AttachedApps []string `json:"attached_apps,omitempty"`
// FinalBackups are the backups taken before the data volume was destroyed, one per attached
// database (ADR-0064 §5). Each is a completed row at an object-store destination — the removal
// does not get past them otherwise — so this is the list of copies that outlived the instance,
// and it is what a restore is addressed with. Empty on a removal that kept its data.
FinalBackups []Backup `json:"final_backups,omitempty"`
// FinalBackupSkipped reports that the data volume was destroyed with NO off-cluster copy taken:
// the override flag was passed, no object-storage provider was registered, or the add-on is not
// one Burrow can dump. It is reported rather than inferred from an empty FinalBackups, because
// "nothing was backed up" and "nothing needed backing up" are the two answers an operator must
// not have to guess between.
FinalBackupSkipped bool `json:"final_backup_skipped,omitempty"`
// FinalBackupNote is the one-line reason behind FinalBackupSkipped, safe to print.
FinalBackupNote string `json:"final_backup_note,omitempty"`
}
RemoveAddonResult is the structured outcome of removing an add-on (ADR-0025): what was torn down and, just as importantly, what was kept. AttachedApps names the apps that held a Burrow-provisioned database on the instance — the concrete scope of the consequence, whether that is "these lost their data" or "these are disconnected until it is reinstalled".
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 RestoreInstanceOptions ¶
type RestoreInstanceOptions struct {
// Backup names a recorded PHYSICAL backup to recover exactly. The instance comes back as that
// base backup was, with no write-ahead log replayed past it.
Backup string
// ToTime is an RFC3339 instant inside the repository's write-ahead-log window. The instance comes
// back as it was at that moment — the point-in-time recovery physical backups exist for.
ToTime string
// Latest recovers to the furthest point the archived write-ahead log reaches, which is the
// newest state the repository can produce.
Latest bool
// SkipSafetyBackup destroys the instance's current state WITHOUT first taking a physical backup
// of it (see the safety backup below). It is for an instance too broken to back up.
SkipSafetyBackup bool
// Destination NAMES the object-storage provider holding this instance's repository (ADR-0063 §6).
// Empty resolves it, which works when exactly one is registered and is refused when several are —
// the repository a live instance is rebuilt from is not a thing to guess at. It is a registry name,
// never a credential, and it is CHECKED against the instance's own `Stanza` before anything is
// destroyed rather than trusted.
Destination string
// Confirm satisfies the addon.restore_instance guardrail's confirmation hold (ADR-0020).
Confirm bool
}
RestoreInstanceOptions is everything `addon restore-instance` needs beyond the add-on type and the environment.
EXACTLY ONE RECOVERY TARGET IS REQUIRED, and defaulting is refused rather than chosen. "The newest state" and "the moment before the bad migration" are different operations with the same blast radius, and a restore that picked one because the operator named neither would be the most destructive default in the product.
type RestoreInstanceOutcome ¶
type RestoreInstanceOutcome struct {
// Instance is the `Cluster` the recovered instance came up as. It is the environment's canonical
// instance name: the recovery replaces the instance under the name every consumer already
// resolves, rather than standing a differently-named one up beside it.
Instance string
// VolumesDestroyed names the data claims of the pre-restore instance that were removed to make
// room for the recovered one.
VolumesDestroyed []string
}
RestoreInstanceOutcome is what the seam did.
type RestoreInstanceRequest ¶
type RestoreInstanceRequest struct {
// Environment selects the instance, its repository path and its credential together, exactly as
// it does on every other add-on path (ADR-0067 §1).
Environment string
// BackupLabel is pgBackRest's own label for the base backup to recover — the handle the
// repository knows it by. Empty unless a recorded backup was named.
BackupLabel string
// TargetTime is an RFC3339 instant to recover to. Empty unless a time was named.
TargetTime string
// Archive is the repository to recover FROM, resolved by the engine and checked by the adapter
// against the instance's own `Stanza` — a recovery pointed at a bucket the instance never wrote
// to produces an empty database rather than an error.
Archive *ArchiveDestination
}
RestoreInstanceRequest is what the Kubernetes seam is asked to do: recover environment Environment's Postgres instance from its own pgBackRest repository.
It carries an ArchiveDestination and therefore a SECRET VALUE, so like ArchiveDestination itself it is an internal in-memory shape only — no JSON tags, never returned over the control-plane API, never audited, never logged, never placed in an error.
type RestoreInstanceResult ¶
type RestoreInstanceResult struct {
Addon AddonType `json:"addon"`
Environment string `json:"environment"`
// Instance is the Postgres instance that was rewound, by the name every consumer resolves it at
// (AddonInstanceName).
Instance string `json:"instance"`
// RecoveryTarget is the point that was recovered to, in the same words the confirmation used. The
// JSON name is not `target`: `burrow`'s own result envelope carries a `target` — the control plane
// the command acted on (ADR-0078 §4) — and two different meanings under one key is how an agent
// composing this result says the wrong thing about where a rewind happened.
RecoveryTarget string `json:"recovery_target"`
// SafetyBackup is the id of the physical backup taken of the instance's state BEFORE the rewind,
// which is the way back from a restore to the wrong point. Empty when none was taken.
SafetyBackup string `json:"safety_backup,omitempty"`
// SafetyBackupNote says why no safety backup was taken, when none was. It is stated as plainly as
// a taken one: an operator who rewound a database believing a copy exists is worse off than one
// who knows none does (ADR-0064 §5's posture, applied to the same decision).
SafetyBackupNote string `json:"safety_backup_note,omitempty"`
// DataVolumesDestroyed names the pre-restore instance's data claims, which the recovery replaced.
// They cannot be retained: CloudNativePG reattaches a `Cluster` to a claim left under its name
// instead of recovering from the repository, so keeping one would silently cancel the restore.
DataVolumesDestroyed []string `json:"data_volumes_destroyed,omitempty"`
// Apps is every app that was attached to the instance when the restore began, by name. It is the
// blast radius, recorded rather than counted.
Apps []string `json:"apps"`
// Reconnected is the apps whose DATABASE_URL was rewritten for the recovered instance and which
// were restarted onto it.
Reconnected []string `json:"reconnected"`
// Stranded is the apps the cutover could not complete, with one line each. They still hold a
// DATABASE_URL for an instance that has been rebuilt, so their credential no longer works and the
// operator has to re-attach them — which is what the line says.
Stranded []StrandedApp `json:"stranded,omitempty"`
}
RestoreInstanceResult reports what a physical restore did (ADR-0066 §4). It names the instance, where it was rewound to, what happened to the state that was there, and every app that was on it — never a credential and never a connection string.
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"`
// Hints are non-blocking notes about the rollback, in the same shape DeployResult carries them:
// today, what the rollback's settle-wait observed and what a `post-deploy` hook made of it
// (ADR-0072 §4). They never gate or fail the rollback — it has already happened by the time any
// of them exists. Empty when there is nothing to note.
Hints []string `json:"hints,omitempty"`
}
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 RolloutOutcome ¶
type RolloutOutcome struct {
// Settled reports that the newest revision finished rolling out and is serving — the completion
// test `kubectl rollout status` uses, not merely that the write was accepted.
//
// WHAT IT DOES NOT MEAN is ADR-0072 §7, and it is worth reading before reading "succeeded": the
// rollout completed and no pod reported a blocking condition. Since ADR-0076 an app that declares
// a health endpoint (or is published) carries a readiness probe, so for those apps this is a much
// stronger statement than it was — a pod that never passes its probe never becomes available and
// the rollout stalls into ReasonProgressDeadlineExceeded. For an app that declares nothing and is
// not published there is still no probe, and Kubernetes marks a pod ready as soon as its
// container starts, so an app that boots and returns 500 to every request settles as a success.
// That is why a smoke test is the natural post-deploy hook: Burrow tells the hook the deploy
// HAPPENED and the hook decides whether it WORKED.
Settled bool
// Reason names why the rollout did not settle. It is a member of the closed vocabulary
// LedgerReasons() enumerates — ADR-0074 §2's IssueReason set plus §6's intent-versus-cluster
// absences — and is empty exactly when Settled is true.
//
// §6's absences are in scope here for one case and one only: a workload that is GONE while the
// registry says it was rolled out is ReasonWorkloadMissing, which is precisely the reason ADR-0074
// coined for it. Reaching for a new string there would have been the one thing ADR-0072 §4 asks
// this not to do.
Reason string
// Detail is ONE Burrow-authored line describing what the wait observed — replica counts, a pod's
// phase, a container's waiting reason. It is what makes an expired deadline a diagnosis rather
// than an elapsed time (§5).
//
// IT IS NEVER APPLICATION OUTPUT. The live status surface's crash-loop Issue carries a bounded
// tail of the application's previous log, which may contain anything the app printed — a
// connection string, a token in a stack frame (ADR-0074 §9). That is an acceptable trade for a
// value read live and discarded; it is not one for a value copied into a Job's environment, where
// it would sit in a Kubernetes object for anyone with read access to the namespace. So the hook
// is told the reason, which is the part it branches on, and the app's own output stays in
// `burrow app logs` and `burrow app status`, which still carry it.
Detail string
}
RolloutOutcome is what a bounded wait for a rollout to settle observed (ADR-0072 §4-§5). It is the answer a `post-deploy` hook is told, and it is deliberately small: an outcome, a reason from a closed set, and one Burrow-authored line of context.
func (RolloutOutcome) Failed ¶
func (o RolloutOutcome) Failed() bool
Failed reports that the wait reached a verdict and the verdict was negative. It is the inverse of Settled, named so a caller reads the two cases in the words the hook is told them in.
func (RolloutOutcome) Outcome ¶
func (o RolloutOutcome) Outcome() string
Outcome renders the outcome in the closed two-value vocabulary a post hook is handed: "succeeded" or "failed" (ADR-0072 §4). There is no third value. A wait that could not read the cluster is not a third kind of answer — it is a wait that ran out of time without observing a settle, which is ReasonDeadlineExceeded with a detail that says the workload could not be read.
type RunRequest ¶ added in v0.12.0
type RunRequest struct {
App string `json:"app"`
// Env is the environment whose namespace the command runs in (ADR-0035): empty or "prod" targets
// the default environment, a name added later targets that environment.
Env string `json:"env,omitempty"`
// Command is the command and its arguments to run, as an argv. It must be non-empty.
Command []string `json:"command"`
// TTLSeconds overrides how long the finished Job lingers before Kubernetes garbage-collects it
// (ttlSecondsAfterFinished; ADR-0048 §7). Nil applies the default (one hour); a value — including
// 0, delete as soon as the output is captured — overrides it. A negative value is rejected.
TTLSeconds *int32 `json:"ttl_seconds,omitempty"`
// Confirm acknowledges the app.run guardrail whose disposition is confirm, letting the command
// proceed past it (ADR-0020). It has no effect on a guardrail set to deny.
Confirm bool `json:"confirm,omitempty"`
}
RunRequest is a one-off command to run in an app's own current image and environment (ADR-0048). It carries only the command (and its arguments) — the image, config, and secrets come from the app, resolved server-side. No code travels here (ADR-0004): the command names an entrypoint that already exists in the app's image.
type RunResult ¶ added in v0.12.0
type RunResult struct {
App string `json:"app"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
// TimedOut reports the command did not finish within the run window (10 minutes).
TimedOut bool `json:"timed_out,omitempty"`
}
RunResult reports the outcome of a one-off command (ADR-0048 §3). A non-zero ExitCode is a normal structured outcome the agent reasons over, not a transport failure. Stdout carries the command's captured output — Kubernetes returns a pod's stdout and stderr as one interleaved stream, so the output lands in Stdout; Stderr is reserved for a future separation.
type RunSpec ¶ added in v0.12.0
type RunSpec struct {
App string
// ID is the unique run identifier the Job name derives from (burrow-run-<ID>), minted by the
// engine's ID seam so the Job name is deterministic in tests.
ID string
// Image is the app's currently-deployed image the command runs in (ADR-0048 §2).
Image string
// Command is the command and its arguments run as the container's command.
Command []string
// Env is the app's non-secret config, sourced as container env alongside the per-app Secret.
Env map[string]string
// TTLSeconds is the Job's ttlSecondsAfterFinished — how long the finished Job lingers before
// Kubernetes garbage-collects it (ADR-0048 §7). Zero deletes it as soon as it finishes.
TTLSeconds int32
// Probe, when set, asks the adapter to make Burrow's own binary executable inside this Job's
// container before Command runs: an init container from Burrow's image copies its own executable
// into an emptyDir that the app's container mounts at ProbeMountPath (ADR-0076 §4).
//
// It exists because the deploy-time dependency check has to run in the APP's image — the app's
// filesystem, service account, network policy and credential are where misconfiguration lives —
// and that image may contain no shell, no psql and no curl, which is exactly the minimal image
// users are told to build. nil (the default) is an ordinary run and authors nothing extra.
//
// No code travels here either (ADR-0004): the init container's image is Burrow's own published
// image, named by the adapter, and the only thing that moves is a reference to it.
Probe *ProbeSpec
}
RunSpec is the desired one-off command Job the kube seam builds for RunJob (ADR-0048): the app's own current image and config env, plus the caller's command and the finished-Job TTL. The per-app Secret is injected by the adapter via envFrom, not carried here, so no secret value crosses this seam. No code travels here (ADR-0004) — the command names an entrypoint already in the image.
type ScaleResult ¶
type SourceCredential ¶ added in v0.13.0
type SourceCredential struct {
// Provider is the source provider the token authenticates (github, gitlab), which fixes the git
// host and the registry host the token also covers (ADR-0057 §1).
Provider ProviderType
// Token is the provider token VALUE (an OSS Personal Access Token). It lives in memory in burrowd
// only long enough to be written into the build Job's mounted Secret.
Token string
}
SourceCredential is a resolved source-provider credential the in-cluster build uses to authenticate a PRIVATE git clone and, where the provider hosts a registry, the build's image push/pull (ADR-0057). The engine resolves it from the guarded credential store just before a build and hands it to the Builder seam, which mounts it into the build Job — the token is never a Job env var, a command-line argument, or anything the agent sees. The zero value (empty Token) means public-source, credential-free — the pre-ADR-0057 behavior. The Token is a SECRET: it is never logged, never placed in an error or an API response, and never stored in Postgres.
func (SourceCredential) IsZero ¶ added in v0.13.0
func (c SourceCredential) IsZero() bool
IsZero reports whether the credential carries no token — the public-source, credential-free path.
type SourceRef ¶ added in v0.13.0
type SourceRef struct {
// Repo is the git repository URL to clone (e.g. "https://github.com/user/app").
Repo string
// Ref is the commit SHA or tag to check out. A build pins an exact reference, so it is required.
Ref string
}
SourceRef names the git source an in-cluster build clones and checks out inside the user's own cluster (ADR-0053 §3): a repository URL plus the commit or tag to build. It is the ONLY thing a build carries over the control channel — never source bytes (ADR-0004). The builder clones the actual code from git inside the cluster; the agent never carries a tarball, a diff, or any code.
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"`
// Failures is this app's recent failure history from the ledger (ADR-0074 §8): the failures
// observed for it over the last StatusFailureLookback, oldest first, resolved ones included.
// Workload above is the live present tense; this is the part nothing else can reconstruct —
// whether the app crash-looped at 02:00 and recovered, and when it started.
Failures []Failure `json:"failures,omitempty"`
// Coverage is what the observer was doing over that same window. It is never omitted, because
// an empty Failures list means "nothing broke" only if something was watching.
Coverage Coverage `json:"coverage"`
}
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 StrandedApp ¶
StrandedApp is one app the DATABASE_URL cutover did not finish for, and why.
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
// Readiness is the readiness probe to author on the container (ADR-0076 §1-§3). The zero value
// means NO PROBE, which is what an app whose port Burrow does not know gets — behaviour
// identical to before probes existed. The engine resolves it with ResolveReadiness on every
// apply, so a deploy, a rollback, and a config reapply all author the same probe.
//
// There is no liveness field here and there is not going to be one by default (§1): readiness
// takes a pod out of service reversibly, liveness restarts the container and manufactures the
// crash loop it was meant to detect.
Readiness ReadinessCheck
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 — a pull failure that
// names the image, the registry host and the `burrow config registry login` fix (ADR-0006); a
// scheduling failure that names the taint or the resource; a crash loop that names the exit
// code (ADR-0074 §2). 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.
//
// It never contains a secret VALUE. A missing config or secret key is named, because the key is
// the actionable part; a crash-loop log tail is the application's own output, bounded and
// labelled as such (ADR-0074 §9).
Issue string `json:"issue,omitempty"`
// IssueReason is the machine-usable reason behind Issue — a member of the closed set
// IssueReasons() enumerates (e.g. "ImagePullBackOff", "Unschedulable", "CrashLoopBackOff") —
// for an agent that wants to branch on the cause rather than parse the prose (ADR-0074 §5).
// Each value is the raw Kubernetes reason string wherever Kubernetes has one. 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.
Source Files
¶
- addon_final_backup.go
- addon_volumes.go
- addons.go
- apiwait.go
- audit.go
- autodeploy.go
- autodeploy_poller.go
- backup_health.go
- build.go
- capabilities.go
- capacity.go
- dependencies.go
- dns.go
- doc.go
- domain.go
- engine.go
- environment.go
- errors.go
- exposures.go
- failures.go
- guardrails.go
- health.go
- history.go
- hooks.go
- ids.go
- imagepull.go
- issues.go
- ledger.go
- limits.go
- objectstorage.go
- observer.go
- physical_backup.go
- physical_restore.go
- prerequisites.go
- provider.go
- results.go
- rollout.go
- run.go
- seams.go
- types.go
- units.go
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 objectstore is the production controlplane.ObjectStoreFactory: a small S3-compatible client for the ONE thing Burrow does with object storage, which is own the seam between a bucket and the backups written to it (ADR-0063).
|
Package objectstore is the production controlplane.ObjectStoreFactory: a small S3-compatible client for the ONE thing Burrow does with object storage, which is own the seam between a bucket and the backups written to it (ADR-0063). |
|
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 registry holds the adapter that lists a container image repository's tags over the Docker Registry HTTP API v2, so burrowd can see which versions exist and compute what auto-deploy would take (ADR-0052).
|
Package registry holds the adapter that lists a container image repository's tags over the Docker Registry HTTP API v2, so burrowd can see which versions exist and compute what auto-deploy would take (ADR-0052). |
|
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). |