localconfig

package
v0.14.0-rc.13 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: 9 Imported by: 0

Documentation

Overview

Package localconfig models Burrow's client-side selector state: the human-edited ~/.burrow/config file that names environment handles and records which one a command targets (ADR-0036). A handle maps a user-chosen name to {context, control-plane namespace, app namespace}; the current selection is either a pinned handle or, by default, whatever kube context kubectl points at ("follow"). This is selector state like the kubeconfig, never agent configuration, so it lives client-side: both `burrow` (the operator CLI) and `burrow-agent` (the agent control channel) consume this package, hence it is a shared top-level package rather than living under cmd/burrow.

It also holds the TARGET the CLI points at (ADR-0078): the managed product, or a Kubernetes cluster the person holds a kubeconfig context for. A target is the layer above an environment handle — it says which control plane, where the handle says which environment inside it — and, for a cluster, it records the context NAME only and never a copy of the credential.

This package is foundation only (ADR-0036 slice 1): the config model plus the resolution that decides the active target. Command wiring (`burrow env`, install, `burrow-agent`) lands in later slices.

Index

Constants

View Source
const (
	// APIVersion is the schema version stamped into the config header so the format can be
	// migrated safely across Burrow versions (ADR-0036).
	APIVersion = "burrow.dev/v1"
	// Kind identifies the document, mirroring the Kubernetes-style header on ~/.kube/config.
	Kind = "Config"
	// DefaultControlPlaneNamespace is where burrowd runs unless a handle says otherwise. The
	// dimension is carried from day one so a future multi-burrowd-per-cluster setup is a
	// non-default value, not a breaking change (ADR-0036).
	DefaultControlPlaneNamespace = "burrow"
)
View Source
const (
	CloudEndpoint    = "burrow-cloud.dev"
	CloudAPIEndpoint = "console." + CloudEndpoint
)

The managed product is NAMED by one host and ADDRESSED at another, and the two constants below exist because collapsing them into one is what broke sign-in: the apex serves the marketing website and answers every API path with a 404, so a device flow aimed at it can never start.

CloudEndpoint is the IDENTITY. It is the target's Name, the `endpoint` field written into ~/.burrow/config, and the credential filename (internal/cloudcred.File). It is keyed to the product rather than to wherever its API happens to be served, so moving the API does not strand the credentials and targets already on disk — which is why this value must not change.

CloudAPIEndpoint is the ADDRESS. It is the console: the host the managed control plane actually answers on, and the only one anything connects to or points a person's browser at. It is derived from CloudEndpoint so the two cannot drift apart.

The rule for choosing between them: prose that NAMES the product takes CloudEndpoint; anything somebody or something CONNECTS TO takes CloudAPIEndpoint.

Variables

This section is empty.

Functions

func Exists

func Exists() (bool, error)

Exists reports whether the config file is present, for first-run detection.

func Path

func Path() (string, error)

Path resolves the config location: $BURROW_CONFIG when set, otherwise ~/.burrow/config (mirroring how the kubeconfig resolves $KUBECONFIG else ~/.kube/config).

Types

type Cluster

type Cluster struct {
	Context   string
	Target    string
	Kind      TargetKind
	Endpoint  string
	InstallID string
}

Cluster is which CLUSTER a privileged command acts on: the guardrail, cluster-setting, add-on, credential and audit commands that reach a cluster without being scoped to one application ([ADR-0084](../docs/adr/0084-everyone-who-uses-burrow-carries-their-own-token.md) §4).

It is a third entry point beside Resolve and ResolveOperate because the question is a different one, and answering it with either of those would be wrong in a way that is hard to see. Those two resolve an ENVIRONMENT: they fold in a pinned handle, its namespaces, and the burrowd environment name to send with the operation. A privileged command is not scoped to an environment, and [ADR-0036](../docs/adr/0036-environment-selection.md) is deliberate that a pinned handle must never redirect one — pinning `staging` is a statement about which apps are being operated, not about which cluster is being administered. So this asks only "which cluster", and a pin is never consulted.

