kube

package
v0.14.0-rc.21 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package kube is the production controlplane.Kubernetes adapter, built on the official client-go SDK (ADR-0011). It translates the workload seam into Kubernetes Deployments and reads their status, scales, streams logs, and deletes them. It is a thin translation layer — no orchestration logic, which lives in the engine. v0.1 supports only WorkloadDeployment.

It lives under controlplane/ (not controlplane/internal) so cmd/burrowd and the managed module can wire it; it is licensed Apache-2.0.

Index

Constants

View Source
const (
	// CNPGAPIGroup is CloudNativePG's API group. Its presence in API-group discovery means the
	// operator's CRDs are installed, which is the same no-RBAC signal cert-manager is detected by.
	CNPGAPIGroup = "postgresql.cnpg.io"
	// CNPGNamespace and CNPGControllerDeployment are where the release manifest puts the operator.
	// They name the wait target for an install; detection uses the label selector below instead, so
	// an operator installed elsewhere (a Helm release into another namespace) is still seen.
	CNPGNamespace            = "cnpg-system"
	CNPGControllerDeployment = "cnpg-controller-manager"
)

CloudNativePG's identity in a cluster, taken from the pinned release's install manifest (CNPGVersion). Burrow does not ship the operator — it applies the upstream release artifact and the cluster pulls the image — so these names are a claim about somebody else's manifest, and they move when the pin moves.

View Source
const (
	// PgBackRestPluginName is the plugin's CNPG-I name — the string a `Cluster`, a `Backup` and a
	// `ScheduledBackup` all name it by — and also its API group, which is where its `Stanza` lives.
	// The two being the same string is the plugin's choice, not a coincidence this code relies on;
	// PgBackRestAPIGroup exists so a reader of either use is not guessing.
	PgBackRestPluginName = "pgbackrest.dalibo.com"
	// PgBackRestAPIGroup is the API group the plugin's own custom resources live under. Its presence
	// in API-group discovery means the plugin's CRDs are installed — the same no-RBAC signal
	// CloudNativePG and cert-manager are detected by.
	PgBackRestAPIGroup = PgBackRestPluginName
	// PgBackRestVersion is the plugin release Burrow targets, applied from the publisher's own tag.
	PgBackRestVersion = "0.0.3"
	// PgBackRestControllerDeployment and PgBackRestNamespace are where the release manifest puts the
	// plugin's controller: alongside the CloudNativePG operator, because the operator connects to it
	// over a Service in that namespace.
	PgBackRestControllerDeployment = "pgbackrest-controller"
	PgBackRestNamespace            = CNPGNamespace
)
View Source
const CNPGVersion = "1.30.0"

CNPGVersion is the CloudNativePG release this package's placement translation targets, and the release Burrow installs when the Postgres add-on needs the operator (ADR-0066 §1).

It is a pin rather than "whatever is latest" because the translation below is a claim about that release's schema. Moving this constant without re-recording cnpg_placement_schema.json fails TestCNPGPlacementSchemaIsTheRecordedRelease, which is the point: a controller upgrade is exactly when a placement field can be renamed or removed, and ADR-0077's Consequences call a mapping that silently stops covering a field the reintroduction of §3's failure.

View Source
const ControlPlaneDatabaseName = "postgres"

ControlPlaneDatabaseName is the name of both shapes of the control plane's own database in the control-plane namespace: the `Cluster` a CloudNativePG install creates, and the Deployment a plain one creates (ADR-0086 §2). They share it because the Service in front of them does, which is what lets one connection URL address either.

View Source
const DefaultCredentialsSecret = "burrow-credentials"

DefaultCredentialsSecret is the one Secret in the control-plane namespace that holds every vendor token, one key per provider (ADR-0023).

View Source
const PostgresPasswordKey = "password"

PostgresPasswordKey is the key under which the superuser password is stored in PostgresSecretName.

View Source
const PostgresSecretName = "burrow-postgres"

PostgresSecretName is the Secret in the add-on namespace that holds the DEFAULT environment's generated superuser password (ADR-0031). It lives in the add-on namespace — not the control-plane credentials Secret — because a pod can only mount a Secret in its own namespace.

The Secret is named after the INSTANCE, so every environment's instance has its own superuser credential and this constant is the default environment's case of that rule (ADR-0067 §1) — which is why an install predating environments keeps the Secret, the volume, and the password it already has. Use postgresSecretName(env) on any path that can serve more than the default environment.

View Source
const PostgresSuperuser = "burrow_admin"

PostgresSuperuser is the fixed superuser role burrowd provisions the add-on Postgres instance with and connects as to run admin SQL (ADR-0031). It is deliberately not the built-in "postgres" role: a distinct, Burrow-owned admin role keeps the boundary clear.

Variables

View Source
var LeanPostgresSettings = []string{
	"shared_buffers=64MB",
	"max_connections=30",
	"work_mem=4MB",
	"maintenance_work_mem=32MB",
	"effective_cache_size=256MB",
}

LeanPostgresSettings are the server settings Burrow runs its Postgres instances with — both the control-plane state database (ADR-0012, rendered into cmd/burrow/manifests/install.yaml.tmpl) and the Postgres add-on, whose `Cluster` carries them as spec.postgresql.parameters (leanPostgresParameters). Burrow's databases are low-traffic control-plane/metadata stores, so the stock postgres defaults (128MB shared_buffers, 100 max_connections, default work_mem) are wildly generous; these lean values let the whole stack (k3s + burrowd + Postgres) fit a 1-2GB VPS with real headroom. The install manifest hard-codes the SAME values as postgres args — keep the two in step.

Functions

func BuilderImageForVersion added in v0.13.0

func BuilderImageForVersion(version string) string

BuilderImageForVersion returns the pinned builder image reference for a stamped release version, so a released burrowd pulls the builder image published under the SAME release tag (reproducible) rather than the floating :latest. For an unstamped dev build (version "" or "v0.0.0") it returns "" — the caller then leaves the :latest default (or an explicit BURROW_BUILD_IMAGE override) in place.

func CNPGManifestURL

func CNPGManifestURL(version string) string

CNPGManifestURL is the release artifact a CloudNativePG version publishes: the CRDs, the RBAC, and the operator Deployment in one document. It is what an install applies AND what the placement schema is recorded from, so the schema Burrow validates against is the schema the cluster holds.

Applying the upstream artifact rather than a vendored copy is deliberate: Burrow ships no third-party bytes, it points a cluster at the images and manifests their publishers already serve.

func ConfigFromKubeconfig

func ConfigFromKubeconfig(path string) (*rest.Config, error)

ConfigFromKubeconfig builds a REST config from an explicit kubeconfig file path. It is used by the integration tests, which point at a disposable cluster rather than the ambient one.

func DetectCapabilities

func DetectCapabilities(ctx context.Context, client kubernetes.Interface) (controlplane.ClusterCapabilities, error)

