Help us shape the future of Go! Take the 2026 Go Developer Survey

config

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package config resolves a project's typed configuration from proto field annotations at runtime, with no per-field generated loader.

The config object IS the proto message: a project holds a *configv1.AppConfig and calls LoadInto / RegisterFlags, which read the (forge.v1.config) options off the message descriptor and resolve every field through the canonical forge precedence — proto default < config file < environment variable < explicit CLI flag.

Three layers live here:

  • loader.go the descriptor walk and the precedence resolution.
  • filelayer.go the first-class config-FILE layer (--config, or the binary's <BINARY>_CONFIG), which loads only for an explicitly-given path and fails loudly on a missing or invalid one.
  • semantic.go the role-annotation helpers (Mode, Validate) and the cobra-facing surface.

The defining property of the semantic helpers: none of them match on a field's NAME. Mode keys off the field tagged role=CONFIG_FIELD_ROLE_MODE, Validate's TLS and CORS invariants key off their own role tags. Renaming an annotated field changes nothing; naming an UNannotated field "environment" grants it no behavior. Behavior follows the annotation, so a rename can never silently drop a guard.

Index

Constants

View Source
const ConfigFlag = "config"

ConfigFlag is the name of the always-registered persistent flag that points the loader at a config file. RegisterFlags registers it unconditionally.

Variables

This section is empty.

Functions

func BuildDefaultsStampError added in v1.0.0

func BuildDefaultsStampError() error

BuildDefaultsStampError is the reason the installed stamp was refused, or nil. Load returns it; it is exported for a binary that wants to report a broken stamp at startup, before any command runs.

func CheckCommandPath

func CheckCommandPath(cmd *cobra.Command, configName string, want ...string) error

CheckCommandPath refuses to load a command-scoped config from any command other than the one it is declared for. configName names the config message and want is its (forge.v1.binary_config).command — the cobra path below the binary's root, e.g. "server", "api".

It is the exact form of a check generate cannot make: whether a command path exists is decided by the cobra tree the binary assembles at runtime, so this compares against that real tree. A config whose declared path is wrong can then never load silently — every Load errors, naming the declared path and the command that called it.

A DETACHED command (no parent) is not checked: a test, or a binary loading a config outside its tree, has no path to compare. A nil cmd is not checked either, matching LoadInto's "cmd may be nil".

func CheckCommandSubtree added in v1.0.0

func CheckCommandSubtree(cmd *cobra.Command, configName string, subtrees ...[]string) error

CheckCommandSubtree refuses to load a subtree config from a command that lies under none of its subtrees. configName names the config message and subtrees are its (forge.v1.binary_config).subtrees paths, each below the binary's root ([]string{"daemon"} covers `daemon` and every command under it).

It is CheckCommandPath's twin for a config several commands load: its flags are persistent on each subtree's command, so a command outside every subtree would load it without them. A detached or nil cmd is not checked, as for CheckCommandPath.

func ConfigPathEnvFor

func ConfigPathEnvFor(binary string) string

ConfigPathEnvFor returns the environment variable that names binary's config file when --config is not given: the binary name upper-cased with every character that cannot appear in a variable name replaced by "_", plus "_CONFIG". "reliant" → RELIANT_CONFIG, "idp-provision-oneshot" → IDP_PROVISION_ONESHOT_CONFIG. "" for an empty binary.

It is the single derivation both halves use: the runtime, for a message carrying (forge.v1.binary_config).binary, and forge's generator, for the generated help and docs and for a project-global config (named after the project).

func DefaultsInto added in v0.1.46

func DefaultsInto(msg proto.Message) error

DefaultsInto populates msg with the defaults layer alone — the base LoadInto starts from before any file, environment or flag overlays it: every annotated field at its proto default, nested config blocks allocated, and sensitive fields empty. It reads nothing from the process and skips the required-field check (a required field with no default is legitimately empty here), so it is the hermetic stand-in for LoadInto that tests need: the config production would load with nothing set.

It errors only on a default that does not parse for its field's kind — a defect in the config proto, never a runtime condition.

func DefaultsTyped added in v0.1.46

func DefaultsTyped[T proto.Message]() (T, error)

DefaultsTyped is DefaultsInto's generic convenience, as LoadTyped is LoadInto's: it allocates a fresh T holding only the proto defaults. The generated config.Defaults() wraps it, and the generated test helpers construct components with that — the config Load returns when no file, environment variable or flag is set, rather than an empty message whose nested blocks are nil and whose defaults all read as zero.

func Load

func Load(cmd *cobra.Command, msg proto.Message, opts ...LoadOption) error

Load populates msg in place from cobra flags > environment variables > config file > proto defaults, for every field carrying a (forge.v1.config) option. It is the cobra-facing alias of LoadInto. cmd may be nil (env + defaults only). Sensitive fields resolve from env / Secret mount only.

func LoadInto

func LoadInto(cmd *cobra.Command, msg proto.Message, opts ...LoadOption) error

LoadInto populates msg in place using the full forge config precedence — later layers OVERRIDE earlier ones:

	defaults  →  build defaults  →  config file  →  environment  →  flags
	(earliest)                                                      (wins)

  - defaults: every annotated field's proto default (sensitive fields get
    none — an inline secret default would be a leak).
  - build defaults: the values this BUILD was stamped with (-ldflags -X
    into the generated pkg/config.BuildDefaults; builddefaults.go), keyed
    by env_var. A stamp that names no field, a sensitive field, or a value
    that does not parse fails the load.
  - config file: when a path is EXPLICITLY given via the --config flag or
    the binary's <BINARY>_CONFIG env var, the file is proto-native
    unmarshaled INTO msg, overlaying the defaults (see filelayer.go). A
    missing/invalid explicit file is a LOUD error — never a silent
    fallback. With no path given this layer is simply skipped (normal
    precedence, not a fallback).
  - environment: each field's env var overrides the file/default value.
    A variable set to "" is unset, whatever the field's kind.
  - flags: a flag changed on THIS invocation overrides everything.

