cf_configuration

package module
v0.0.14 Latest Latest
Warning

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

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

README

caerus-framework-configuration

CI codecov License

Caerus Framework — configuration component.

Per-component, strongly-typed configuration with validated hot-reload. Each component registers its own config source and reads the current value through a typed accessor. Load order (later wins):

code zero value → file (optional) → env overlay (EnvPrefix)
→ flag overlay (flag tags) → AfterLoad → Validate

Files are the Kubernetes rotation plane (External Secrets → mount → fsnotify). Env overlays support local/CI/PaaS; fileless sources are allowed when EnvPrefix is set. Flags are a process-start overlay for the same fields (ParseFlags) — not a second config system; unknown flags and positional args are returned so subcommands survive. Values are swapped atomically after validation; rejected reloads keep the previous value.

Features

  • Per-component sources: every source is generic over its config type — no shared global config struct, no map[string]interface{} reads.
  • Fail-fast startup: AddSource builds + validates immediately and returns the error; a broken config never starts.
  • Max file size: each source file is capped at 1 MiB (MaxConfigFileBytes). That matches a Kubernetes ConfigMap/Secret object. A larger file is rejected (startup fails; reload keeps last-good). The cap is on-disk bytes before decode — it does not stop a YAML bomb from expanding in memory; prefer JSON in production.
  • Env overlay: EnvPrefix maps PREFIX + env/json field names onto the struct after the file decode (or onto a zero value when Path is empty). By design, an environment variable set to the empty string (FOO="") is treated like “not set”, so it does not clear a value that came from the config file.
  • Flag overlay: flag-tagged fields get a --<flag> via ParseFlags, applied after env and before AfterLoad. Long names only (stdlib flag).
  • Scalar and pointer fields: both are first-class. Prefer scalars when zero-value-is-default. Use *T only when absent must differ from an explicit value (non-zero code defaults, meaningful zero, reload presence). Env/flag overlays allocate pointer fields on first set; omitted keys stay nil. Do not pointer-wrap every setting for uniformity.
  • AfterLoad: hook for DSN/URL overlays (e.g. POSTGRES_DSN, VALKEY_URL) before Validate.
  • Validated hot-reload: fsnotify watches the file's directory. On change the file is re-read, env/AfterLoad reapplied, re-validated, then swapped.
  • Forced reload: Reload(name) / ReloadAll() re-apply env+AfterLoad even when file bytes are unchanged (see “Detecting env changes” below).
  • Reload dispatch: after a validated swap, the source's owner component (by name) gets OnConfigReload(source, cfg) with the fresh value if it implements cf.ConfigReloader. The owner is also notified once at Init with the source's initial value, and immediately from AddSource when the component is already initialized — so logs/observability (which cannot import this module) boot on defaults and then receive their real config.
  • Kubernetes-safe: watches the parent directory and re-stats the target, so ConfigMap/Secret symlink swaps are detected; identical content is deduplicated by hash (no spurious reloads). See docs/K8S.md.
  • Trusted paths: Source.Path and --<Name> are operator/Pod-spec inputs. The component is not a filesystem sandbox (see below).
  • Secret fields: tag credentials secret:"redact". Overlay and Get still hold the real value. LogArgs / SecretPresence are the only helpers that look at the tag — use them on reload summaries instead of logging the struct.

Secret fields (secret:"redact")

Configuration declares which fields are secrets. Logs prints [redacted]. Pick this one tag; do not invent a second convention.

type PostgresConfig struct {
    Host     string `json:"host" env:"HOST"`
    Password string `json:"password" env:"PASSWORD" secret:"redact"`
}
Wrong: slog.Info("reload", "cfg", cfg)          // dumps password
Right: slog.Info("reload", LogArgs(cfg)...)     // password=[redacted], password_set=true, host visible
Right: slog.Info("reload", "host", cfg.Host, SecretPresence(cfg)...)

Get / Lookup / env / flags are unchanged. Empty secrets log password_set=false and do not print [redacted]. Nested structs are not walked (same limit as env overlay). First consumers: postgresql password, valkey password, resend api_key.

