Documentation
¶
Overview ¶
Package settings provides runtime settings: non-secret tunables declared in Go with a default and bounds, stored in PostgreSQL only when changed, and applied on every instance without a restart (ADR-0031).
Secrets and infrastructure (database URL, API keys, listen addresses) stay in the environment (ADR-0020); they can't be declared here.
reg := settings.NewRegistry()
codeTTL := settings.Duration(reg, "auth.verification_code_ttl", 15*time.Minute,
settings.Describe("How long email verification codes stay valid."),
settings.Range(5*time.Minute, time.Hour),
settings.ReasonRequired(),
)
store, err := settings.NewStore(ctx, pool, reg, recorder)
// Run store with the app's runners; pass codeTTL, a config.Value, to modules.
A Store loads every stored value at startup, applies changes made through Store.Set and Store.Reset immediately, and learns about changes made by other instances through PostgreSQL LISTEN/NOTIFY, with a periodic full reload as a fallback.
Settings declared with OrgOverridable also accept a value per organisation (ADR-0056). Setting.Get returns the organisation's value when the context's actor acts in one (actor.Actor.OrgID, set by orgs.RequireMember), and the platform value otherwise.
Stability: stable (ADR-0015, ADR-0054).
Index ¶
- Constants
- Variables
- type Change
- type HistoryEntry
- type InvalidValueError
- type Kind
- type Option
- func Describe(text string) Option
- func Email() Option
- func Group(name string) Option
- func MaxItems(n int) Option
- func MaxLen(n int) Option
- func OneOf(values ...string) Option
- func OrgOverridable() Option
- func Range[T int | float64 | time.Duration](lo, hi T) Option
- func ReasonRequired() Option
- func RestartRequired() Option
- func URL() Option
- func Validate[T any](fn func(T) error) Option
- type Registry
- type Setting
- func Bool(r *Registry, key string, def bool, opts ...Option) *Setting[bool]
- func Duration(r *Registry, key string, def time.Duration, opts ...Option) *Setting[time.Duration]
- func Enum(r *Registry, key string, def string, allowed []string, opts ...Option) *Setting[string]
- func Float(r *Registry, key string, def float64, opts ...Option) *Setting[float64]
- func Int(r *Registry, key string, def int, opts ...Option) *Setting[int]
- func String(r *Registry, key string, def string, opts ...Option) *Setting[string]
- func StringList(r *Registry, key string, def []string, opts ...Option) *Setting[[]string]
- type Store
- func (s *Store) DeleteHistoryBefore(ctx context.Context, before time.Time, limit int) (int64, error)
- func (s *Store) Get(key string) (View, error)
- func (s *Store) GetForOrg(orgID, key string) (View, error)
- func (s *Store) History(ctx context.Context, key string, before int64, limit int) ([]HistoryEntry, error)
- func (s *Store) HistoryForOrg(ctx context.Context, orgID, key string, before int64, limit int) ([]HistoryEntry, error)
- func (s *Store) List() []View
- func (s *Store) ListForOrg(orgID string) ([]View, error)
- func (s *Store) OldestHistory(ctx context.Context) (oldest time.Time, ok bool, err error)
- func (s *Store) Overrides(ctx context.Context, key, after string, limit int) ([]View, error)
- func (s *Store) Reload(ctx context.Context) error
- func (s *Store) Reset(ctx context.Context, key string, change Change) (View, error)
- func (s *Store) ResetForOrg(ctx context.Context, orgID, key string, change Change) (View, error)
- func (s *Store) Run(ctx context.Context) error
- func (s *Store) Set(ctx context.Context, key string, value json.RawMessage, change Change) (View, error)
- func (s *Store) SetForOrg(ctx context.Context, orgID, key string, value json.RawMessage, change Change) (View, error)
- func (s *Store) UnknownKeys() []string
- type StoreOption
- type View
Examples ¶
Constants ¶
const DefaultMaxItems = 100
DefaultMaxItems limits a StringList setting declared without MaxItems, so a list is never bounded only by the request body limit.
const DefaultResyncInterval = 5 * time.Minute
DefaultResyncInterval is how often a running Store reloads every value, covering notifications lost while disconnected.
Variables ¶
var ( // ErrUnknownSetting reports a key that isn't declared in the registry. ErrUnknownSetting = errors.New("settings: unknown setting") // ErrVersionConflict reports that the setting changed after the caller // read it. Read it again and retry. ErrVersionConflict = errors.New("settings: setting changed since it was read") // ErrReasonRequired reports a change without a reason to a setting // declared with [ReasonRequired]. ErrReasonRequired = errors.New("settings: a reason is required to change this setting") // ErrActorRequired reports a change without an authenticated actor in // the context. ErrActorRequired = errors.New("settings: changes require an authenticated actor") // ErrNotOrgOverridable reports an organisation value for a setting not // declared with [OrgOverridable]. ErrNotOrgOverridable = errors.New("settings: setting can't be changed per organisation") // ErrInvalidOrgID reports an empty or malformed organisation ID. ErrInvalidOrgID = errors.New("settings: invalid organisation ID") // ErrInvalidValue reports a value that fails decoding or validation. // The error is an [*InvalidValueError]. ErrInvalidValue = errors.New("settings: invalid value") )
Errors returned by Store methods. Check them with errors.Is.
var Migrations fs.FS = mustSub(migrationFiles, "migrations")
Migrations holds the module's goose migrations: the settings_values and settings_history tables. Apps copy them into db/migrations (the settings recipe does this); tests can apply them directly with pgtest.
Functions ¶
This section is empty.
Types ¶
type Change ¶
type Change struct {
// Version is the View.Version the caller last read.
Version int64
// Reason explains the change; required for ReasonRequired settings.
Reason string
}
Change carries what a caller supplies with a change. The actor comes from the context.
type HistoryEntry ¶
type HistoryEntry struct {
ID int64
Key string
// OrgID is the organisation whose value changed; empty for a
// platform-wide change.
OrgID string
OldValue json.RawMessage
NewValue json.RawMessage
Version int64
Reason string
ActorKind actor.Kind
ActorID string
RequestID string
ChangedAt time.Time
}
HistoryEntry is one change to a setting. A nil value means the default.
type InvalidValueError ¶
InvalidValueError describes why a value was rejected. Reason never contains the rejected value.
func (*InvalidValueError) Error ¶
func (e *InvalidValueError) Error() string
Error names the setting key and the reason the value was rejected.
func (*InvalidValueError) Unwrap ¶
func (e *InvalidValueError) Unwrap() error
Unwrap returns ErrInvalidValue.
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
An Option configures a setting declaration. An option that doesn't fit the setting's kind makes the declaration panic at startup.
func Email ¶
func Email() Option
Email requires a String setting, or each item of a StringList, to be a bare email address such as no-reply@example.com.
func Group ¶
Group sets the group used to organise settings in listings. Default: the key's first segment.
func MaxItems ¶
MaxItems limits a StringList setting to n items. Without it, a StringList setting is limited to DefaultMaxItems.
func OrgOverridable ¶
func OrgOverridable() Option
OrgOverridable lets each organisation have its own value, within the same validation, through Store.SetForOrg (ADR-0056). Setting.Get returns it when the context's actor acts in that organisation. Only mark settings an organisation may choose for itself: never security-relevant ones such as sign-in, rate limits, retention, maintenance or email senders. It can't be combined with RestartRequired.
Example ¶
package main
import (
"context"
"fmt"
"time"
"gorbital.dev/actor"
"gorbital.dev/modules/settings"
)
func main() {
reg := settings.NewRegistry()
invitationTTL := settings.Duration(reg, "orgs.invitation_ttl", 7*24*time.Hour,
settings.Range(24*time.Hour, 30*24*time.Hour),
settings.OrgOverridable(),
)
// orgs.RequireMember returns a context whose actor acts in the
// organisation. Get returns the organisation's own value, set with
// Store.SetForOrg, when it has one, and the platform value otherwise.
ctx := actor.With(context.Background(), actor.Actor{Kind: actor.KindUser, ID: "usr_1", OrgID: "org_1"})
fmt.Println(invitationTTL.Get(ctx))
}
Output: 168h0m0s
func Range ¶
Range limits an Int, Float or Duration setting to [lo, hi]. The bounds' type must match the setting: Range(0.0, 2.0) for a Float.
func ReasonRequired ¶
func ReasonRequired() Option
ReasonRequired makes every change to the setting require a reason, recorded in its history. Use it for security-relevant settings.
func RestartRequired ¶
func RestartRequired() Option
RestartRequired makes Setting.Get return the value loaded at startup. Changes are stored immediately and take effect when instances restart.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
A Registry holds declared settings and their current values. Declare every setting before calling NewStore. A Registry is safe for concurrent use.
type Setting ¶
type Setting[T any] struct { // contains filtered or unexported fields }
A Setting is a typed handle to one declared setting. It implements config.Value, so library modules can accept it for live options.
func Duration ¶
Duration declares a duration setting, stored and edited as a Go duration string such as "15m" or "1h30m". Combine with Range.
Example ¶
package main
import (
"context"
"fmt"
"time"
"gorbital.dev/modules/settings"
)
func main() {
reg := settings.NewRegistry()
codeTTL := settings.Duration(reg, "auth.verification_code_ttl", 15*time.Minute,
settings.Describe("How long email verification codes stay valid."),
settings.Range(5*time.Minute, time.Hour),
settings.ReasonRequired(),
)
// Until a store loads a changed value, Get returns the default.
fmt.Println(codeTTL.Get(context.Background()))
}
Output: 15m0s
func Float ¶
Float declares a decimal setting. Combine with Range, passing float bounds such as Range(0.0, 2.0).
func StringList ¶
StringList declares a list-of-text setting, such as allowed origins. Get returns a copy the caller may modify. Combine with MaxItems; the default limit is DefaultMaxItems.
func (*Setting[T]) Get ¶
Get returns the setting's current value. It returns the default while the setting is unchanged, when its stored value fails validation, and before a store has loaded. Settings declared with RestartRequired keep the value loaded at startup. For a setting declared with OrgOverridable, when the actor in ctx acts in an organisation that has a valid value of its own, Get returns that value. Get never touches the database.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists settings in PostgreSQL and keeps a Registry current. It is an app.Runner: run it so changes from other instances arrive. It is safe for concurrent use.
func NewStore ¶
func NewStore(ctx context.Context, pool *pgxpool.Pool, reg *Registry, recorder audit.Recorder, opts ...StoreOption) (*Store, error)
NewStore loads every stored value into reg and returns the store. After NewStore, reg accepts no new declarations. Changes are recorded as "settings.value.changed" audit events through recorder.
The settings tables come from Migrations; apply them first.
Example ¶
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5/pgxpool"
"gorbital.dev/audit"
"gorbital.dev/modules/settings"
)
func main() {
ctx := context.Background()
var (
pool *pgxpool.Pool // from postgres.Open
recorder audit.Recorder // for example an auditpg store
)
reg := settings.NewRegistry()
maintenance := settings.Bool(reg, "ops.maintenance_mode", false,
settings.Describe("Return 503 for all non-ops routes."),
settings.ReasonRequired(),
)
store, err := settings.NewStore(ctx, pool, reg, recorder)
if err != nil {
log.Fatal(err)
}
// Run store alongside the HTTP server so changes from other instances
// arrive: app.Run(ctx, []app.Runner{server, store}, ...).
_ = store
_ = maintenance
}
Output:
func (*Store) DeleteHistoryBefore ¶
func (s *Store) DeleteHistoryBefore(ctx context.Context, before time.Time, limit int) (int64, error)
DeleteHistoryBefore deletes up to limit setting changes made before before, oldest first, and returns how many it deleted. Retention calls it until it deletes fewer than limit (ADR-0051).
func (*Store) Get ¶
Get returns one setting, or ErrUnknownSetting.
func (*Store) GetForOrg ¶
GetForOrg returns one setting as organisation orgID sees it, or ErrInvalidOrgID, ErrUnknownSetting or ErrNotOrgOverridable.
func (*Store) History ¶
func (s *Store) History(ctx context.Context, key string, before int64, limit int) ([]HistoryEntry, error)
History returns changes to key, newest first. For the next page, pass the last entry's ID as before (0 starts from the newest). limit is clamped to 1–100.
func (*Store) HistoryForOrg ¶
func (s *Store) HistoryForOrg(ctx context.Context, orgID, key string, before int64, limit int) ([]HistoryEntry, error)
HistoryForOrg returns changes to organisation orgID's value of key, newest first, paged like Store.History.
func (*Store) List ¶
List returns every declared setting in declaration order. It reads memory only.
func (*Store) ListForOrg ¶
ListForOrg returns the settings declared OrgOverridable, in declaration order, as organisation orgID sees them: Value is the organisation's own value when it has a valid one, else PlatformValue. It reads memory only. It returns ErrInvalidOrgID for an empty or malformed ID; checking that the organisation exists, and that the caller may see it, is the caller's job.
func (*Store) OldestHistory ¶
OldestHistory returns when the oldest recorded change was made; ok is false when there are none.
func (*Store) Overrides ¶
Overrides returns the organisations that have their own value of key, by organisation ID, as views for each organisation. For the next page, pass the last view's OrgID as after ("" starts from the first). limit is clamped to 1–100. A setting not declared OrgOverridable has none; an undeclared key returns ErrUnknownSetting.
func (*Store) Reload ¶
Reload reads every stored value from the database, organisation values included.
func (*Store) Reset ¶
Reset returns key to its default. It returns the same errors as Store.Set, except for invalid values.
func (*Store) ResetForOrg ¶
ResetForOrg removes organisation orgID's own value of key, so the organisation gets the platform value again. It returns the same errors as Store.SetForOrg, except for invalid values.
func (*Store) Run ¶
Run keeps the registry current until ctx is done, then returns nil. It listens on a dedicated connection for changes committed by any instance, reloads everything after connecting (changes made while disconnected send no notification), and reloads everything every resync interval as a fallback. Connection failures are logged and retried with backoff.
func (*Store) Set ¶
func (s *Store) Set(ctx context.Context, key string, value json.RawMessage, change Change) (View, error)
Set validates value (JSON, such as `"30m"` or `42`) and stores it for key. The change applies to this instance immediately and to others within moments. It returns ErrUnknownSetting, an *InvalidValueError, ErrReasonRequired, ErrActorRequired or ErrVersionConflict. Setting the current value again changes nothing.
func (*Store) SetForOrg ¶
func (s *Store) SetForOrg(ctx context.Context, orgID, key string, value json.RawMessage, change Change) (View, error)
SetForOrg validates value like Store.Set and stores it as organisation orgID's own value of key, a setting declared OrgOverridable. Pass the organisation view's Version. The change is recorded in the organisation's history and as a "settings.value.changed" audit event carrying the organisation, and applies on every instance within moments.
It returns the errors of Store.Set, ErrInvalidOrgID and ErrNotOrgOverridable. The organisation must exist; checking that the caller may change its settings is the caller's job.
func (*Store) UnknownKeys ¶
UnknownKeys returns stored keys that no declaration matches, for example settings removed from the code. Their rows are kept and ignored.
type StoreOption ¶
type StoreOption interface {
// contains filtered or unexported methods
}
A StoreOption configures NewStore.
func WithLogger ¶
func WithLogger(logger *slog.Logger) StoreOption
WithLogger sets the logger for listener and invalid-value warnings. Default: discard.
func WithResyncInterval ¶
func WithResyncInterval(d time.Duration) StoreOption
WithResyncInterval sets how often Store.Run reloads every value. Default: DefaultResyncInterval.
type View ¶
type View struct {
Key string
Kind Kind
Group string
Description string
// Value is the effective value as JSON; Default is the declared default.
Value json.RawMessage
Default json.RawMessage
// Modified reports whether a valid stored value overrides the default,
// or, in a view for an organisation, the platform value.
Modified bool
// InvalidStoredValue reports a stored value that fails validation, so
// the default is in effect.
InvalidStoredValue bool
// Version increases with every change; pass it back to change the
// setting. It is 0 for a setting that has never been changed. In a view
// for an organisation, it is the version of the organisation's value.
Version int64
UpdatedAt time.Time
UpdatedBy string
ReasonRequired bool
RestartRequired bool
// RestartPending reports a restart-required setting changed since this
// instance started.
RestartPending bool
// Constraints summarises validation, such as min, max, one_of, max_len,
// max_items and format. Treat it as read-only.
Constraints map[string]any
// OrgOverridable reports a setting declared with [OrgOverridable].
OrgOverridable bool
// OrgID is the organisation of a view returned by [Store.ListForOrg],
// [Store.GetForOrg], [Store.SetForOrg], [Store.ResetForOrg] or
// [Store.Overrides]; empty for the platform-wide value.
OrgID string
// PlatformValue, in a view for an organisation, is the value the
// organisation gets without its own: the effective platform-wide value.
PlatformValue json.RawMessage
}
View is a setting's declaration and current state, for operator APIs.