It is the runtime, descriptor-driven loader: empty-env handling, required- field errors, and per-kind parsing all key off the (forge.v1.config) field options. cmd may be nil (file via <BINARY>_CONFIG + env + defaults only), so LoadInto works from non-cobra entrypoints too.

Durations: declare a duration-shaped field as google.protobuf.Duration in the config proto — env/flag layers parse the Go-duration string ("5s") into the message, and consumers read it with .AsDuration(). (A plain string field stays a string; the descriptor has no name heuristic.)

After the layers it checks required fields and then runs Validate (the TLS/CORS role invariants and allowed_values), so a loaded config has already passed every declared check: an out-of-set value fails the load, naming the field. Mode (role=MODE) and Validate live in this package too (semantic.go), as FREE FUNCTIONS over the message; Validate is for a message built by hand, which no load has checked. There is no parallel generated struct: a project holds the proto config type.

func LoadTyped

func LoadTyped[T proto.Message](cmd *cobra.Command, opts ...LoadOption) (T, error)

LoadTyped is the generic convenience: it allocates a fresh T, loads it, and returns it typed. T must be a pointer proto message type (e.g. *configv1.AppConfig). Usage:

cfg, err := config.LoadTyped[*configv1.AppConfig](cmd)

It mirrors the generated Load(cmd) (*Config, error) signature shape so a project can drop the generated loader and call this directly.

func RegisterConfigFlag

func RegisterConfigFlag(cmd *cobra.Command, pathEnv string)

RegisterConfigFlag registers the persistent --config <path> flag on cmd, with pathEnv as its environment twin: the variable that supplies the path when the flag is not given. An empty pathEnv registers a flag with no twin.

It is a no-op when cmd or a parent already carries --config — the first registration on a command path names the variable for that whole path. That is what makes it an override: call it on a binary's root command before the generated Register*Flags, and every load beneath that root reads the variable named here instead of the derived <BINARY>_CONFIG.

The generated pkg/config calls it for a project-global config, which carries no (forge.v1.binary_config) annotation to derive a name from.

func RegisterFlags

func RegisterFlags(cmd *cobra.Command, msg proto.Message) error

RegisterFlags registers one cobra flag per config-bound field of msg that carries a non-empty flag annotation, using the annotated name, default, and description. Sensitive fields are skipped (env/Secret only). It is the cobra-facing form of RegisterFlagsFor and is generic over ANY config message — no per-field codegen.

It ALSO registers a persistent --config <path> flag (ConfigFlag) — always, for every config message. This is the first-class config-FILE layer's entry point: it is on, not dormant. Load resolves the file from this flag (or its environment twin — <BINARY>_CONFIG for a message bound to a binary, see ConfigPathEnvFor) and unmarshals it as the layer between defaults and env. Registering it persistent means it is also visible to subcommands. The flag is registered idempotently so re-registering on the same command (or a parent that already has it) is safe.