DetectCapabilities reads a cluster's capabilities read-only over the given clientset (ADR-0034): the ingress controller (a ready ingress-nginx controller Deployment) and its IngressClasses, the default and all StorageClasses, the cloud provider (from node providerIDs/labels), cert-manager and metrics-server (via API-group discovery), the CloudNativePG operator (its API group plus a running controller), and detects LoadBalancer support from whatever actually services LoadBalancers — a recognized cloud provider, k3s's built-in servicelb, or MetalLB (ADR-0043). It performs only get/list reads and API-group discovery — it never writes. It is a free function so the same detection runs whether driven by the kubeconfig client (install) or burrowd's in-cluster client (live). The returned report omits the DNS capability, which is a control-plane registry fact filled by the engine.

func DetectCertManager

func DetectCertManager(client kubernetes.Interface) (controlplane.CertManagerCapability, error)

DetectCertManager reports whether cert-manager is installed. It is exported for the same reason DetectCloudNativePG is: `burrow cluster postgres install` has to know, because the pgBackRest plugin's release manifest contains cert-manager Certificate and Issuer objects and applying it without cert-manager fails part-way through, leaving CRDs installed and no controller — and one detector is how the setup command and `burrow cluster` cannot disagree about the answer.

func DetectCloudNativePG

func DetectCloudNativePG(ctx context.Context, client kubernetes.Interface) (controlplane.CloudNativePGCapability, error)

DetectCloudNativePG reports the CloudNativePG operator's situation: whether its CRDs are served, whether a controller is actually running, and which release that controller is.

Present and Ready are separate for the reason detectIngress keeps them separate: a CRD is cluster-scoped and OUTLIVES the operator that installed it. Delete the cnpg-system namespace and every `postgresql.cnpg.io` CRD is still there, still served by discovery, and nothing reconciles a `Cluster` written against it — the object is accepted and then sits there. Reporting that as "CloudNativePG is installed" would be the orphan-IngressClass mistake on a component that holds tenant data.

It needs no new RBAC: API-group discovery needs none at all, and the cluster-wide apps/deployments get/list the capability ClusterRole already holds for ingress-controller and MetalLB detection covers the rest.

It is exported, unlike the survey's other detectors, so `burrow cluster postgres install` decides whether to install from the SAME read `burrow cluster` reports. The ingress path grew a second, separate presence check in cmd/burrow and the two disagreed about an orphaned install; one detector is how that does not happen twice.

func DetectPgBackRest

DetectPgBackRest reports the pgBackRest plugin's situation: whether its CRDs are served and whether its controller is actually running.

Present and Ready are separate for DetectCloudNativePG's reason, which is sharper here. A CRD is cluster-scoped and outlives the controller that installed it, and a `Stanza` written against a served CRD with no controller behind it is accepted and then reconciled by nothing — so the instance comes up, `archive_command` hands write-ahead log to a sidecar that was never injected, and the first sign is a database that will not archive.

NO VERSION IS REPORTED, unlike CloudNativePG's detector, and the absence is deliberate rather than an omission. The plugin's release artifact does not carry its own version anywhere Burrow can read back — the controller image tag in a tagged manifest has been seen to lag the tag it was published under — so a version read off the running Deployment would be a claim Burrow cannot stand behind about the component holding the backups. What Burrow targets is a constant; what is running is reported as present or not.

It needs no new RBAC: API-group discovery needs none, and the cluster-wide apps/deployments get/list the capability ClusterRole already holds covers the rest.

func LoadConfig

func LoadConfig() (*rest.Config, error)

LoadConfig resolves the cluster connection the way a control plane should: the in-cluster service account when running inside Kubernetes, otherwise the ambient kubeconfig (KUBECONFIG or ~/.kube/config). burrowd uses this.

func PgBackRestManifestURL

func PgBackRestManifestURL(version string) string

PgBackRestManifestURL is the release artifact for a plugin version: the CRDs, the RBAC, the controller Deployment and its cert-manager certificates in one document, at the publisher's own tag. Burrow ships no third-party bytes; it applies what the publisher serves.

func ReadResourceState added in v0.13.0

func ReadResourceState(ctx context.Context, client kubernetes.Interface) (controlplane.ClusterResourceState, error)