Context is the kubeconfig context to connect to, and it is EMPTY when no target is selected. That is the pre-ADR-0078 world, and an empty context is how the caller keeps reaching it exactly as it did: through the kubeconfig's current context, chosen by whatever `kubectl config use-context` was last run on.

Target names the selected target and is empty when none is, Kind is that target's kind, and Endpoint is set only for a Burrow Cloud target — which names no cluster at all, so its Context is empty for a reason the caller has to decide what to do about.

InstallID is the install the target was pointed at (ADR-0084 §5), sent on every request so a control plane that turns out to be a different install refuses rather than acts. It travels with the context because the two answer the halves of one question: the context is how the request gets there, and the id is what says it arrived. It is empty when no target is selected, when the target predates install ids, or when it is the managed product.

func ResolveCluster

func ResolveCluster(cfg *Config, kubeconfigPath string) (Cluster, error)

ResolveCluster decides which cluster a privileged command acts on, from the selected target alone.

Three outcomes, and each is a different thing for the caller to do:

  • No target selected. The zero Cluster, with no error: the caller follows the kubeconfig's current context, exactly as every command did before targets existed. This is still the default and nothing about it changes.
  • A Kubernetes target. Its context name, checked against the kubeconfig first so a target that has gone stale is caught here — where the message can name the target and the context — rather than at connect time, where it surfaces as a kubeconfig error about a cluster the reader did not know they were reaching for.
  • A Burrow Cloud target. No context, and Cloud reports true. There is no cluster to name.

A pinned environment handle is not consulted in any of the three; see Cluster.

func (Cluster) Cloud

func (c Cluster) Cloud() bool

Cloud reports whether the selected target is the managed product. It names no cluster, so a caller that can only act on one has to either refuse or say out loud which cluster it fell back to.

func (Cluster) Selected

func (c Cluster) Selected() bool

Selected reports whether a configured target decided this cluster. When it is false the caller is in the pre-ADR-0078 world and follows the kubeconfig, which stays the default.

type Config

type Config struct {
	APIVersion    string        `yaml:"apiVersion"`
	Kind          string        `yaml:"kind"`
	Current       string        `yaml:"current,omitempty"`
	Environments  []Environment `yaml:"environments,omitempty"`
	Targets       []Target      `yaml:"targets,omitempty"`
	CurrentTarget string        `yaml:"currentTarget,omitempty"`
}

Config is the on-disk selector state. APIVersion/Kind form the migratable header; Current names the pinned handle, or is empty to follow the current kube context (the default); Environments is the set of named handles.

Targets/CurrentTarget are the ADR-0078 layer above the handles: WHERE the control plane is (the managed product, or a cluster you hold a kubeconfig context for), chosen with `burrow auth login`. They are additive — with no target recorded the CLI resolves exactly as it did before, following the kubeconfig — and they never hold a credential (see target.go).

func Load

func Load() (*Config, error)

Load reads and parses the config from Path. A missing file is not an error: it returns a zero/empty Config (first run). Use Exists to detect the first-run case. An empty file is tolerated as first-run; a present apiVersion/kind is validated so future migrations are safe.

func (*Config) ActiveTarget

func (c *Config) ActiveTarget() (Target, bool, error)

ActiveTarget returns the target commands act against, and whether one is selected at all. No target selected is the pre-ADR-0078 world and is not an error: the CLI then behaves exactly as it did, following the kubeconfig. A CurrentTarget naming an unregistered target is an error, and is caught on load by validateTargets.

func (*Config) Add

func (c *Config) Add(env Environment) error

Add registers a new handle. The name must be non-empty and not already in use.

func (*Config) Lookup

func (c *Config) Lookup(name string) (Environment, bool)

Lookup returns the handle with the given name, and whether it was found.

func (*Config) LookupByContext added in v0.8.0

func (c *Config) LookupByContext(context string) (Environment, bool)

