Files
member-console/internal/config/overlay.go
T
cgalo5758 ad7a219adf Enforce schema and boot invariants
Enforce 10j's verified gaps (schema-hardening change):

- Migration 00010: partial unique indexes for one default pool and one
  primary assignment per workspace, plus CHECKs pinning
  pool/provider/subscription vocabularies and provider lifecycle
  timestamps.
- Workspace creation shares a transactional provisioning function;
  extension validates its target pool; last-tier deletion of a defaulted
  ladder is guarded; signup completes plan-less on a broken ladder.
- Boot asserts integration slug parity and validates declared config
  enums; Stripe invoice amounts are range-checked; domain cancellation
  runs a final evidence probe; rule authoring is additive-only.
2026-08-22 18:02:46 -05:00

213 lines
7.0 KiB
Go

package config
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/spf13/viper"
)
// Override is one operator-set row from core.integration_config_overrides,
// as loaded by the composition root. Only key and value matter to the
// overlay; the audit columns stay in the store.
type Override struct {
Key string
Value string
}
// Source identifies the layer a key's boot-effective value came from.
type Source string
const (
// SourceOverride: an operator-set row in core.integration_config_overrides.
SourceOverride Source = "override"
// SourceEnvironment: any non-default boot source — environment
// variable, config file, or changed flag. (Viper cannot cheaply
// distinguish these from each other, and the settings surface does
// not need to.)
SourceEnvironment Source = "environment"
// SourceDefault: the declared ConfigKey.Default. A key explicitly set
// to exactly its default value is also reported as default — the
// label describes the effective value, not the write history.
SourceDefault Source = "default"
)
// Effective is a key's resolved value at boot, in display-string form
// (list values comma-joined), with the layer that won. Secret keys are
// never recorded.
type Effective struct {
Value string
Source Source
}
// bootEffective is the snapshot ApplyOverlay records for the operator
// settings surface: declared non-secret key → effective value + winning
// source. Written once during single-threaded boot, read-only afterwards —
// which is also why saving an override cannot move it: changes apply on
// restart, and the surface derives its "pending restart" indicator from
// exactly this staleness.
var bootEffective = map[string]Effective{}
// BootEffective returns a copy of the boot-time effective-value snapshot.
// Empty until ApplyOverlay has run.
func BootEffective() map[string]Effective {
out := make(map[string]Effective, len(bootEffective))
for k, v := range bootEffective {
out[k] = v
}
return out
}
// ApplyOverlay layers operator-set overrides over the environment. Each
// override is validated against its key's declaration — secret keys are
// never overridable, enum keys must hold a member — coerced to the key's
// declared default type (list keys comma-split, bool/int keys parsed), and
// set into viper at its highest-precedence layer, so override →
// environment → default resolution falls out of viper's own layering for
// every existing read path.
//
// It must run exactly once, during single-threaded boot, after migrations
// and before any route or worker registration: viper's internals are not
// synchronized for concurrent Set/Get, and every integration captures its
// config at registration time. Nothing re-reads overrides at runtime;
// changes apply on restart.
//
// An override whose key matches no declaration is skipped — the operator
// surface lists such rows as unrecognized. An override that fails
// validation or coercion aborts startup with the offending key and its
// remediation: save-time validation makes that state reachable only by
// hand-written SQL, and a silently ignored (or half-applied) config is
// worse than a named refusal to boot.
func ApplyOverlay(specs []ConfigKey, overrides []Override) error {
byName := make(map[string]ConfigKey, len(specs))
for _, s := range specs {
byName[s.Name] = s
}
// Pre-overlay pass: what each key resolves to from environment/flag/
// config-file/default alone, for source attribution below.
pre := make(map[string]string, len(specs))
for _, s := range specs {
if !s.Secret {
pre[s.Name] = effectiveString(s)
}
}
applied := make(map[string]bool, len(overrides))
for _, o := range overrides {
spec, ok := byName[o.Key]
if !ok {
continue
}
if spec.Secret {
return fmt.Errorf(
"integration config override %q: key is declared secret and cannot be overridden; remove the row: DELETE FROM core.integration_config_overrides WHERE key = '%s'",
o.Key, o.Key)
}
value, err := CoerceOverride(spec, o.Value)
if err != nil {
return fmt.Errorf(
"integration config override %q: %w; fix it in the operator panel or remove the row: DELETE FROM core.integration_config_overrides WHERE key = '%s'",
o.Key, err, o.Key)
}
viper.Set(o.Key, value)
applied[o.Key] = true
}
for _, s := range specs {
if s.Secret {
continue
}
eff := Effective{Value: effectiveString(s)}
switch {
case applied[s.Name]:
eff.Source = SourceOverride
case pre[s.Name] != defaultString(s):
eff.Source = SourceEnvironment
default:
eff.Source = SourceDefault
}
bootEffective[s.Name] = eff
}
return nil
}
// enumMember reports whether raw is a member of spec's declared Enum set. A
// key with no declared Enum (len(spec.Enum) == 0) imposes no constraint, so
// every value is a member — callers that also want to reject empty/unset
// values must check that separately. Shared by CoerceOverride (the operator-
// override save path) and ValidateStart's env-enum loop (validateEnums,
// validate.go), so the two membership checks cannot diverge (design D8).
func enumMember(spec ConfigKey, raw string) bool {
return len(spec.Enum) == 0 || slices.Contains(spec.Enum, raw)
}
// CoerceOverride validates a raw override value against its key's
// declaration and converts it to the key's declared default type. The raw
// form is the transport encoding — the same one the environment uses.
// Shared by the boot overlay and the operator surface's save path, so a
// value that saves is a value that boots.
func CoerceOverride(spec ConfigKey, raw string) (any, error) {
if !enumMember(spec, raw) {
return nil, fmt.Errorf("value %q is not one of %s", raw, strings.Join(spec.Enum, "|"))
}
switch spec.Default.(type) {
case []string:
return SplitList(raw), nil
case bool:
b, err := strconv.ParseBool(raw)
if err != nil {
return nil, fmt.Errorf("value %q is not a boolean", raw)
}
return b, nil
case int:
n, err := strconv.Atoi(raw)
if err != nil {
return nil, fmt.Errorf("value %q is not an integer", raw)
}
return n, nil
default:
return raw, nil
}
}
// SplitList parses the transport encoding for list-typed keys:
// comma-separated, whitespace-trimmed, empty elements dropped.
func SplitList(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
// effectiveString renders a key's current viper resolution in display/
// transport form (list values comma-joined).
func effectiveString(spec ConfigKey) string {
if _, ok := spec.Default.([]string); ok {
return strings.Join(viper.GetStringSlice(spec.Name), ",")
}
return viper.GetString(spec.Name)
}
// defaultString renders a key's declared default in the same form.
func defaultString(spec ConfigKey) string {
switch d := spec.Default.(type) {
case []string:
return strings.Join(d, ",")
case bool:
return strconv.FormatBool(d)
case int:
return strconv.Itoa(d)
case string:
return d
default:
return ""
}
}