Every cobra-level Register* also records msg on cmd as a config the command path reads, so a process loading several configs (a root config and a command config) reads them all from its one config file: the file's keys are the union of their fields (see filelayer.go).

func RegisterFlagsFor

func RegisterFlagsFor(flags *pflag.FlagSet, msg proto.Message) error

RegisterFlagsFor walks msg's fields and registers one cobra/pflag flag per field that carries a non-empty (forge.v1.config).flag. The flag's type matches the proto field kind (string/int32/int64/bool/float; duration messages register as a string flag — LoadInto parses "5s" → Duration), and its default is the proto option's DefaultValue.

It is the pflag-level primitive; the cobra-facing RegisterFlags(cmd, msg) in semantic.go wraps it. It recurses into nested config blocks.

Fields without a flag (typically secrets sourced only from env / Secret mounts) are intentionally skipped — defense-in-depth against shell-history / `ps` exposure of credentials.

func RegisterFlagsScoped

func RegisterFlagsScoped(flags *pflag.FlagSet, msg proto.Message, scope FlagScope) error

RegisterFlagsScoped registers the subset of msg's flags selected by scope onto flags. See FlagScope for why the split exists and which cobra flag set each half belongs on. ScopeAll is identical to RegisterFlagsFor.

func RegisterInheritedFlags added in v1.0.0

func RegisterInheritedFlags(cmd *cobra.Command, msg proto.Message) error

RegisterInheritedFlags registers EVERY config-bound leaf of msg — its own and those of the blocks it composes — as PERSISTENT flags on cmd, and NOT the --config file flag.