LookupByContext returns the handle registered for a kube context name, and whether one matched. It backs follow-mode resolution, where the current context is matched to a handle, and `burrow-agent`'s per-context resolution of the scoped agent kubeconfig (ADR-0038).

func (*Config) LookupTarget

func (c *Config) LookupTarget(name string) (Target, bool)

LookupTarget returns the target with the given name, and whether it was found.

func (*Config) Remove

func (c *Config) Remove(name string) error

Remove deletes the named handle. Removing the pinned handle reverts the selection to follow mode. It errors if the name is not registered.

func (*Config) Rename

func (c *Config) Rename(oldName, newName string) error

Rename changes a handle's name, carrying the pin if the renamed handle was pinned. The new name must be non-empty and unused; the old name must be registered.

func (*Config) Save

func (c *Config) Save() error

Save writes the config to Path, creating ~/.burrow (0700) as needed and writing the file 0600. The apiVersion/kind header is always stamped.

func (*Config) SetAgentCredential added in v0.8.0

func (c *Config) SetAgentCredential(name, kubeconfig, context string) bool

SetAgentCredential records the scoped agent kubeconfig path and its context on the named handle, backing the ADR-0038 phase 3 backfill (`upgrade` and a re-run `install` join provisioning the scoped credential onto a handle registered before it existed). It reports whether a handle by that name was found; the caller Saves.

func (*Config) SetEnvironmentContext

func (c *Config) SetEnvironmentContext(name, context string) bool

SetEnvironmentContext re-points the named handle at a different kube context, which is what a renamed context needs: the handle is the record of which cluster an environment lives on, and nothing else in the config can be corrected to fix a name that no longer resolves. It reports whether a handle by that name was found; the caller Saves.

It deliberately leaves any scoped agent credential alone. That credential holds the cluster's address and CA rather than a context name (ADR-0038), so a rename does not invalidate it, and silently discarding it here would take away the very access the handle exists to carry.

func (*Config) SetInstallID

func (c *Config) SetInstallID(context, installID string) bool

SetInstallID records the id of the Burrow install reachable through a kubeconfig context on every local record that names that context — Kubernetes targets and environment handles alike — and reports whether anything was updated. The caller Saves.

Both, because the two are read by different callers and each one alone leaves a hole: the `burrow` CLI resolves through a target, and `burrow-agent` resolves through a handle. Recording only on the target would check the operator and exempt the agent, which is the thing actually deploying.

It is addressed by CONTEXT rather than by name because that is what the writer knows. `burrow cluster install <context>`, its join path, and `burrow cluster upgrade` are told a context and learn an id from the cluster they just reached; which target or handle happens to point at that context is this file's business, not theirs. Nothing registered for the context is an ordinary outcome, not an error: installing does not require having run `burrow auth login` first, and an id with nowhere to be recorded is simply not recorded (ADR-0084 §5).

More than one record may name the same context. All of them are updated, because they all reach the same install, and leaving one behind would make the mismatch fire on which record happened to be selected rather than on the cluster having actually changed.

func (*Config) SetTarget

func (c *Config) SetTarget(t Target) error

SetTarget records a target and makes it active, replacing any existing entry with the same name (re-authenticating against a target you already have is an ordinary thing to do). The caller Saves.

Replacing the entry wholesale CLEARS any install id it carried, and that is correct rather than an oversight. Re-pointing at a context is exactly what somebody does after rebuilding the cluster behind it, so carrying the old id forward would preserve a mismatch through the act meant to resolve it. This command contacts no cluster and cannot learn the new id, so it leaves the target unchecked until an install or a join records one — unchecked is the state every target was in before ids existed, and it is served.

func (*Config) SwitchTarget

func (c *Config) SwitchTarget(name string) error

SwitchTarget makes an already-recorded target active without re-authenticating (ADR-0078 §4). It errors, naming what is registered, when the name is not one of them. The caller Saves.

func (*Config) TargetNames

func (c *Config) TargetNames() string

TargetNames returns the registered target names, sorted, joined for an error or a hint. It reads "none" when there are no targets, so a message never trails off into an empty list.