Do not log overlay parse errors at Info with file bytes. Reload failures stay at Error with the parse error (JSON/YAML messages, not a dump of the file).

Usage

Configuration is always-on core: cf.New(&cf.FrameworkOptions{…}) registers logs → configuration → observability. main does not construct cf_configuration.New() or call ParseFlags — components own their sources (WithConfigSource / cf.ConfigSourceRegistrar), and the framework absorbs argv (registrar pass → ParseFlags) before Initialize / Run.

Golden path (same shape as caerus-framework-demoapp):

package main

import (
	"context"
	"log"
	"time"

	cf "github.com/caerus-framework/caerus-framework"
	cf_postgres "github.com/caerus-framework/caerus-framework-postgresql"
	cf_valkey "github.com/caerus-framework/caerus-framework-valkey"

	"example.com/myapp/internal/app"
)

func main() {
	fw := cf.New(&cf.FrameworkOptions{
		Logs: &cf.LogsSettings{
			Format:       "json",
			Level:        "info",
			ConfigSource: "logs", // core Source[LogConfig]; file config/logs.json
		},
		Observability: &cf.ObservabilitySettings{
			Bind:         ":9090",
			ConfigSource: "observability",
		},
		Components: []cf.CaerusComponent{
			// Module registers Source[PostgresConfig] itself (name, path, env, job).
			cf_postgres.New(
				cf_postgres.WithConfigSource("postgresql", "config/postgresql.json",
					cf_postgres.WithSourceEnvPrefix("POSTGRES_")),
				// Local only: WithMigrateOnInit(). Prod: --postgresql.job=migrate.
			),
			cf_valkey.New(
				cf_valkey.WithConfigSource("valkey", "config/valkey.json"),
			),
			app.New(app.Options{}), // may register a "demoapp" / app source the same way
		},
	})

	if err := fw.RunWithSignals(context.Background(),
		cf.WithShutdownTimeout(15*time.Second),
	); err != nil {
		log.Fatal(err)
	}
}

What the module does under WithConfigSource (you normally do not call this from main — stock chassis already do):

// Inside RegisterConfigSources / Init-time registrar (owner = c.Name()):
_ = cf_configuration.AddSource(cfg, cf_configuration.Source[cf_postgres.PostgresConfig]{
	Name:      "postgresql",
	Path:      "config/postgresql.json", // K8s-mounted file / symlink OK
	Format:    cf_configuration.FormatJSON,
	Owner:     c.Name(),
	EnvPrefix: "POSTGRES_",
	Job:       cf.JobSpec{Flag: "postgresql.job", Tasks: []string{"migrate"}},
	AfterLoad: func(c *cf_postgres.PostgresConfig) error {
		if dsn := os.Getenv("POSTGRES_DSN"); dsn != "" {
			return cf_postgres.OverlayDSN(c, dsn)
		}
		return nil
	},
	Validate: func(v *cf_postgres.PostgresConfig) error { /* … */ return nil },
})

Validate must return an error message that names the field and the constraint. For structured errors, return *cf_configuration.FieldError:

Validate: func(v *PostgresConfig) error {
	if v.MaxConns < 1 {
		return &cf_configuration.FieldError{
			Field: "max_conns",
			Err:   errors.New("must be >= 1"),
		}
	}
	return nil
},

Wrong vs right:

Wrong: return errors.New("invalid")
Right: return &FieldError{Field: "max_conns", Err: errors.New("must be >= 1")}
       → AddSource: source "postgresql": field "max_conns": must be >= 1

Read the current value after the configuration stage has initialized (prefer Lookup / Get so a missing source is an error, not a panic). Both functions return by value — you get a snapshot copy you can freely read and pass around. Mutating it does not affect the live config, and it will not go stale when a reload replaces the internal pointer.

func (c *CFPostgres) Init(ctx context.Context, fw *cf.CaerusFramework) error {
	cfg, err := cf_configuration.Lookup[cf_postgres.PostgresConfig](fwCfg, c.configSource)
	if err != nil {
		return err
	}
	// cfg is a PostgresConfig value (not a pointer). Use it directly.
	_ = cfg
	return nil
}