It is the registration for a SUBTREE config ((forge.v1.binary_config) .subtrees) whose binary also has a root config: called on each subtree's command, every command beneath inherits the flags, while --config stays the root config's (persistent on the binary's root, inherited everywhere). A subtree config with no root config beside it registers with RegisterPersistentFlags, which adds --config itself.

func RegisterLocalFlags

func RegisterLocalFlags(cmd *cobra.Command, msg proto.Message) error

RegisterLocalFlags registers EVERY config-bound leaf of msg — its own and those of the blocks it composes — as LOCAL flags on cmd, and NOT the --config file flag.

It is the registration for a command-scoped config whose binary also has a root config: the root config's RegisterPersistentFlags already put --config on the root, every subcommand inherits it, and a second declaration here would only shadow it. A command config with no root config beside it registers with RegisterFlags, which adds --config itself.

func RegisterOwnFlags

func RegisterOwnFlags(cmd *cobra.Command, msg proto.Message) error

RegisterOwnFlags registers a per-binary config's OWN half — the leaves declared directly on the message, skipping composed blocks — as LOCAL flags on cmd. `app admin --help` then lists exactly what admin reads, and two binaries may each declare a --port without colliding, because a local flag set belongs to one command.

The composed base is deliberately NOT registered here: the root already carries it persistently, and cobra merges inherited persistent flags into a subcommand's Flags(), so Load still resolves both halves in one pass.

func RegisterPersistentFlags

func RegisterPersistentFlags(cmd *cobra.Command, msg proto.Message) error

RegisterPersistentFlags registers EVERY config-bound leaf of msg — its own and those of the blocks it composes — as PERSISTENT flags on cmd, plus the --config file flag.

It is the registration for a binary's ROOT config, the binary-wide surface every one of its processes loads: called on the binary's root command, every subcommand inherits the flags, so `reliant server api --log-level debug` and `reliant --log-level debug server api` both resolve, and Load from any subcommand sees them (cobra merges inherited persistent flags into a subcommand's Flags()). A command-scoped config registers on its own subcommand with RegisterFlags instead, as LOCAL flags.

func RegisterSharedFlags

func RegisterSharedFlags(cmd *cobra.Command, msg proto.Message) error

RegisterSharedFlags registers a per-binary config's SHARED half — the leaves of the config blocks it composes (conventionally BaseConfig) — as PERSISTENT flags on cmd, so every subcommand beneath it inherits them and `app --log-level=debug admin` resolves. It also registers the --config file flag, exactly as RegisterFlags does.

Call it once on the ROOT command, then RegisterOwnFlags on each binary subcommand. The two calls partition the message: no flag is registered twice, and a binary's own flags stay invisible to its siblings.

func SetBuildDefaults added in v1.0.0

func SetBuildDefaults(stamp string, msgs ...proto.Message)

SetBuildDefaults installs the build-time defaults layer from stamp (the URL-query-form string -ldflags -X wrote into the generated pkg/config.BuildDefaults) for every config message in msgs — the configs this binary declares, which bound the keys a stamp may name. The generated pkg/config calls it from init; a hand-assembled binary calls it before its first Load.

It never fails itself: a stamp it refuses makes every later Load fail with the reason, so a command that loads no config (--help, version) still runs, and one that does cannot run on a stamp nobody checked. An empty stamp clears the layer.

func Validate

func Validate(msg proto.Message) error

Validate runs the cross-field and closed-set config invariants that cannot be expressed as per-field defaults, failing fast (before the listener binds) on the known misconfiguration classes. It is the annotation/TYPE-driven replacement for the deleted name-matched validators (CORS-wildcard / TLS-pair / log-format):

  • allowed_values: a string field carrying a closed value set is a string ENUM; a resolved value outside the set is rejected. (True proto enum fields need no check — protoreflect enforces their domain. The empty value is always allowed: an unset, non-required field.)
  • TLS keypair: the fields tagged role=TLS_CERT and role=TLS_KEY are both-or-neither. Exactly one set is an error (the server would silently serve plaintext).
  • CORS: a wildcard origin ("*") in the role=CORS_ORIGINS field combined with role=CORS_ALLOW_CREDENTIALS=true is spec-invalid and rejected.

Every check keys off the ANNOTATION, never the field name — renaming a field never silently drops its guard. Validate recurses into nested config blocks so a block can carry its own allowed_values fields. The cross-field TLS/CORS checks operate per message level (the role fields of the same message), which is where those pairs naturally live.

Then it runs the message's buf.validate rules (rules.go), the invariants a config declares for itself: field ranges and message-level CEL.

Types

type FlagScope

type FlagScope int

FlagScope selects WHICH of a config message's flags a registration call covers. It exists for per-binary config, where one message's flags are split across two cobra flag sets rather than all landing on one command.

A per-binary config composes a SHARED block (conventionally BaseConfig, holding port/log level/mode — every binary wants each of them, as its own value) alongside the leaves only that binary reads. Those two halves want different cobra homes:

  • the shared block's leaves are PERSISTENT flags on the root command, so `app --log-level=debug admin` works and one definition serves every subcommand;
  • the binary's own leaves are LOCAL flags on that binary's subcommand, so `app admin --help` lists exactly what admin reads and two binaries can each define a `--port` without colliding.

Splitting by scope rather than registering everything twice is what keeps ownership disjoint at the CLI: a flag registered under ScopeOwn belongs to one subcommand's flag set and no other command can see it.

Loading is deliberately NOT scoped — LoadInto walks the whole message, and cobra's Flags() on a subcommand already merges inherited persistent flags (so Changed()/Lookup() resolve a root flag from the subcommand). One load call therefore fills both halves with the normal precedence.

The GENERATED per-binary surface does not split a message: since (forge.v1.binary_config).command scopes a config to one subcommand, each message has one flag home — a binary's root config registers ScopeAll persistent on the root (RegisterPersistentFlags), a command config ScopeAll local on its subcommand (RegisterLocalFlags / RegisterFlags). ScopeShared and ScopeOwn remain for a tree assembled by hand.

const (
	// ScopeAll registers every config-bound field, blocks included. It is the
	// single-config default and what RegisterFlags/RegisterFlagsFor use.
	ScopeAll FlagScope = iota
	// ScopeShared registers ONLY the leaves of composed config blocks — the
	// shared base a per-binary config embeds. Intended for the root command's
	// persistent flag set.
	ScopeShared
	// ScopeOwn registers ONLY the leaves declared directly on the message,
	// skipping composed blocks — the fields this binary alone reads.
	// Intended for a binary subcommand's local flag set.
	ScopeOwn
)

type LoadOption added in v1.0.0

type LoadOption func(*loadOptions)

LoadOption narrows what a load demands of the configuration.

func RequiredWithin added in v1.0.0

func RequiredWithin(paths ...string) LoadOption

RequiredWithin makes `required: true` apply only to the named fields — the ones the process reaches, by path from the root message ("database_url", "stripe.secret_key"). A process running one root of the composition graph loads with the fields that root reaches (cmd/<bin>/cmd/roots_gen.go), so a worker that never opens the database does not demand DATABASE_URL, and its workload is not handed the credential to satisfy it. Every field still loads and validates; only the "must be set" demand narrows.

type Root added in v1.0.0

type Root struct {
	// Name is the root's name (//forge:root <name>); Args the command path
	// that runs it (`<bin> <args>`).
	Name string
	Args []string
	// Vars is every environment variable it reaches, across every config
	// its process loads, in declaration order; Families the env families.
	Vars     []RootVar
	Families []RootFamily
	// Configs is what its process loads, root config first.
	Configs []RootConfig
}

Root is one root of the composition graph as a process.

func (Root) Env added in v1.0.0

func (r Root) Env() []string

Env is the non-sensitive variables the root reaches: ROOTS' env.

func (Root) Outside added in v1.0.0

func (r Root) Outside(env map[string]string) []string

Outside is the keys of env the root never reads, sorted: a variable — or a secret — handed to a process that does not need it.

func (Root) Reaches added in v1.0.0

func (r Root) Reaches(name string) bool

Reaches reports whether the process reads the variable name: a reached field's variable or alias, or one under a reached env family's prefix. The prefix is owned, so a misspelt entry under it is the load's to report — as the family it fails to match — not an unread variable.

func (Root) RequiredSecrets added in v1.0.0

func (r Root) RequiredSecrets() []string

RequiredSecrets is the credentials the root reaches and requires: ROOTS' required_secrets, which every workload running the root is handed.

func (Root) Secrets added in v1.0.0

func (r Root) Secrets() []string

Secrets is the credentials the root reaches: ROOTS' secrets.

func (Root) Validate added in v1.0.0

func (r Root) Validate(env map[string]string) []error

Validate runs what the root's process runs at boot over env instead of the process environment — each config it loads, with the reach its command demands, no flags — and returns every problem, not the first: a required variable missing, a value the process would refuse (unparseable, outside allowed_values, a misspelt env-family entry, a role invariant), and, as an *UnreachedVarError each, every variable of env the root never reads. nil: the process would start on env.

env is the process's whole environment as the deployment renders it. A variable naming a config file is honoured as at boot, so the file is read from this machine.

type RootConfig added in v1.0.0

type RootConfig struct {
	// Message is the config message (any value of its type).
	Message proto.Message
	// Reach is the fields of it the root reaches, by path.
	Reach []string
}

RootConfig is one config a root's process loads, with the paths of it the root reaches — what its command passes Load<Msg>Reach.

type RootFamily added in v1.0.0

type RootFamily struct {
	Prefix, Suffix string
	Config, Path   string
}

RootFamily is one env family a root reaches: a map field whose entries are every variable named Prefix<key>Suffix.

type RootVar added in v1.0.0

type RootVar struct {
	// Name is the variable; Aliases are the other names the process also
	// reads the field from (env_var_aliases), in order.
	Name    string
	Aliases []string
	// Config is the config message that declares the field, Path its path
	// from that message ("database_url", "stripe.secret_key").
	Config, Path string
	// Type is the field's type: a Go type ("string", "int32", "bool",
	// "[]string"), or "duration" for a google.protobuf.Duration.
	Type string
	// Required: the process refuses to start without it. Sensitive: a
	// credential, delivered as a secret.
	Required, Sensitive bool
}

RootVar is one environment variable a root reaches.

type RuntimeMode

type RuntimeMode int

RuntimeMode is the typed runtime mode. The zero value is ModeProduction: a message with no role=MODE field (or one whose mode field is empty) always reports production — dev permissiveness is never the default.

const (
	// ModeProduction is the zero value: full production posture (auth required).
	ModeProduction RuntimeMode = iota
	// ModeDevelopment enables local-dev permissiveness. Never set in
	// deployed environments.
	ModeDevelopment
)

func Mode

func Mode(msg proto.Message) RuntimeMode

Mode derives the runtime mode from msg's role=MODE field: "development"/"dev" (case-insensitive) → ModeDevelopment, anything else (including no role field) → ModeProduction. Deriving — rather than caching — means a hand-built message in tests can never disagree with its own mode value.

func (RuntimeMode) IsDev

func (m RuntimeMode) IsDev() bool

IsDev reports whether the mode is ModeDevelopment. It gates NON-SECURITY dev ergonomics only (permissive CORS, verbose errors, dev DB defaults). It MUST NOT gate authentication — auth is enforced in every mode.

type UnreachedVarError added in v1.0.0

type UnreachedVarError struct {
	Root, Name string
}

UnreachedVarError is a variable in an env the root never reads.

func (*UnreachedVarError) Error added in v1.0.0

func (e *UnreachedVarError) Error() string

Jump to

Keyboard shortcuts

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