type ContextMissingError

type ContextMissingError struct {
	Name    string
	Context string
	Pinned  bool
}

ContextMissingError reports that a target or an environment handle names a kube context the kubeconfig no longer holds. It is a type rather than a formatted string so the commands that REPORT on the configuration — `burrow env list`, `burrow auth status` — can recognise it and mark the stale entry, instead of failing in place of the command that would have failed.

Name is the target or handle, Context the name it records, and Pinned distinguishes the two wordings: the fixes differ, and an error that names the wrong command is barely better than none.

func (*ContextMissingError) Error

func (e *ContextMissingError) Error() string

type Environment

type Environment struct {
	Name                  string `yaml:"name"`
	Context               string `yaml:"context"`
	ControlPlaneNamespace string `yaml:"controlPlaneNamespace,omitempty"`
	AppNamespace          string `yaml:"appNamespace,omitempty"`
	Env                   string `yaml:"env,omitempty"`
	// AgentKubeconfig is the path to the self-contained, burrowd-only kubeconfig `burrow cluster install`
	// mints for the scoped agent credential (ADR-0038), written under ~/.burrow/ (never
	// ~/.kube/config). AgentContext names the single context inside it. Both are empty for handles
	// created before the scoped credential existed or joined out of band; consumers fall back to
	// the ambient kubeconfig then. The operate path (`burrow-agent` and the CLI) reads them to reach
	// burrowd with the scoped credential; `install` (fresh mint or join), `env list --discover`, and `upgrade`
	// write them.
	AgentKubeconfig string `yaml:"agentKubeconfig,omitempty"`
	AgentContext    string `yaml:"agentContext,omitempty"`
	// InstallID is the id of the Burrow install this handle's cluster is running (ADR-0084 §5). It
	// is recorded here as well as on a target because the two are read by different callers: a
	// target is what the `burrow` CLI resolves through, and a handle is what `burrow-agent` resolves
	// through. The agent is the primary caller — it is the thing doing deploys — so a check that
	// only covered targets would leave the deploy path, which is most of the failure the record is
	// about, unprotected.
	//
	// Empty for every handle registered before install ids existed, and for one joined to a control
	// plane that predates them. An empty id sends no header and is served.
	InstallID string `yaml:"installID,omitempty"`
}

Environment is a user-named handle resolving to a kube context and the namespaces the environment lives in (ADR-0036). ControlPlaneNamespace defaults to DefaultControlPlaneNamespace when empty; AppNamespace empty means callers fall back to the burrowd default app namespace.

Env is the burrowd-registered environment NAME a command sends with each operation, which burrowd maps to the operation's namespace and per-environment guardrails. Empty means the cluster's default app namespace and the global guardrails (the cluster-per-environment case, where the whole cluster is the environment); a namespace-per-environment handle carries the same name it was registered with via `burrow env add`. It is deliberately distinct from AppNamespace, which is for display only: burrowd resolves a registered NAME, not a raw namespace.

type Mode

type Mode string

Mode is how the active target was selected: pinned to a named handle, or following the current kube context.

const (
	// ModePinned means a handle was pinned with `burrow env use`.
	ModePinned Mode = "pinned"
	// ModeFollowing means the target tracks the current kube context (the default).
	ModeFollowing Mode = "following"
	// ModeTargeted means an ADR-0078 target chosen with `burrow auth login` decided the cluster,
	// and no pinned handle inside that cluster narrowed it further.
	ModeTargeted Mode = "targeted"
)

type Resolved