ReadResourceState reads node allocatable and pod requests read-only over the given clientset (issue #275): each schedulable node's .status.allocatable CPU/memory, and every non-terminal pod's summed resource requests with the node it is scheduled on. It performs only get/list reads — it never writes — and needs read on nodes (already granted for capability detection) plus a cluster-wide read on pods. It is a free function so the same read runs whether driven by the kubeconfig client or burrowd's in-cluster client. All of this comes from the Kubernetes API alone; no metrics-server is involved (that would add the separate live-usage layer, issue #276).

func ShipperImageForVersion

func ShipperImageForVersion(version string) string

ShipperImageForVersion returns the pinned shipper image for a stamped release version, so a released burrowd ships backups with the binary published under the SAME release tag. For an unstamped dev build (version "" or "v0.0.0") it returns "" — there is no published image at a pseudo-version — and the caller leaves the :latest default, or an explicit BURROW_SHIPPER_IMAGE override, in place.

Types

type Adapter

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

Adapter operates Burrow workloads in a single app namespace, and provisions add-ons in a separate add-on namespace (ADR-0025) so backing services don't mix with user workloads.

func New

func New(client kubernetes.Interface, namespace string) *Adapter

New returns an Adapter over the given clientset and namespace (defaulting to "default"). Tests inject a fake clientset; production injects a real one (see NewFromConfig).

func NewFromConfig

func NewFromConfig(cfg *rest.Config, namespace string) (*Adapter, error)

NewFromConfig builds an Adapter from a REST config and namespace.

It wires the dynamic client alongside the typed one, so a production adapter can address the custom resources Burrow creates (the CloudNativePG `Cluster` of ADR-0066 §1). Nothing else changes by having it: a cluster with no CloudNativePG has no such object, and every path that reads one answers "there is none" (getCNPGCluster).

func (*Adapter) AddonReady

func (a *Adapter) AddonReady(ctx context.Context, name string) (bool, error)

AddonReady reports whether the named add-on's backing workload is available (ADR-0025). Readiness is a live property of the cluster — the registry of what add-ons exist lives in the database — so this is a cheap single-object probe. A missing workload is reported as not ready (false, nil); only a real API error is returned.

WHICH OBJECT IS THE ADD-ON is resolved here rather than assumed, and that is what keeps a Postgres instance from reading as an ADR-0074 §6 discrepancy. §6's diagnosis is an ABSENCE — the registry says this exists and the cluster does not have it — and it is made by the failure observer from exactly this seam's answer. A Postgres instance has no Deployment by design: the operator reconciles a StatefulSet from the `Cluster` Burrow wrote. A probe that only looked for a Deployment would report a serving database as not running, once a minute, forever, and the ledger would carry an AddonNotRunning row about an add-on that is fine — a false absence, which is worse than no ledger at all because it is indistinguishable from a real one.

The Deployment is looked for FIRST because it answers for every other add-on in one Get. The custom resource is consulted only when there is none, so a cluster running no Postgres add-on pays nothing for this.

No new REASON is introduced by any of this. A `Cluster` that will not come up is an add-on whose workload is not available, which is what ReasonAddonNotRunning already says; the ledger's vocabulary is closed (ADR-0074 §5) and this does not widen it.

func (*Adapter) AddonVolumes

func (a *Adapter) AddonVolumes(ctx context.Context) ([]controlplane.AddonVolume, error)

AddonVolumes lists the add-on PersistentVolumeClaims in the add-on namespace — every claim Burrow created for an add-on, including the ones whose add-on has since been removed. It is the read that makes ADR-0064 §6 possible: a removed add-on leaves no registry row, so the claim it left behind can only be found by looking at the cluster.

Claims are identified by the LABELS Burrow writes at creation, never by a name prefix: a claim is Burrow's when it carries app.kubernetes.io/managed-by=burrow, and it is attributed to an add-on by the burrow.cloud/addon label recording the type. A user's own claim in this namespace carries neither and is not reported.

func (*Adapter) ApplyAutoscaler added in v0.8.0

func (a *Adapter) ApplyAutoscaler(ctx context.Context, app string, spec controlplane.AutoscaleSpec) error

ApplyAutoscaler creates or updates an autoscaling/v2 HorizontalPodAutoscaler named after app, targeting app's Deployment, with the requested replica band and utilization targets (ADR-0006). It mirrors ApplyWorkload's create-or-update-under-conflict-retry: the HPA controller continuously writes the object's status, so a get-then-update can lose the resourceVersion race and 409; we re-read and retry on conflict.

func (*Adapter) ApplyWorkload

func (a *Adapter) ApplyWorkload(ctx context.Context, spec controlplane.WorkloadSpec) error

func (*Adapter) AutoscalerActive added in v0.8.0

func (a *Adapter) AutoscalerActive(ctx context.Context, app string) (bool, error)

AutoscalerActive reports whether app has an active HorizontalPodAutoscaler owning its replica count. It gets the autoscaling/v2 HPA named after app: present means active, NotFound means inactive (false, nil, not an error). A workload apply consults it so it leaves the HPA-managed count untouched.

func (*Adapter) AwaitRollout

func (a *Adapter) AwaitRollout(ctx context.Context, app string, timeout time.Duration) (controlplane.RolloutOutcome, error)

AwaitRollout waits for app's newest revision to settle, or reports why it did not, bounded by timeout (ADR-0072 §4-§5). Every observable condition is a RolloutOutcome; the error return is reserved for a call that could not be made at all.

func (*Adapter) BackupJobPresent

func (a *Adapter) BackupJobPresent(ctx context.Context, backupID string) (bool, error)

BackupJobPresent reports whether the Job for a backup id still exists (ADR-0074 §6). It is a plain read, and it answers the one question the registry cannot: a row left `pending` by a burrowd that restarted mid-backup looks exactly like a backup still running, and the difference is otherwise discovered at restore time. A missing Job is absent (false, nil), not an error.

func (*Adapter) DeleteAddon

func (a *Adapter) DeleteAddon(ctx context.Context, name string, t controlplane.AddonType, deleteData bool) (controlplane.AddonRemoval, error)

DeleteAddon tears an add-on's workload down and, only when deleteData is set, destroys its data volume with it. The default is data-preserving: the PVC of a stateful add-on outlives the removal, so `addon remove` stops the instance without destroying what it holds (ADR-0025, ADR-0064 §1). The retained volume keeps its resource name, which is also the add-on name, so a re-install lands on exactly the same claim and the instance comes back with its data.

t is the add-on's TYPE, and it is TOLD rather than discovered. The registry recorded it at install, and it decides which teardown runs: a Postgres instance is a CloudNativePG `Cluster` (ADR-0066 §1) with no Deployment at all, and every other add-on is the Deployment below. A removal is the one operation that must not infer that — inferring means reading the cluster, and every way of failing to read the cluster looks like "the object is not there", which on this path would mean walking past a running database and deleting the registry row that named it. The teardown itself still refuses anything it cannot read (cnpg_remove.go).

func (*Adapter) DeleteAutoscaler added in v0.8.0

func (a *Adapter) DeleteAutoscaler(ctx context.Context, app string) error

DeleteAutoscaler removes app's HorizontalPodAutoscaler. A missing HPA is a no-op, not an error, so turning autoscaling off is idempotent.

func (*Adapter) DeleteWorkload

func (a *Adapter) DeleteWorkload(ctx context.Context, app string) error

func (*Adapter) DeployAddon

func (*Adapter) Expose

func (a *Adapter) Expose(ctx context.Context, spec controlplane.ExposeSpec) error

func (*Adapter) ExposureStatus

func (a *Adapter) ExposureStatus(ctx context.Context, app string) (controlplane.ExposureStatus, error)

func (*Adapter) ListWorkloads

func (a *Adapter) ListWorkloads(ctx context.Context) ([]controlplane.WorkloadStatus, error)

func (*Adapter) Logs

func (*Adapter) MetricsAPIAvailable added in v0.8.0

func (a *Adapter) MetricsAPIAvailable(ctx context.Context) (bool, error)

MetricsAPIAvailable reports whether the metrics.k8s.io API group is served, i.e. metrics-server is installed. It reads API-group discovery, which needs no RBAC (ADR-0034). A discovery error is returned so the caller can decide; the engine treats it as "absent" and warns rather than failing, so a probe hiccup never blocks applying an HPA.

func (*Adapter) PhysicalBackupPresent

func (a *Adapter) PhysicalBackupPresent(ctx context.Context, backupID string) (bool, error)

PhysicalBackupPresent reports whether the `Backup` object for a backup id is STILL GOING (ADR-0074 §6). It is the physical answer to the one question a `pending` row cannot answer about itself: is something still working on this, or is nothing ever going to finish it?

EXISTENCE IS NOT THE TEST HERE, which is where it differs from the Job path. A Job is reaped on success, so a Job that is gone is a backup that is over; a `Backup` object is owned by the `Cluster` and Burrow never deletes it, so it outlives the backup by the life of the instance. Asking only whether it exists would report every pending row as still running for ever, and the case the sweep exists for — a burrowd that restarted mid-backup, leaving the row pending while the operator went on and finished the backup — would never be caught. So the PHASE decides: a settled object is not something that will finish this row.

A missing object, an unwired dynamic client, an absent CRD and a refused read are all reported as absent, exactly as getCNPGCluster collapses them and for the same reason: on a cluster where the read cannot succeed, Burrow cannot have created the object either.

func (*Adapter) RestartWorkload

func (a *Adapter) RestartWorkload(ctx context.Context, app string, at time.Time) error

RestartWorkload bumps app's pod-template restarted-at annotation to at, triggering a rolling update so a running app picks up a secret change that envFrom reads only at pod start (ADR-0028). A missing Deployment is ErrNotFound — nothing running to roll.

func (*Adapter) RestoreInstance

RestoreInstance rewinds environment req.Environment's whole Postgres instance to a point in its pgBackRest repository (ADR-0066 §4).

The order is the safety, and every step is placed where it is because the step after it can fail:

  1. The instance's own `Stanza` is read and checked against the destination the caller resolved. This happens BEFORE anything is deleted, because a recovery pointed at a repository this instance never wrote to does not fail — it produces an empty database — and finding that out after the live instance has been removed would be the worst possible moment.
  2. The pre-restore `Cluster` is deleted, then its claims, then the claims are WAITED for. A `Cluster` created while the old claim still exists is reattached to it by CloudNativePG's own classifier instead of recovering, which would silently produce the exact state the operator asked to leave.
  3. The recovery `Cluster` is created under the instance's own name and waited for.

func (*Adapter) RunBackupJob

func (a *Adapter) RunBackupJob(ctx context.Context, app, env, backupID string, dest *controlplane.BackupDestination) (controlplane.BackupJobOutcome, error)

RunBackupJob pg_dumps app's database to the backup PVC via a one-shot Job and waits for it to finish (ADR-0032), and — when dest is non-nil — writes that dump on to the object store and reads it back before the Job is allowed to succeed (ADR-0063 §7). The dump command names no host or password: those come from libpq env, the password via secretKeyRef.

The two destinations are a TIER, not alternatives. The PVC is where pg_dump writes and where pg_restore reads, and ADR-0066 §4 keeps that single-app logical path deliberately; the object store is what takes the backup out of the database's failure domain, which is why ADR-0063 exists. A dump therefore always lands on the volume, and the Job's SUCCESS means whichever destination the caller resolved was actually reached — so the Backup row the caller writes from this outcome is never a claim about bytes that stayed in the cluster.

A failure returns the closed reason the Job reported alongside the error, so the caller records WHY rather than that it failed: the dump itself, a store that would not answer, a store that answered and refused, or a store that took the write and could not serve it back.

func (*Adapter) RunJob added in v0.12.0

RunJob runs spec.Command in a one-shot Job in the app namespace, built from the app's own current image and its config env plus per-app Secret via envFrom (ADR-0048 §2), then waits for it to finish and captures the pod's output and the container's exit code into a RunResult (ADR-0048 §3). A non-zero exit is a normal structured outcome, NOT an error: the error return is reserved for a launch, poll, or timeout failure. The finished Job is garbage-collected by Kubernetes' native ttlSecondsAfterFinished (ADR-0048 §7), set from spec.TTLSeconds — no imperative reap.

func (*Adapter) RunPhysicalBackup

func (a *Adapter) RunPhysicalBackup(ctx context.Context, env, backupID string, archive *controlplane.ArchiveDestination) (controlplane.PhysicalBackupOutcome, error)

RunPhysicalBackup asks CloudNativePG for a base backup of environment env's instance and waits for the answer (ADR-0066 §2).

It refuses BEFORE writing anything when the instance is not archiving. An instance whose `Cluster` carries no pgBackRest plugin has no repository to write a base backup to, and a `Backup` object created against it would sit in `pending` until the wait timed out — ten minutes to learn something a single read answers now, and the refusal can say what to do about it.

func (*Adapter) RunRestoreJob

func (a *Adapter) RunRestoreJob(ctx context.Context, app, env, backupID string) error

RunRestoreJob pg_restores app's dump from the backup PVC into its database via a one-shot Job and waits for it (ADR-0032). --clean --if-exists replaces current contents. Like backup, the command names no host or password.

func (*Adapter) ScaleWorkload

func (a *Adapter) ScaleWorkload(ctx context.Context, app string, replicas int32) error

func (*Adapter) SecretKeys

func (a *Adapter) SecretKeys(ctx context.Context, app string) ([]string, error)

SecretKeys returns the env-var names in app's per-app Secret, sorted, never the values (ADR-0028/0004). A missing Secret is an app with no secrets set: empty slice, no error.

func (*Adapter) SetSecretValue

func (a *Adapter) SetSecretValue(ctx context.Context, app, key, value string) error

SetSecretValue upserts key=value into app's per-app Secret (controlplane.AppSecretName(app)) in the app namespace, creating the Secret (Opaque, Burrow labels) if absent (ADR-0029). The value arrives here over burrowd's authenticated control-plane API and is written to the Kubernetes Secret; it never reaches a log, the audit log, Postgres, or the agent control channel (ADR-0029/0004). The returned error names the app and key only, never the value. It retries on conflict since a concurrent set/unset can race the resourceVersion.

func (*Adapter) Unexpose

func (a *Adapter) Unexpose(ctx context.Context, app string) error

func (*Adapter) UnsetSecretKey

func (a *Adapter) UnsetSecretKey(ctx context.Context, app, key string) error

UnsetSecretKey removes one key from app's per-app Secret (get, delete the key, update). A missing Secret or a missing key is a no-op, not an error. The value never crosses here — the caller passes only the key name (ADR-0004).

func (*Adapter) WithAddonNamespace

func (a *Adapter) WithAddonNamespace(ns string) *Adapter

WithAddonNamespace sets the namespace Burrow deploys add-ons (and their collectors) into, kept separate from the app namespace and the credential-holding control-plane namespace (ADR-0025). An empty value leaves the default. Returns the Adapter for chaining.

func (*Adapter) WithControllerPodPlacement

func (a *Adapter) WithControllerPodPlacement(p PodPlacement) (*Adapter, error)

WithControllerPodPlacement wires the operator's placement policy for pods Burrow causes a third-party controller to create — today the CloudNativePG `Cluster` behind the Postgres add-on (ADR-0066 §1). It is the third seam of ADR-0077 §2, alongside WithPodMutator (the app's own image) and WithPlatformPodMutator (Burrow's own images), and it does not overlap either: those two reach pods this package builds, and this one reaches pods it does not.

"Controller" here is the third-party controller that authors the pod. ADR-0077 calls these workloads "operator-authored"; this method says "controller" because "operator" already means two other things in this repository — Burrow's own Kubernetes operator under operator/, and the person operating this binary, whose policy is what this carries.

It can refuse, and that is the point

A returned error means some part of p has NO DESTINATION on a target Burrow writes, naming the exact JSON path. Refusing here rather than dropping it later is ADR-0077 §3: a CRD's structural schema PRUNES fields it does not know, silently and without an API error, so a `Cluster` written with an unknown placement field comes back without it and nothing anywhere says so. The operator who wired the hook believes their policy is in force. A database that refuses to start is recoverable; a database silently running unplaced is discovered during an incident.

Policy that has no field on ANY target — a runtimeClassName, a security context, a volume — cannot be expressed at all: PodPlacement has no such field, so it is a compile error rather than a runtime refusal. The refusal below is for the subtler case, where the vocabulary has a field and a specific target's schema turns out not to carry it.

The check runs against the placement schema recorded from the pinned CNPG release (cnpg_placement_schema.json), not against the CRD installed in the cluster. That is the honest limit: an install running a CNPG older than the pin can prune a field this check accepted. Reading the live CRD at start-up would close it and needs a cluster to do so.

Returns the adapter for chaining, or a nil adapter and an error so a caller cannot proceed on a policy that would not be carried.

func (*Adapter) WithDynamicClient

func (a *Adapter) WithDynamicClient(d dynamic.Interface) *Adapter

WithDynamicClient wires the client custom resources are read and written through — today the CloudNativePG `Cluster` behind a Postgres add-on instance (ADR-0066 §1).

It is separate from the typed clientset because it is separately OPTIONAL. An Adapter built with New (every unit test, and any embedder that has not wired one) has no dynamic client, and every custom-resource path then behaves exactly as it did before CNPG existed: no `Cluster` is found, nothing is created, and an add-on is its Deployment. NewFromConfig wires both, so burrowd always has it.

Returns the Adapter for chaining.

func (*Adapter) WithNamespace added in v0.7.0

func (a *Adapter) WithNamespace(ns string) controlplane.Kubernetes

WithNamespace returns a copy of the Adapter whose app-resource operations 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). The add-on namespace is unchanged, so add-ons still land in their own namespace. An empty ns, or ns equal to the current app namespace, returns the receiver unchanged, so default-environment behavior is identical to before environments existed. The copy is shallow: it shares the same clients (typed and dynamic) and ALL THREE placement seams — the app hook of ADR-0061, the platform hook of ADR-0073, and the controller placement of ADR-0077 — so an environment-scoped view applies the same policy, and a seam wired once at construction reaches every per-tenant view of the adapter. That is load-bearing: policy that survived only on the receiver would work in a single-namespace install and silently stop applying the moment an operation was routed to a named environment. No new connection is made per operation.

