featureflags

package
v1.3.15 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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")
)
View Source
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.

func Register

func Register(def FlagDef) error

Register adds a flag definition to the process-wide registry. Call this from package init() so all known flags are present before the HTTP server boots. Returns an error rather than panicking so callers can decide.

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

func LookupDef(id string) (FlagDef, bool)

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

type HydrationRow struct {
	ID        string
	Enabled   bool
	UpdatedAt int64
}

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.

const (
	SourceDefault Source = "default"
	SourceDB      Source = "db"
	SourceRemote  Source = "remote"
	SourceFile    Source = "file"
)

type Store

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

Store holds the per-process effective state for every flag.

func New

func New(cfg Config) (*Store, error)

New creates a Store. Call Hydrate and ApplyFile during bootstrap to load DB and file overrides respectively.

func (*Store) ApplyFile

func (s *Store) ApplyFile(id string, enabled bool)

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

func (s *Store) ApplyRemote(id string, enabled bool, writtenAt int64)

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

func (s *Store) IsEnabled(id string) bool

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

func (s *Store) Set(_ context.Context, id string, enabled bool) (FlagStatus, error)

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.

func (*Store) Status

func (s *Store) Status(id string) (FlagStatus, error)

Status returns the status of a single flag. Unregistered flags with no override return ErrFlagNotFound.

type SyncDelegate

type SyncDelegate interface {
	OnSet(id string, enabled bool, writtenAt int64)
}

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.

Jump to

Keyboard shortcuts

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