type Resolved struct {
	Name                  string
	Context               string
	Namespace             string
	ControlPlaneNamespace string
	Env                   string
	Mode                  Mode
	Target                string
	Kind                  TargetKind
	Endpoint              string
	AgentKubeconfig       string
	AgentContext          string
	// InstallID is the install this resolution expects to be talking to (ADR-0084 §5). It comes from
	// the selected target when there is one and from the registered handle otherwise, so the check
	// covers both the CLI's targeted path and the handle-based path `burrow-agent` resolves through.
	// A context that matches neither carries none, and sends no header.
	InstallID string
	// ContextStale is set when the PINNED handle this resolution came from records a kube context the
	// kubeconfig no longer holds, and the resolution proceeded anyway because the handle carries a
	// scoped credential that reaches the cluster without one (issue #488). It is nil on every other
	// resolution, including the one that refuses.
	//
	// It is data handed back rather than a message written out for the same reason ContextMissingError
	// is a type: this package resolves and reports, and the commands decide what to say. The caller is
	// expected to say it once — the recorded name is wrong and correcting it is one command — and then
	// carry on, since the credential is what the connection is made with either way.
	ContextStale *ContextMissingError
}

Resolved is the concrete target a command will act against, derived from the config and the kubeconfig. Namespace is the app namespace (for display); empty means the caller falls back to the burrowd default app namespace. Env is the burrowd-registered environment NAME to send with the operation (empty means the cluster's default namespace and global guardrails); it is what burrowd resolves, not a raw namespace. In follow mode Name (and Env) are empty when the current context matches no registered handle (an "unregistered" current context). AgentKubeconfig/AgentContext carry the resolved handle's scoped, burrowd-only credential (ADR-0038) so the operate path can default to it; both are empty when the handle records none. Target names the ADR-0078 target that decided the cluster, and is empty when no target is selected (the pre-ADR-0078 behaviour, and still the default). Kind is that target's kind, and Endpoint is set only for a Burrow Cloud target: the endpoint the target NAMES, in place of the kube context a cluster target resolves to. For the managed product that is CloudEndpoint, the identity, and the caller turns it into an origin — a step that exists because the identity and the API's address are two different hosts (see CloudAPIEndpoint).

func Resolve

func Resolve(cfg *Config, kubeconfigPath string) (Resolved, error)

Resolve decides which environment a command targets (ADR-0036, ADR-0078).

When an ADR-0078 target is selected it decides the CLUSTER first, since that is what the target says: a Kubernetes target resolves to its kubeconfig context, and a pinned handle applies only when it is a handle inside that same cluster (a pin for a different cluster is not a narrowing of this one). A Burrow Cloud target has no kubeconfig to resolve and is reported as such.

With no target selected the behaviour is as it was. When a handle is pinned (cfg.Current set), it resolves to that handle, erroring clearly if the pinned name is not registered — or if the context it records is no longer in the kubeconfig AND the handle has no scoped credential to reach the cluster with, which is the check a renamed context needs and the one that was missing. A handle that does carry one resolves, and reports the stale name on ContextStale instead. Otherwise it follows the kubeconfig's current context: the target is that context, its namespace (so kubens moves Burrow too; empty when the context sets none, leaving the burrowd default to apply), and the default control-plane namespace. If the current context matches a registered handle by context name, that handle's Name and Env (the burrowd env name to send) are surfaced; otherwise both are empty.

func ResolveOperate

func ResolveOperate(cfg *Config, kubeconfigPath string) (Resolved, error)

ResolveOperate is Resolve for the ordinary application-facing commands — deploy, status, logs and the rest — which can act through EITHER kind of target, because the control plane they call is the same API whether it is reached through a cluster's API server or over HTTPS at the managed product (ADR-0078 §1).

It is a separate entry point rather than a change to Resolve on purpose. A caller that genuinely needs a cluster — anything that reads a kubeconfig, mints a scoped credential, or installs something — keeps getting the refusal it gets today, so forgetting to opt in fails safe with a legible message instead of silently acting on whatever cluster the kubeconfig happens to point at.

func (Resolved) Cloud

func (r Resolved) Cloud() bool

Cloud reports whether this resolution is the managed product, which is reached over HTTPS with the credential sign-in stored rather than through a kubeconfig. It is the one branch a caller needs: everything else about a cloud resolution — no context, no namespace, no scoped kubeconfig — falls out of there being no cluster.

func (Resolved) Render

func (r Resolved) Render() string

Render names a resolved target for display on a command, so the target is never ambiguous (ADR-0036, ADR-0078). Examples:

prod
"do-nyc1" (no environment registered)
kube context "do-nyc1-dev" (no environment registered)
"burrow-cloud.dev" (the managed product)

It names the environment and nothing else. The kube context and the namespace are how Burrow FOUND that environment rather than what it is, and they are exactly the Kubernetes vocabulary the rest of the CLI keeps out of sight; `burrow auth status` reports them for anyone who wants them.

The remaining branches have no environment name to give, so each says what it did resolve through — the selected target, or the kube context being followed — and says plainly that no environment is registered for it. Naming the context there is not the mechanism leaking back in: it is the only name the resolution has.

type Target

type Target struct {
	Name     string     `yaml:"name"`
	Kind     TargetKind `yaml:"kind"`
	Context  string     `yaml:"context,omitempty"`  // Kubernetes only: the kubeconfig context name
	Endpoint string     `yaml:"endpoint,omitempty"` // Burrow Cloud only: the host signed in to
	// InstallID is the id of the Burrow install this target was pointed at (ADR-0084 §5). The context
	// name says HOW TO GET THERE; the id says WHETHER YOU ARRIVED, and the two answer different
	// questions because a context name is a label rather than an identity. It is user-controlled, it
	// is reusable, and providers generate it deterministically — `doctl kubernetes cluster kubeconfig
	// save` writes names like `do-nyc3-burrow`, so destroying a cluster and standing another one up
	// produces a byte-identical context name for an entirely different cluster. With only the name
	// recorded, the target follows it there and says nothing.
	//
	// It is not a secret and authorises nothing: it identifies an install so a mismatch can be named.
	// The CLI sends it on every request and the control plane refuses a request that names an install
	// it is not.
	//
	// It is optional, and validate deliberately does not require it. Every target recorded before
	// this field existed has none, targets are written by `burrow auth login`, which contacts no
	// cluster and so has nothing to learn an id from, and a target with no id is served exactly as it
	// was — the check is a refinement of an existing relationship, not a new precondition on it.
	InstallID string `yaml:"install_id,omitempty"`
}

Target is a control plane the CLI can point at, recorded in ~/.burrow/config.

A Kubernetes target stores the kubeconfig context NAME and NEVER a copy of the credential (ADR-0078 §1). The kubeconfig stays the single source of truth, so rotating it, re-issuing a certificate, or having a cloud provider's CLI manage it all keep working with nothing here going stale. A copied credential is a credential nobody remembers to rotate.

A Burrow Cloud target likewise stores no token: only the endpoint it signs in to. Where the token lives is cloud ADR-0028's decision, not this file's.

func CloudTarget

func CloudTarget() Target

CloudTarget builds the target for the managed product.

func KubernetesTarget

func KubernetesTarget(context string) Target

KubernetesTarget builds a target for a kubeconfig context, named after the context so a person selects a cluster by a name they already recognise.

func (Target) Describe

func (t Target) Describe() string

Describe renders what a target IS, for `burrow auth status` and for any message that has to name one.

type TargetKind

type TargetKind string

TargetKind distinguishes the two kinds of target (ADR-0078 §1).

const (
	// TargetKindCloud is the managed product. The credential is a token obtained by signing in, and
	// it is never written here (cloud ADR-0028 owns it).
	TargetKindCloud TargetKind = "burrow-cloud"
	// TargetKindKubernetes is a cluster with Burrow installed in it. The credential is the
	// kubeconfig the person already has, used exactly as ADR-0014 already uses it.
	TargetKindKubernetes TargetKind = "kubernetes"
)

A target is where the control plane is (ADR-0078 §1). There are two kinds and deliberately not three: the managed product, or a Kubernetes cluster the person holds a kubeconfig context for. A managed control plane operated on somebody else's cluster is a shape the roadmap keeps open and is not modelled here, because inventing a kind for it now would be inventing the product it is a target for.

Jump to

Keyboard shortcuts

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