Receive reloads by implementing cf.ConfigReloader on the owner. The configuration component delivers the fresh value as cfg any (type-assert it) plus the source name:

func (c *CFPostgres) OnConfigReload(source string, cfg any) {
	if source != c.configSource {
		return
	}
	typed, ok := cfg.(*cf_postgres.PostgresConfig)
	if !ok {
		c.logger.Error("cf_postgres: config reload rejected", "source", source)
		return
	}
	// build new pool → ping → swap under mutex → close old (last-good on failure)
	_ = typed
}

Note: Prefer Lookup (or Get) in Init/reload so misconfiguration returns an error. MustGet panics on a missing source — fine for main/tests, not for reload paths. It is safe to call Get / Lookup / MustGet from within OnConfigReload: the configuration component releases its internal lock before invoking the callback, so there is no risk of deadlock.

AddComponent / bare cf.New() without options remain valid for tests and embedded use; production services should follow the FrameworkOptions shape above.

Flag overlay (ParseFlags)

Flags are a process-start overlay over the exact same fields as env — not a second config system. A field gets a --<flag> (stdlib flag, long names only) when its flag struct tag is set; flag:"-" opts out, and an absent tag means no CLI for that field. Bool fields (including *bool) register as bare flags (--tls, --tls=false); other scalars and pointer-to-scalars take a value (--host db.internal or --host=db.internal). Pointer fields stay nil when the flag/env key is omitted. Every source with a Path additionally gets a --<Name> file-path flag (default = the source's Path).

Contract:

  • Register first, parse second: every AddSource must run before ParseFlags so flag definitions exist. The framework enforces that order (registrars → ParseFlags). ParseFlags re-loads all sources with the flag values applied, and the parsed map is kept and re-applied on every later Reload / ReloadAll (flags do not hot-reload on their own; they are process-start only). ParseFlags is a one-shot in production — call it once at process start. Tests that need a second argv pass may call ResetFlags() first (test hygiene only; not for production).
  • Unique field flags: flag names are a process-wide namespace across all registered sources (including core logs / observability). The same --short-flag declared on two sources — or twice on one source — is a wiring error at ParseFlags, even when types match. Do not reuse short tags like flag:"host" across modules; pick distinct names (e.g. log-level, http-addr).
  • Per-source file-path flags: every source with a Path also gets a --<Name> flag (default = its Path). Providing it overrides where that source's file is read from — the file location is itself a per-source option. There is no "config directory" bootstrap setting; each source declares its own file, env and arg options. That override is trusted the same way Source.Path is (see “Trusted paths” below).
  • Unknown flags and positional args survive: the first unknown flag, single-dash arg, positional arg, or -- terminator moves the rest of the command line to the returned rest untouched — so serve / migrate / app flags fall through to the app.
  • Layering: flags win over env, env wins over file, AfterLoad runs last (DSN/URL merges see the final value).
  • Job flags: a module declares a job on its source with Source.Job (cf.JobSpec{Flag, Tasks}). Tasks is required when Flag is set — declaring a job flag without at least one task is a wiring error at AddSource. The flag names the instance and the value names the task to run on it (e.g. --postgresql.job=migrate). Jobs are CLI-only — the value never flows from env or file (the config struct carries no job field); ParseFlags registers --<Flag> as a string flag and JobRequests() (implements cf.JobSource) returns the parsed request(s) after argv absorption, validating the task against the declared Tasks (fail-fast before any data Init). Two job flags that name the same Owner (the same component Name()) are a JobRequests error: one job per target per process. A job flag colliding with a field flag or another source's job flag is a parse-time wiring error. Distinct Owners (for example --postgresql.job=migrate and --postgresql.orders.job=migrate on two named postgres instances) are two targets and both run.

In production binaries main never calls ParseFlags. The framework runs the registrar pass (every ConfigSourceRegistrar) then ParseFlags at the start of Initialize / Run / RunWithSignals. Leftover positionals / unknown flags are available as fw.LeftoverArgs() for the app. See demoapp cmd/demoapp/main.go for the full pattern.

Detecting env changes (with or without a file)

The process environment is not watchable (no inotify on environ). With a file present:

Trigger What happens
File bytes change (External Secrets, ConfigMap swap) Automatic: re-read file → re-apply current env → AfterLoad → notify owner
Env changes, file unchanged Nothing until something calls Reload / ReloadAll
Fileless source (EnvPrefix only) Same: use Reload after env changes

Recommended patterns:

  1. Kubernetes (preferred): put rotating secrets in mounted files; do not rely on env for rotation. File watch is the signal.
  2. Explicit refresh: call cfg.Reload("postgresql") from a SIGHUP handler, admin endpoint, or after a known env update in tests.
  3. Do not poll the environment in a tight loop.
// Example: SIGHUP re-applies env overlays and notifies ConfigReloaders.
go func() {
    for range sighupCh {
        _ = cfg.ReloadAll()
    }
}()

Component contract

Implements caerusframework.CaerusComponent:

  • Name()"configuration" (cf_configuration.ComponentName)
  • GetInitOrderStage()caerusframework.ConfigurationStage (second bootstrap stage, right after logs — so later components can read their config during Init)
  • GetDependencies()[logs]: the component logs through the framework logs component; the logger is re-delivered on logs Reconfigure. WithLogger(*slog.Logger) overrides the logger for tests/embedded use; without a logs component the fallback is slog.Default().
  • Init starts the watcher + reload loop; Shutdown stops them cleanly.
  • Does not implement cf.MetricsProvider (bootstrap — importing observability would cycle). Exposes MetricSamples() for observability's internal configurationMetricsCollector, which emits configuration_info (source count + comma-separated names on /metrics). Returns nil before any source is registered (lazy pickup).

Hot-reload semantics

Situation Behaviour
Initial load failure (missing file, oversized file, bad parse, validation error) AddSource returns an error; source not registered; startup continues to fail via the caller
Valid change detected New value swapped in atomically; owner OnConfigReload(source, cfg) called
Malformed content on reload Rejected; previous value kept; error logged
File larger than 1 MiB on reload Rejected; previous value kept; error logged
Validator rejects new value on reload Rejected; previous value kept; error logged
Content unchanged (e.g. K8s rewrites identical bytes) Skipped (sha256 dedup); no reload, no notification
Multiple configs in one directory Any event re-checks affected sources; hash dedup keeps it cheap and correct

Trusted paths (same uid as the process)

The configuration component opens whatever path you give it: Source.Path from WithConfigSource, or the --<Name> file-path flag (--postgresql=/some/file.json). If the process user can read that file, we try to parse it as JSON/YAML. That is normal Unix file permissions, not a second security layer.

A random HTTP client cannot set this path. Whoever writes the Pod spec, Helm args, or the binary’s WithConfigSource can. The trust boundary is the same as the process uid, not “the internet.”

filepath.Abs only turns ./foo into an absolute path. It is not a sandbox and does not stop --postgresql=/etc/shadow. (That path will usually fail JSON parse, but we still opened the file.)

Production: mount the file (ConfigMap/Secret) under a directory you chose and point Path at that mount. Reloading that file is the rotation plane. Do not treat --<Name> as a way to read arbitrary host files from untrusted argv.

Wrong: treat Path as a jail (“Abs means we cannot leave config/”).
Right: Path is trusted operator input; the uid of the process is the
       filesystem policy. A wrong --postgresql= is a Pod-spec mistake,
       same class as pointing at a 2 GiB dump (the 1 MiB cap then
       rejects the load).

There is no directory allowlist and no .. filter. Constraining argv on a shared host would be a new construct option; this module does not ship that.

Docs

  • docs/K8S.md — running on Kubernetes: ConfigMap/Secret mounts, symlink swaps, and what the watcher does about them.
  • ARCHITECTURE.md — component model and stage ordering.

License

Apache License 2.0 — see LICENSE.

Documentation

Index

Constants

View Source
const (
	// SecretTag is the struct-tag key.
	SecretTag = "secret"
	// SecretRedact is the only supported tag value: print [redacted] / presence,
	// never the cleartext, when using LogArgs / SecretPresence.
	SecretRedact = "redact"
)

Secret tag convention (this module owns the name; logs owns the appearance).

Password string `json:"password" env:"PASSWORD" secret:"redact"`

Overlay, Get, and Lookup are unchanged: the live value still holds the password. Only helpers in this file look at the tag, for logs and tests.

View Source
const ComponentName = "configuration"

ComponentName is the framework component name for the configuration component. It is the identifier other components use in GetDependencies to require configuration.

View Source
const MaxConfigFileBytes int64 = 1 << 20

MaxConfigFileBytes is the largest configuration file this component will read (1 MiB). That matches the Kubernetes ConfigMap/Secret object size. Typed chassis configs are far smaller; a bigger file usually means the process was pointed at the wrong path. The bound is on-disk size before decode — it does not cap YAML expansion in memory (prefer JSON in production).

Variables

This section is empty.

Functions

func AddSource

func AddSource[T any](c *Configuration, src Source[T]) error

AddSource registers and loads a configuration source on the given component. The value is built immediately (fail-fast); the source is not registered on failure. Reloads never fail the process: a rejected reload keeps the previous value. AddSource is safe to call before or after Init.

Path and/or EnvPrefix must be set. Format is required when Path is set.

func Get

func Get[T any](c *Configuration, name string) (T, bool)

Get returns a snapshot copy of the current value of the named source, typed as T. It reports false if the source does not exist or was registered with a different type. Returning by value means the caller owns a stable copy: it cannot accidentally mutate the live config, and the value does not go stale when a reload swaps the internal pointer.

func LogArgs added in v0.0.8

func LogArgs(cfg any) []any

LogArgs returns slog key/value pairs for a config struct (or pointer). Top-level exported fields only (same limit as env/flag overlay).

  • Unmarked scalars stay visible (host, port, …).
  • Fields tagged `secret:"redact"` become RedactedString plus `<json>_set`.
  • Nested structs are skipped. Do not slog.Any the raw struct instead.

func Lookup

func Lookup[T any](c *Configuration, name string) (T, error)

Lookup returns a snapshot copy of the current value of the named source, typed as T, or an error if it does not exist or was registered with a different type. Prefer Lookup (or Get) from Init and OnConfigReload so misconfiguration surfaces as error rather than panic.

func MustGet

func MustGet[T any](c *Configuration, name string) T

MustGet returns a snapshot copy of the current value of the named source, typed as T, or panics if it does not exist or was registered with a different type. Prefer Lookup in Init/reload; MustGet is crash-fast sugar for main and tests where a missing source is a programmer error.

func SecretPresence added in v0.0.8

func SecretPresence(cfg any) []any

SecretPresence returns only `<json>_set` bools for `secret:"redact"` string fields. Use on reload summaries when you already log host/port yourself.

Types

type Configuration

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

Configuration is the caerus-framework-configuration component. It owns a set of per-component configuration sources: each is loaded exactly once, watched for changes, and swapped atomically on a validated reload.

func New

func New(opts ...Option) *Configuration

New creates a configuration component. Add sources with AddSource (from any component's Init) and read the current value with Get/MustGet.

func (*Configuration) AddSourceValue

func (c *Configuration) AddSourceValue(src cf.ConfigSourceValue) error

AddSourceValue registers a configuration source from its generic-free declaration (cf.ConfigSourceValue). It is the cycle-free entry point for core modules (logs, observability) that the configuration module imports: the framework hands them the component as cf.ConfigSourceAdder and they call this with their own declaration. Sample's dynamic type selects the concrete config struct and decoding, exactly as Source[T].T would.

func (*Configuration) GetDependencies

func (c *Configuration) GetDependencies() []string

GetDependencies implements cf.Dependencies. The component logs through the framework logs component.

func (*Configuration) GetInitOrderStage

func (c *Configuration) GetInitOrderStage() cf.Stage

GetInitOrderStage implements cf.CaerusComponent. Configuration runs in the second bootstrap stage, right after logs, so later components can read their config during Init.

func (*Configuration) Init

Init implements cf.CaerusComponent. It starts the file watcher and the reload loop, and begins watching every source registered so far. Sources registered later (during other components' Init) are watched immediately.

After starting the watcher it notifies each source's owner with the already-loaded value. The core components initialize before configuration (logs) or without a Lookup path (logs again) — the initial notification is how they receive their configuration after booting on defaults.

func (*Configuration) JobRequests

func (c *Configuration) JobRequests() ([]cf.JobRequest, error)

JobRequests implements cf.JobSource. It inspects every registered source's declared job flag (the flag must have been parsed by ParseFlags) and reports the requested jobs: the flag names the instance (the source's Owner), the value names the task to run on it (e.g. --postgresql.job=migrate → run task "migrate" on the "postgresql" instance). A task outside the source's declared Tasks set is an error. Two flags that name the same Owner are an error: one job per target per process. CLI-only: file and environment values never produce a job request. Empty (no job flag provided) returns an empty slice.

JobRequests must run after argv absorption; the framework calls it before any component initializes. Sources are visited in registration order so a duplicate-Owner error names a stable pair of flags.

func (*Configuration) MetricSamples added in v0.0.14

func (c *Configuration) MetricSamples() []MetricSample

MetricSamples returns the configuration_info sample when at least one source is registered. Returns nil before any source is registered (lazy pickup on /metrics).

func (*Configuration) Name

func (c *Configuration) Name() string

Name implements cf.CaerusComponent.

func (*Configuration) ParseFlags

func (c *Configuration) ParseFlags(args []string) (rest []string, err error)

ParseFlags registers --<flag> for every currently registered source's flag-tagged fields and a --<source-name> file-path flag for every source with a Path, parses args, and re-applies the resulting values across all sources (flags win over env; env wins over file). The file-path flags override where each source's config file is read from; defaults are the sources' current paths, so an absent flag is a no-op.

Flags are a process-start overlay: the parsed field map is kept and re-applied on every subsequent Reload / ReloadAll; a path override persists on the source itself (reloads and the file watcher follow it).

Register all AddSource calls first so the flag definitions exist. Unknown flags and positional args are returned untouched — subcommands (`serve`, `migrate`) and app flags fall through to the caller.

func (*Configuration) Reload

func (c *Configuration) Reload(name string) error

Reload forces a re-load of the named source, reapplying env overlay and AfterLoad even when the file bytes are unchanged. Use this after an external process env change (for example from a SIGHUP handler). The process environment is not watchable; without Reload (or a file change), new env values are invisible. Returns an error if the source is unknown or the load is rejected (previous value kept). Notifies the owner on success when the effective value changed.

func (*Configuration) ReloadAll

func (c *Configuration) ReloadAll() error

ReloadAll forces Reload on every registered source. Owners are notified after all loads complete. The first load error is returned; later sources still run.

func (*Configuration) ResetFlags added in v0.0.14

func (c *Configuration) ResetFlags()

ResetFlags clears the process-start flag overlay. It is for tests only — production binaries call ParseFlags once at process start and never reset.

func (*Configuration) Shutdown

func (c *Configuration) Shutdown(ctx context.Context) error

Shutdown implements cf.CaerusComponent. It stops the watcher and waits for the reload loop to exit. Safe to call even if Init never ran.

func (*Configuration) Sources

func (c *Configuration) Sources() []string

Sources returns the names of registered configuration sources in sorted order. Returns nil when no sources are registered.

type FieldError added in v0.0.14

type FieldError struct {
	Field string
	Err   error
}

FieldError attaches a configuration field name to a validation failure. Return it from Source.Validate so AddSource and reload errors name the field and constraint:

if cfg.MaxConns < 1 {
    return &FieldError{Field: "max_conns", Err: errors.New("must be >= 1")}
}

func (*FieldError) Error added in v0.0.14

func (e *FieldError) Error() string

func (*FieldError) Unwrap added in v0.0.14

func (e *FieldError) Unwrap() error

type Format

type Format int

Format selects the on-disk encoding of a configuration file.

const (
	// FormatJSON parses JSON files with encoding/json.
	FormatJSON Format = iota
	// FormatYAML parses YAML files with gopkg.in/yaml.v3.
	FormatYAML
)

type MetricSample added in v0.0.14

type MetricSample struct {
	Name   string
	Help   string
	Value  float64
	Labels map[string]string
}

MetricSample is one bootstrap metric sample for observability to scrape. Configuration does not implement cf_observability.MetricsProvider — that would create an import cycle — so observability registers an internal collector that calls MetricSamples.

type Option

type Option func(*options)

Option configures the configuration component at construction time.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger overrides the logger used for reload/watcher diagnostics. By default the component logs through the framework logs component (declared in GetDependencies); WithLogger is an explicit override for tests and embedded use and wins over the framework logger. slog.Default() remains the fallback only when neither is available.

type Source

type Source[T any] struct {
	// Name is the logical name of this source (e.g. "mongodb"). It is the key
	// used by Get/MustGet and must be unique within the framework.
	Name string
	// Path is the configuration file path. It may be a symlink (as with
	// Kubernetes ConfigMap/Secret mounts), so the directory is watched and the
	// target is re-stat'ed on every event; see docs/K8S.md. Empty is allowed
	// when EnvPrefix is set (fileless / env-only source).
	//
	// A source with a Path also gets a --<Name> file-path flag in ParseFlags:
	// providing it overrides where the file is read from (defaults to this
	// Path). There is no "config directory" bootstrap setting — each source
	// declares its own file, env and arg options.
	//
	// Path is trusted: the process opens any file that uid can read. There is
	// no directory allowlist. The --<Name> override is the same trust (argv /
	// Pod spec), not a sandbox. filepath.Abs is not a jail.
	Path string
	// Format selects the file encoding. Ignored when Path is empty.
	Format Format
	// Owner is the Name of the component that consumes this configuration. On a
	// validated reload, the owner's OnConfigReload (if it implements
	// cf.ConfigReloader) is invoked. Empty disables reload dispatch.
	Owner string
	// EnvPrefix, when non-empty, overlays matching environment variables onto
	// the decoded value after the file is read (or onto a zero value when Path
	// is empty). Keys are EnvPrefix + `env` tag, or UPPER_SNAKE of the json
	// name. Example: EnvPrefix "POSTGRES_" and field `Host` → POSTGRES_HOST.
	EnvPrefix string
	// Job, when declared, registers a CLI-only job flag for this source's Owner.
	// The flag names the instance and the value names the task to run on it
	// (e.g. --postgresql.job=migrate); the framework reads the request via
	// cf.JobSource after argv absorption and routes it before serving. CLI-only:
	// the value lives in the parsed flag, never in the config file or
	// environment. Tasks lists allowed task strings and is required when Flag
	// is set (Flag without Tasks fails at AddSource). A value outside Tasks
	// fails JobRequests. Two sources must not set a
	// job flag for the same Owner in one process (JobRequests fails closed:
	// one job per target). The source must set Owner.
	Job cf.JobSpec
	// AfterLoad runs after file+env overlay and before Validate. Use it for
	// DSN/URL overlays (e.g. POSTGRES_DSN → OverlayDSN). Nil skips the step.
	AfterLoad func(*T) error
	// Validate runs after every successful load (initial and reload). It must
	// return nil for the new value to be accepted. On reload, a rejected value
	// keeps the previous one in effect.
	Validate func(*T) error
}

Source describes one configuration source and how to interpret it. It is generic over the concrete config type, so each component gets its own strongly-typed config with no shared global struct.

Load order (later wins): file (if Path set) → env overlay (if EnvPrefix set) → flag overlay (if ParseFlags ran and the struct has flag tags) → AfterLoad → Validate. Files are the Kubernetes rotation plane (External Secrets → mount → fsnotify); env is for local/CI/PaaS; flags are a process-start overlay and do not hot-reload by themselves.

Jump to

Keyboard shortcuts

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