func (*Adapter) WithOperationalLimits

func (a *Adapter) WithOperationalLimits(f controlplane.ClusterConfigFunc) *Adapter

WithOperationalLimits registers the source of the operator-set operational limits this adapter reads (ADR-0068 §6). It is read at the moment the adapter acts rather than captured here, so `burrow cluster config set` takes effect without restarting burrowd. A nil supplier (the default) resolves every limit to its built-in default. Returns the Adapter for chaining.

func (*Adapter) WithPlatformPodMutator

func (a *Adapter) WithPlatformPodMutator(fn func(*corev1.PodSpec)) *Adapter

WithPlatformPodMutator registers a hook the adapter applies to the pod specs it authors that run BURROW's own images, after each is constructed and before the object is sent to the API server (ADR-0073 §2, §6). It is the platform-side counterpart of WithPodMutator, which covers the app's own image.

Its reach is every pod this adapter runs on Burrow's behalf rather than the app's: the add-on instance Deployment (Postgres, the logs and metrics stores, the cache), the log-collector DaemonSet, the metrics-collector Deployment, and the backup and restore Jobs. Without it those pods carry no placement fields at all, so on the tainted-pool cluster ADR-0061 was written for an operator has working deploys and a backup that never runs — and each failure is quiet. A Pending Job leaves both Failed and Succeeded at zero, so the waiter burns its full timeout and reports a timeout rather than an unschedulable pod; a Pending add-on reports zero ready replicas, which reads like a slow start. The worst case is the restore, discovered during an incident.

