Documentation
¶
Overview ¶
Package featureflags provides a process-wide boolean toggle registry with layered configuration sources (code default, configstore DB, config.json file) and an optional SyncDelegate hook for cluster gossip.
Precedence (highest wins):
config.json file > configstore DB > code default
File-locked flags reject Set() with ErrLocked and silently ignore ApplyRemote, preserving the GitOps invariant that operators' config.json / Helm values cannot be overridden at runtime by either UI toggles or peer gossip. The DB row is still kept around inert so it re-emerges if the file override is later removed.
Index ¶
- Variables
- func MustRegister(def FlagDef)
- func Register(def FlagDef) error
- type Config
- type EntrySnapshot
- type FlagDef
- type FlagStatus
- type HydrationRow
- type Source
- type Store
- func (s *Store) ApplyFile(id string, enabled bool)
- func (s *Store) ApplyRemote(id string, enabled bool, writtenAt int64)
- func (s *Store) Hydrate(rows []HydrationRow)
- func (s *Store) IsEnabled(id string) bool
- func (s *Store) List() []FlagStatus
- func (s *Store) Restore(id string, snap EntrySnapshot, hadEntry bool)
- func (s *Store) Set(_ context.Context, id string, enabled bool) (FlagStatus, error)
- func (s *Store) SetDelegate(d SyncDelegate)
- func (s *Store) Snapshot(id string) (EntrySnapshot, bool)
- func (s *Store) Status(id string) (FlagStatus, error)
- type SyncDelegate
Constants ¶
This section is empty.
Variables ¶
var ( // ErrFlagNotFound means the id is neither registered in code nor // present in the store's override map. ErrFlagNotFound = errors.New("feature flag not found") // ErrFlagLocked means the flag's value is pinned by config.json or // Helm. The operator must edit the file (or redeploy) to change it. ErrFlagLocked = errors.New("feature flag is locked by config.json") // ErrFlagUnregistered means the flag exists in the override store but // no code registered it, so toggling has no effect. The caller should // DELETE the stale row instead. ErrFlagUnregistered = errors.New("feature flag has no code registration") // ErrFlagEnterpriseOnly means the flag is marked EnterpriseOnly in // its registration and the current process is running in OSS mode. // Such flags are inert: IsEnabled always returns false and toggling // is rejected. ErrFlagEnterpriseOnly = errors.New("feature flag is enterprise-only") )
var ( // ErrFlagIDInvalid is returned when a flag id does not match the // allowed character set: lowercase letters, digits, dots, dashes, with // no leading/trailing separators. ErrFlagIDInvalid = errors.New("feature flag id is invalid") // ErrFlagAlreadyRegistered is returned when the same flag id is // registered twice. We fail loud here so that two packages cannot // silently disagree on a default. ErrFlagAlreadyRegistered = errors.New("feature flag already registered") )
Functions ¶
func MustRegister ¶
func MustRegister(def FlagDef)
MustRegister is the panicking variant intended for package init() use where a registration error is a programming bug, not a runtime condition.
Types ¶
type Config ¶
type Config struct {
// IsEnterprise should be true when the binary is the enterprise build.
// Flags registered with EnterpriseOnly=true are inert when this is
// false: IsEnabled returns false, Set rejects with ErrFlagEnterpriseOnly,
// and the UI renders them disabled. Wired from initFeatureFlags by
// checking schemas.BifrostContextKeyIsEnterprise on the bootstrap ctx.
IsEnterprise bool
}
Config controls Store behavior.
type EntrySnapshot ¶
type EntrySnapshot struct {
// contains filtered or unexported fields
}
EntrySnapshot opaquely captures the full prior state of a feature flag override (enabled / source / writtenAt / fromFile) so a caller can roll back a subsequent failed mutation without corrupting metadata. The fields are unexported - the only valid use is passing the snapshot back to Restore.
type FlagDef ¶
type FlagDef struct {
ID string
DisplayName string
Description string
Default bool
EnterpriseOnly bool
}
FlagDef describes a feature flag at registration time.
- ID is the stable, machine-readable identifier used everywhere the flag is referenced from code (IsEnabled, the URL path, the DB key, gossip messages). Restricted to lowercase letters, digits, and [._-] separators so it is URL-safe and visually unambiguous.
- DisplayName is the human-readable label rendered in the UI. Free text; can be changed without breaking call sites.
- Description is the paragraph-level detail shown under the row.
- Default is the value used when no override (file or DB) is present.
- EnterpriseOnly marks flags that gate enterprise-only features: in OSS mode such flags are inert (IsEnabled always returns false), reject Set(), and surface in the UI with the toggle disabled and an "Enterprise" badge so operators can see the feature exists.
func LookupDef ¶
LookupDef returns the registered definition for a flag, or false if the flag is not registered. Unregistered flags can still exist in DB/file as stale data; the store handles them as registered=false in List().
func RegisteredDefs ¶
func RegisteredDefs() []FlagDef
RegisteredDefs returns a snapshot of all registered flag definitions, sorted by id for deterministic output (UI list, tests).
type FlagStatus ¶
type FlagStatus struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Default bool `json:"default"`
Enabled bool `json:"enabled"`
Source Source `json:"source"`
Locked bool `json:"locked"`
Registered bool `json:"registered"`
EnterpriseOnly bool `json:"enterprise_only"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
FlagStatus is the API/UI-facing snapshot of a single flag. ID is the stable identifier; DisplayName is what the UI renders. Keeping both on the wire lets the UI show the friendly label as the primary text and the id as muted secondary text for debugging.
type HydrationRow ¶
HydrationRow is the shape produced by configstore.ListFeatureFlags. We avoid importing configstore here to keep the package free of DB types.
type Source ¶
type Source string
Source records which configuration layer produced the effective value. Surfaced in the API/UI so operators can see why a flag is on or off without grepping logs.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store holds the per-process effective state for every flag.
func New ¶
New creates a Store. Call Hydrate and ApplyFile during bootstrap to load DB and file overrides respectively.
func (*Store) ApplyFile ¶
ApplyFile installs a value from config.json. Called LAST during bootstrap (after Hydrate) so file values win. fromFile=true means subsequent Set calls reject and gossip is ignored. The delegate is NOT invoked since file values are node-local by design.
func (*Store) ApplyRemote ¶
ApplyRemote is the inbound gossip path. Last-write-wins by writtenAt. File-locked flags are silently ignored: each node trusts its own config. The delegate is NOT invoked, preventing echo loops.
func (*Store) Hydrate ¶
func (s *Store) Hydrate(rows []HydrationRow)
Hydrate loads DB overrides during bootstrap. Call BEFORE ApplyFile so file values overwrite any DB conflicts in-memory while leaving the DB row intact (so removing the file override later re-exposes the DB value).
func (*Store) IsEnabled ¶
IsEnabled is the hot-path read. Unregistered/unknown flags return false so guarding code can treat "I don't recognize this id" as "off." Enterprise- only flags always return false in OSS mode regardless of any override, which lets guarding code use the flag uniformly across builds without extra plumbing.
func (*Store) List ¶
func (s *Store) List() []FlagStatus
List returns a status row for every flag known to the process: registered flags (always included, even when at default) plus any orphan rows in the override map (e.g. a config.json or DB entry whose code registration was removed). Sorted by id for deterministic output.
func (*Store) Restore ¶
func (s *Store) Restore(id string, snap EntrySnapshot, hadEntry bool)
Restore reverts the in-memory entry for id to a snapshot captured earlier. When hadEntry is false (no override existed when the snapshot was taken), the current entry is deleted instead so the flag returns to its code default. Restore does NOT fire the gossip delegate; rollback is a local concern and re-broadcasting would create cluster noise after a single-node failure.
func (*Store) Set ¶
Set is the LOCAL toggle path. Used by the HTTP handler when an operator flips a switch in the UI. Returns ErrFlagLocked if the flag is currently pinned by config.json, ErrFlagUnregistered if no code registered it.
func (*Store) SetDelegate ¶
func (s *Store) SetDelegate(d SyncDelegate)
SetDelegate installs (or replaces) the gossip hook. Safe to call before or after the store has any entries; only future Set() calls are observed.
func (*Store) Snapshot ¶
func (s *Store) Snapshot(id string) (EntrySnapshot, bool)
Snapshot captures the current override for id, if any. The second return is false when no override exists; pass it back to Restore to preserve "no override -> fall back to default/code" semantics, rather than mis-recording the flag as a forever-pinned remote entry.
type SyncDelegate ¶
SyncDelegate is invoked synchronously after a local Set() succeeds. Enterprise's gossip layer implements this and broadcasts each change to peers. ApplyRemote is the inbound path; it deliberately does NOT call the delegate to prevent echo loops.