Two hooks rather than one, because the two sets take genuinely different policy: a managed operator may want the tenant's image under a sandboxed runtime on tenant-only nodes, and their own Postgres and collectors somewhere the tenant's code is not. One hook could serve that only by having the operator key off a container image or a label to reconstruct a classification this package already has, and a wrong branch puts the tenant's code on the platform pool. Which hook a path gets is decided here, and stated at each call site.

The mutator runs over the FULLY-constructed pod spec and on every write of it, so two obligations follow. It must be **idempotent** — appending to a slice (tolerations, volumes, env) without first checking whether the entry is already there will drift; set or replace rather than append blindly. And it must **tolerate pod specs it did not expect**: a backup or restore Job pod arrives with RestartPolicy Never already set, and the log-collector DaemonSet pod arrives with a blanket `Operator: Exists` toleration it is meant to keep (ADR-0073 §3 — a collector that skips tainted nodes silently loses exactly those nodes' logs). Overwriting either produces an object the API server rejects or a collector that stops collecting.

This hook is more dangerous than the app one, because it reaches STATEFUL workloads: the Postgres add-on holds tenant data, and a mutator that moves that pod to a pool where its volume cannot attach breaks the add-on rather than one deploy. The trust model is unchanged — the hook is compiled into the binary by whoever operates that binary, not supplied at runtime — but the blast radius is not.

Wiring nothing sandboxes nothing (ADR-0073 §5). This is a seam, not enforcement: the engine wires neither hook itself, and an operator who needs isolation enforced wants admission policy. A nil mutator (the default) leaves every object this adapter constructs byte-for-byte as it is today (ADR-0073 §4).

Returns the adapter for chaining.

func (*Adapter) WithPodMutator

func (a *Adapter) WithPodMutator(fn func(*corev1.PodSpec)) *Adapter

WithPodMutator registers a hook the adapter applies to the pod specs it authors for an app, after each is constructed and before the object is sent to the API server (ADR-0061). It is the deploy-path counterpart of BuildAdapter.WithBuildPodMutator (ADR-0053 §6).

Its reach is every pod this adapter runs the app's own image in: the Deployment's pod template, and the one-off command Job of ADR-0048. A run is the app's image, in the app's namespace, with the app's environment — the same workload for one command — so it is admitted and scheduled under the same cluster constraints, and a hook that covered only the Deployment would leave `burrow app run` unschedulable on precisely the clusters this seam exists for. Add-ons, collectors, and the backup and restore Jobs are NOT covered: they run images Burrow chooses rather than the app's, so they take WithPlatformPodMutator (ADR-0073 §2). The build Job has its own hook again (BuildAdapter.WithBuildPodMutator), for Burrow's builder image over the app's source.

It exists for cluster requirements the engine cannot know about, because they are properties of a cluster rather than of Burrow: a toleration for a tainted node pool (a GPU pool, spot capacity, a pool reserved for one team), a mandated runtimeClassName, a priorityClassName, a topologySpreadConstraint, a nodeSelector, an image-pull secret for a private base registry. Burrow hard-codes none of these — the operator embedding the engine supplies what their cluster requires. A nil mutator (the default) leaves every object this adapter constructs exactly as-is.

Unlike the build seam, whose Job is created once, this hook runs on EVERY write of the pod template — creates and updates alike, so a rollout does not drop what the deploy was given (ADR-0061 §2), and once more per run. The mutator must therefore be idempotent: appending to a slice (tolerations, volumes, env) without first checking whether the entry is already there will drift across redeploys. Set or replace rather than append blindly.

It must also tolerate a Job pod, not only a Deployment's: a run pod arrives with RestartPolicy Never already set, and a mutator that overwrites it produces a Job the API server rejects.

The hook is trusted and unvalidated: it can set anything on the pod spec, including breaking it. It is compiled into the binary by whoever operates that binary, not supplied at runtime.

Returns the adapter for chaining.

func (*Adapter) WithShipperImage

func (a *Adapter) WithShipperImage(image string) *Adapter

WithShipperImage overrides the image the backup Job's shipping container runs (ADR-0063 §7). An empty value leaves the default, so a caller can pass an unresolved override through without having to branch on it. Returns the Adapter for chaining.

func (*Adapter) WorkloadStatus

func (a *Adapter) WorkloadStatus(ctx context.Context, app string) (controlplane.WorkloadStatus, error)

type BuildAdapter added in v0.13.0

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

BuildAdapter is the production controlplane.Builder: it runs an in-cluster build as a Kubernetes Job in the dedicated burrow-builds namespace (issue #278, ADR-0053 §4). It clones the git reference inside the cluster, builds with buildah or Cloud Native Buildpacks, pushes to the target registry reference, and returns the resulting image digest — the immutable identity the resulting guarded deploy pins (ADR-0053 §4). Isolation lives INSIDE this implementation, not on the seam (ADR-0053 §6): the OSS path is single-tenant (§7), so the build runs under restricted PodSecurity as defense in depth, not as an adversary boundary — a hardened, sandboxed executor is the commercial product's job behind the same seam.

It lives under controlplane/ (not controlplane/internal) so cmd/burrowd and the managed module can wire it; it is licensed Apache-2.0.

func NewBuilder added in v0.13.0

func NewBuilder(client kubernetes.Interface) *BuildAdapter

NewBuilder returns a BuildAdapter over the given clientset. The build always runs in the dedicated burrow-builds namespace (issue #278), isolated from both the app and control-plane namespaces — the caller no longer chooses it. Tests inject a fake clientset; production injects a real one (see NewBuilderFromConfig).

func NewBuilderFromConfig added in v0.13.0

func NewBuilderFromConfig(cfg *rest.Config) (*BuildAdapter, error)

NewBuilderFromConfig builds a BuildAdapter from a REST config — the production wiring path, mirroring NewFromConfig for the Kubernetes seam.

func (*BuildAdapter) Build added in v0.13.0

func (b *BuildAdapter) Build(ctx context.Context, source controlplane.SourceRef, targetImage string, insecure bool, cred controlplane.SourceCredential) (string, error)

Build runs the in-cluster build to completion and returns the pushed image's content digest (ADR-0053 §4). Only the git reference and the target reference cross into the builder; the source is cloned inside the cluster, so no code travels over the control channel (ADR-0004, ADR-0053 §3). A clone, build, or push failure is returned as a structured error and nothing is pushed; the caller does NOT touch the deploy path on error (ADR-0053 §4). It blocks until the Job succeeds or fails, or the build timeout elapses.

func (*BuildAdapter) BuildAttributed

func (b *BuildAdapter) BuildAttributed(ctx context.Context, intent controlplane.BuildIntent, source controlplane.SourceRef, targetImage string, insecure bool, cred controlplane.SourceCredential, progress func(controlplane.DeployEvent)) (string, error)

BuildAttributed is BuildWithProgress, additionally recording what the build is FOR on the build Job itself (issue #504) — the app, the environment, and the reference its deploy pins. It is the one implementation the other two entry points delegate to.

THE JOB IS WHERE THE INTENT BELONGS, because the Job is what survives. It outlives the request that created it, it outlives the goroutine waiting on it, and it outlives burrowd; the caller's call frame outlives none of those. Recorded here, a build that succeeds after its caller has gone is still finishable by whoever is running when it finishes (see StrandedBuilds). The intent is small, non-secret metadata on an object that was being created anyway — a label and an annotation — and a zero intent records nothing, which is exactly what Build and BuildWithProgress do.

progress may be nil, meaning nobody asked to observe this build.

func (*BuildAdapter) BuildWithProgress

func (b *BuildAdapter) BuildWithProgress(ctx context.Context, source controlplane.SourceRef, targetImage string, insecure bool, cred controlplane.SourceCredential, progress func(controlplane.DeployEvent)) (string, error)

BuildWithProgress is Build, reporting the build's stages as the Job reaches them (issue #503): the clone, then the build, from what the Job's pod actually shows, plus a repeat of the running stage often enough that the response survives a proxy's read timeout. Its result and its errors are Build's, exactly — reporting is beside the build, never part of it.

func (*BuildAdapter) HoldBuild

func (b *BuildAdapter) HoldBuild(ctx context.Context, id, reason string) error

HoldBuild marks a build whose unattended deploy a guardrail did not allow. The build is LEFT IN PLACE: the image is good and has already been paid for, so a person who re-runs the same build with a confirmation reuses it (the deterministic Job name finds it Succeeded and returns its digest) instead of paying for the same minutes again. The marker is what stops the guardrail decision being re-recorded on every sweep; stampBuildIntent clears it when the build is re-run.

func (*BuildAdapter) ReapBuild

func (b *BuildAdapter) ReapBuild(ctx context.Context, id string) error

ReapBuild discards a build there is nothing further to do with, exactly as the ordinary success path does: background propagation, so the Job owner goes immediately and its pods and owned credential Secret are collected asynchronously. A Job already gone is not an error — the TTL controller and this call are both allowed to be the one that removed it.

func (*BuildAdapter) StrandedBuilds

func (b *BuildAdapter) StrandedBuilds(ctx context.Context, completedBefore time.Time) ([]controlplane.StrandedBuild, error)

StrandedBuilds lists the builds that SUCCEEDED and whose deploy never ran: build Jobs that carry intent, have completed successfully, are not already held, finished before completedBefore, and still have a readable digest.

Every one of those conditions is doing work. Intent is what makes a build finishable at all. Success is what makes it worth finishing — a failed build is left for diagnosis and its own retention reaps it. completedBefore is the caller's settling margin against racing the build's original caller. And a build whose digest cannot be read has nothing to deploy: reporting it would hand the control plane a build it can only fail at, so it is not reported (the ordinary path treats the same condition as a build failure rather than pinning a deploy to nothing).

func (*BuildAdapter) WithBuildImage added in v0.13.0

func (b *BuildAdapter) WithBuildImage(image string) *BuildAdapter

WithBuildImage overrides the build image (the buildah + Buildpacks bundle). An empty value leaves the default. Returns the adapter for chaining.

func (*BuildAdapter) WithBuildNamespace

func (b *BuildAdapter) WithBuildNamespace(ns string) *BuildAdapter

WithBuildNamespace overrides the namespace the in-cluster build Job (and any credential Secret) is created in. The default remains the dedicated burrow-builds namespace; an empty value leaves it. This parameterizes what is otherwise a constant for downstream callers that run builds in a different namespace — the managed product's per-tenant build namespaces (cloud ADR-0003) — without changing OSS behavior, which never sets it. Returns the adapter for chaining.

func (*BuildAdapter) WithBuildPodMutator

func (b *BuildAdapter) WithBuildPodMutator(fn func(*corev1.PodSpec)) *BuildAdapter

WithBuildPodMutator registers a hook the adapter applies to the build Job's pod template spec after it is constructed and before the Job is created. It is the ADR-0053 §6 seam's executor extension point: the managed product (cloud ADR-0003) uses it to run the build under a gVisor RuntimeClass with a non-privileged restricted security context, a hard activeDeadlineSeconds, and pod labels its egress NetworkPolicy selects — none of which OSS itself needs (OSS runs privileged, no RuntimeClass, per ADR-0059). A nil mutator (the default) leaves the OSS behavior exactly as-is. Returns the adapter for chaining.

func (*BuildAdapter) WithCapacityProber added in v0.13.0

func (b *BuildAdapter) WithCapacityProber(p controlplane.CapacityProber) *BuildAdapter

WithCapacityProber enables the pre-build scheduling-headroom check (issue #274): before creating a build Job, the adapter reads the cluster's capacity through the prober and refuses with an actionable error when no node has room for the build's request. A nil prober (the default) leaves the check off and the build proceeds. Returns the adapter for chaining.

func (*BuildAdapter) WithGitImage added in v0.13.0

func (b *BuildAdapter) WithGitImage(image string) *BuildAdapter

WithGitImage overrides the clone init-container image. An empty value leaves the default. Returns the adapter for chaining.

func (*BuildAdapter) WithOperationalLimits

func (b *BuildAdapter) WithOperationalLimits(f controlplane.ClusterConfigFunc) *BuildAdapter

WithOperationalLimits registers the source of the operator-set operational limits this adapter reads (ADR-0068 §6) — the build Job's retention and the unschedulable grace. It is read at the moment a build runs rather than captured here, so `burrow cluster config set` takes effect without restarting burrowd. A nil supplier (the default) resolves every limit to its built-in default. Returns the adapter for chaining.

type Credentials

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

Credentials is the production controlplane.Credentials adapter: it reads and writes vendor tokens in the single burrow-credentials Secret in the control-plane namespace (ADR-0023, ADR-0030). It reads on every call so a rotated token is picked up without a restart, and it writes a token value the engine received over burrowd's authenticated control-plane API. burrowd's Role grants `get` and `update` on exactly this one object, so this is its sole access to a Secret's contents.

func NewCredentials

func NewCredentials(client kubernetes.Interface, namespace, secret string) *Credentials

NewCredentials returns a Credentials reader over the given clientset, control-plane namespace, and Secret name (defaulting to burrow-credentials). Tests inject a fake clientset; production injects a real one (see NewCredentialsFromConfig).

func NewCredentialsFromConfig

func NewCredentialsFromConfig(cfg *rest.Config, namespace, secret string) (*Credentials, error)

NewCredentialsFromConfig builds a Credentials reader from a REST config.

func (*Credentials) SetToken

func (c *Credentials) SetToken(ctx context.Context, key, value string) error

SetToken upserts key=value into burrow-credentials, creating the Secret (Opaque) if absent so the command works against an install that has not yet created it (ADR-0030). The value arrives over burrowd's authenticated control-plane API and is written straight to the Secret; it never reaches a log, Postgres, or an API response — the returned error names the key only, never the value. It retries on conflict since a concurrent set can race the resourceVersion.

func (*Credentials) Token

func (c *Credentials) Token(ctx context.Context, key string) (string, error)

Token returns the token stored under key, or ErrNotFound when the Secret or the key is absent.

type PodPlacement

type PodPlacement struct {
	// NodeSelector restricts the pod to nodes carrying these labels. Merged into the target's own
	// node selector field; it does not replace a selector the controller sets for its own reasons.
	NodeSelector map[string]string
	// Tolerations let the pod schedule onto tainted nodes. This is the field the tainted-pool
	// cluster of ADR-0061 needs and the only one the managed product sets (ADR-0077 §5).
	Tolerations []corev1.Toleration
	// NodeAffinity steers the pod toward or away from nodes by label expression.
	NodeAffinity *corev1.NodeAffinity
	// PodAffinity co-locates the pod with other pods. Under CNPG this lands on
	// `additionalPodAffinity`: ADDITIONAL to whatever the controller generates, not a replacement
	// for it.
	PodAffinity *corev1.PodAffinity
	// PodAntiAffinity separates the pod from other pods. Under CNPG this lands on
	// `additionalPodAntiAffinity`, and CNPG's own instance anti-affinity (`enablePodAntiAffinity`,
	// `podAntiAffinityType`) still applies underneath — the target's replica-spreading policy is
	// the controller's business, not placement policy Burrow relays.
	PodAntiAffinity *corev1.PodAntiAffinity
	// TopologySpreadConstraints spread the pods across failure domains.
	TopologySpreadConstraints []corev1.TopologySpreadConstraint
}

PodPlacement is the placement policy Burrow carries to pods it CAUSES TO EXIST but does not author — [ADR-0077](../../docs/adr/0077-placement-policy-for-pods-burrow-does-not-author.md) §2's third seam, wired by WithControllerPodPlacement.

Why a third seam rather than a wider second one

ADR-0073's two hooks are func(*corev1.PodSpec) because Burrow builds the pod spec and hands it over before writing it. A CloudNativePG `Cluster` is authored by the CONTROLLER: Burrow creates a custom resource and CNPG composes the pod, and what CNPG accepts is `spec.affinity` (nodeSelector, tolerations, node and pod affinity) and `spec.topologySpreadConstraints` — not a pod template. There is no pod spec to hand a hook, so there is no way to reach the most consequential pod Burrow places without a seam shaped like what the controller actually consumes.

Synthesising a `PodSpec`, letting a hook mutate it, and scraping the fields back out was rejected in ADR-0077 §2: it invents a pod that never exists, and a field set on the fake spec that has no destination vanishes with nothing to notice it — the exact silent drop §3 is written to prevent.

It is the vocabulary, not CNPG's schema

The fields below are Kubernetes' own placement vocabulary, spelled with Kubernetes' own types. Nothing here is named after CNPG, so a second controller maps onto the same seam and a CNPG version bump is not a breaking change for anyone who wired it. Where a target's field is named differently — CNPG carries pod affinity as `additionalPodAffinity`, because its own generated anti-affinity is still there underneath — the translation absorbs the difference and the doc comment on the field says so.

It is a value, not a function

The two ADR-0073 hooks are functions because they run over a fully-constructed pod spec and may key their decision off what the engine composed (ADR-0073 §6). Here there is nothing to key off: Burrow does not compose the pod, so a function would be handed an argument invented for it. A value also makes ADR-0077 §3 exact — policy with no destination is refused when it is WIRED, because the whole policy is known then, rather than at some later write. And it removes ADR-0073 §6's idempotency obligation from the wiring author entirely: a value applied twice is the value.

The obligation on whoever wires it (ADR-0077 §4)

**Placement decides whether the database's volume can attach.** Under CNPG the controller manages the PersistentVolumeClaims, and a `Cluster` whose pods cannot reach their volumes is not a scheduling inconvenience — it is a database that will not start. Steering is not restricted here, because Burrow cannot know a cluster's topology, but it is the reason the managed product's own policy is one toleration and deliberately nothing else: k3s local-path volumes bind to one node, so any nodeSelector or affinity strands them (ADR-0077 §5).

The zero value carries no policy and leaves every object Burrow writes exactly as it would be otherwise, which is ADR-0073 §4's guarantee at this seam.

type PostgresProvisioner

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

PostgresProvisioner is the production controlplane.DatabaseProvisioner: it connects to an environment's Postgres add-on instance as the burrow_admin superuser and gives each app its own database and login role (ADR-0031). It reads that instance's superuser password from the Secret of the same name in the add-on namespace through a Kubernetes client (a pod can only mount a Secret in its own namespace, so the password lives there), and reaches the instance in-cluster at <instance>.<addon-ns>.svc:5432. It holds no long-lived database handle — it opens a short-lived connection per operation so a rotated superuser password is always picked up.

EVERY OPERATION IS SCOPED TO AN ENVIRONMENT (ADR-0067 §1). The provisioner has no notion of "the" instance: the environment argument selects the host and the credential together, so a call cannot reach an instance other than the named environment's, and a call that names no environment reaches none at all. That is what makes the issue #339 collision unrepresentable rather than merely avoided — `web` in staging and `web` in production are databases with the same name on different servers, and no code path can resolve one to the other.

func NewPostgresProvisioner

func NewPostgresProvisioner(client kubernetes.Interface, addonNamespace string) *PostgresProvisioner

NewPostgresProvisioner returns a provisioner over the given clientset and add-on namespace.

func NewPostgresProvisionerFromConfig

func NewPostgresProvisionerFromConfig(cfg *rest.Config, addonNamespace string) (*PostgresProvisioner, error)

NewPostgresProvisionerFromConfig builds a provisioner from a REST config.

func (*PostgresProvisioner) DropAppDatabase

func (p *PostgresProvisioner) DropAppDatabase(ctx context.Context, app, env string) error

DropAppDatabase drops app's database and login role from environment env's instance (ADR-0031). It validates env and app and quotes identifiers before any SQL. Dropping an already-absent database or role is a no-op (IF EXISTS), not an error. The database is dropped WITH (FORCE) so live sessions do not block teardown. The environment is required and unvalidated values are refused before the connection is opened: this is the destructive half of the pair, so reaching another environment's server here would drop a database that is still in use (ADR-0067 §1).

func (*PostgresProvisioner) EnsureAppDatabase

func (p *PostgresProvisioner) EnsureAppDatabase(ctx context.Context, app, env string) (string, error)

EnsureAppDatabase provisions (idempotently) an isolated database and login role for app on environment env's instance and returns its DATABASE_URL with a freshly generated password (ADR-0031). It validates env and app against the strict identifier patterns and quotes every identifier BEFORE any SQL runs. On a fresh attach it CREATEs the role and database and locks the database down to that role; on a re-attach (role or database already present) it ALTERs the role's password to rotate, so the returned URL is always current. The returned connection string is a SECRET value — the caller writes it straight into the app's Secret and never logs, audits, or returns it.

Idempotence is what made the missing environment dangerous rather than merely wrong (issue #339): finding an existing database is the NORMAL case of a re-attach, so a second environment's attach did not fail — it adopted the first environment's database and rotated its password. With the environment selecting the instance, the only database this can find is one on that environment's own server, and adopting it is again exactly what a re-attach should do.

func (*PostgresProvisioner) ListAppDatabases

func (p *PostgresProvisioner) ListAppDatabases(ctx context.Context, env string) ([]string, error)

ListAppDatabases returns the apps that hold a Burrow-provisioned database on environment env's instance, sorted (ADR-0031). It asks the instance itself rather than any registry, because the instance is the only place that knows: attach records the FACT of attachment nowhere but the app's own Secret and the databases on this server, and it is these databases — not a row somewhere — that a data-deleting add-on removal destroys.

The set is derived from ownership, not from names: a database whose owner is one of the app_<app> login roles attach creates is a provisioned app database, and its name is the app's name. That excludes the maintenance databases (postgres, template0/template1, all owned by the superuser) and anything a human created by hand as the superuser, without needing a naming convention to hold.

func (*PostgresProvisioner) QueryAppDatabase

QueryAppDatabase runs one statement against app's database on environment env's instance and returns its columns and rows (ADR-0087). It is the production controlplane.DatabaseQuerier.

IT CONNECTS AS THE APP'S OWN ROLE, NOT AS THE SUPERUSER, and everything about the shape of this method follows from that. It reads the connection string burrowd wrote into the app's Secret at attach — the credential it already minted, so no new secret and no new grant — and dials with it. The role is `app_<app>`, which holds CONNECT on its own database and on no other (EnsureAppDatabase revokes CONNECT from PUBLIC), so what the statement may touch is what the application itself may touch. The DATABASE IS CHOSEN BY THE CREDENTIAL rather than by the caller: there is no argument that names one, so no form of this call reaches the instance, `template1`, or another app's database (ADR-0087 §1).

Note what is deliberately NOT here: no `SET ROLE` and no `SET SESSION AUTHORIZATION` off a superuser connection. Both look equivalent and are not — a session that authenticated as a superuser can `RESET` its way back, so the statement would be one line away from the whole instance.

The credential is read here and spent here. It is never returned, logged, wrapped into an error, or handed back across the seam; the caller names the Secret's KEY and never sees its value (ADR-0029/0031).

Three bounds, from ADR-0087 §7. ONE connection, closed when the statement finishes. A STATEMENT TIMEOUT, applied as the connection's own `statement_timeout` so Postgres enforces it rather than a client that could decide not to wait. And a ROW CAP, at which the connection is CLOSED rather than drained — which is what makes the cap a bound on the work the instance does and not merely on the size of the answer.

func (*PostgresProvisioner) WithAdminEndpoint

func (p *PostgresProvisioner) WithAdminEndpoint(hostPort string) *PostgresProvisioner

WithAdminEndpoint overrides the host:port the provisioner dials for admin SQL (see adminEndpoint). It is for tests that reach the instance through a port-forward; production leaves it unset.

type Prober

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

Prober is the production controlplane.ClusterProber: it detects a cluster's read-only capabilities over a client-go clientset (ADR-0034). It wraps the same clientset burrowd already holds, so the live read needs only the narrow read-only ClusterRole the install grants (get/list on nodes, storageclasses, ingressclasses, and deployments) plus API-group discovery (no RBAC). It never writes.

func NewProber

func NewProber(client kubernetes.Interface) *Prober

NewProber returns a Prober over the given clientset.

func NewProberFromConfig

func NewProberFromConfig(cfg *rest.Config) (*Prober, error)

NewProberFromConfig builds a Prober from a REST config — burrowd's in-cluster config, so the live capability read uses the narrow read-only ClusterRole the install grants (ADR-0034).

func (*Prober) DetectCapabilities

func (p *Prober) DetectCapabilities(ctx context.Context) (controlplane.ClusterCapabilities, error)

DetectCapabilities reads the cluster's capabilities read-only, and — when this Prober knows the control-plane namespace — which shape the control plane's own database runs in.

func (*Prober) ReadResourceState added in v0.13.0

func (p *Prober) ReadResourceState(ctx context.Context) (controlplane.ClusterResourceState, error)

ReadResourceState reads the cluster's scheduling-capacity facts read-only (issue #275).

func (*Prober) WithControlPlaneNamespace

func (p *Prober) WithControlPlaneNamespace(namespace string) *Prober

WithControlPlaneNamespace tells the Prober where the control plane's own database lives, so it can report which of the two shapes is running (ADR-0086 §2). Only burrowd sets it: the namespace is its own, and without it that one capability is reported empty rather than guessed.

Returns the Prober for chaining.

func (*Prober) WithDynamicClient

func (p *Prober) WithDynamicClient(d dynamic.Interface) *Prober

WithDynamicClient wires the client the control-plane database's `Cluster` is read through. It is separate from the clientset because it is separately optional, exactly as it is on the Adapter: a Prober without one reads the plain Deployment and reports nothing about a CloudNativePG database, which is the right answer for a build that cannot address custom resources at all.

Returns the Prober for chaining.

Jump to

Keyboard shortcuts

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