Files
Warp/server/internal/settings/settings.go
T
tajniak81andClaude Opus 5 b4fb60a8ab
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 0: bootstrap the Warp homeserver
Scaffolds the server module and everything Phase 0 of PLAN.md calls for:
a binary that boots, serves its discovery endpoints and nothing else yet.

- config: one YAML file plus per-field WARP_* environment overrides,
  unknown fields rejected, validation that reports every problem at once
- logging and metrics: slog, a Prometheus registry, per-route HTTP
  instrumentation labelled by pattern rather than path
- httputil: the Matrix error envelope, error-returning handlers, CORS,
  request logging, panic recovery, and a router over http.ServeMux
- endpoints: /_matrix/client/versions, /.well-known/matrix/client,
  /.well-known/matrix/server (gated), /healthz, /metrics, and
  M_UNRECOGNIZED for everything else
- storage: driver registry, shared database/sql base, checksummed
  migration runner, sqlite and postgres drivers, pocketbase stub
- settings: server_settings table behind a cached service, so the
  federation and registration switches change without a restart
- federation: the gate that makes federation a runtime mode
- cmd/warp: serve, migrate, generate-keys, version
- Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests,
  race on Linux, storage tests against PostgreSQL, lint, boot smoke test),
  Dockerfile, example config and docs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 17:01:13 +02:00

194 lines
6.1 KiB
Go

// Package settings is the runtime-settings service: the small set of
// switches an administrator can flip without restarting the server.
//
// Each setting has a value in the config file and, optionally, a value in
// the server_settings table. The stored value wins when the config allows
// the setting to be toggled at runtime; otherwise the file is authoritative
// and the stored value is ignored (but kept, so that re-enabling the toggle
// restores the administrator's last choice).
//
// Readers get a cached value — these are consulted on every federated
// request — and components that must react to a change subscribe.
package settings
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"sync"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// Setting keys as stored in server_settings.
const (
KeyFederationEnabled = "federation.enabled"
KeyRegistrationEnabled = "registration.enabled"
)
// ErrNotToggleable is returned when a setting is pinned by the config file.
var ErrNotToggleable = errors.New("setting is not toggleable at runtime")
// Defaults come from the config file.
type Defaults struct {
FederationEnabled bool
FederationAllowRuntimeToggle bool
RegistrationEnabled bool
RegistrationAllowRuntimeToggle bool
}
// Snapshot is a consistent view of every runtime setting.
type Snapshot struct {
FederationEnabled bool
RegistrationEnabled bool
}
// Service caches the runtime settings and persists changes.
type Service struct {
store storage.SettingsStore
log *slog.Logger
defaults Defaults
mu sync.RWMutex
current Snapshot
listeners []func(Snapshot)
// writeMu serialises Set calls; see set.
writeMu sync.Mutex
}
// New loads the persisted settings and returns a service. A stored value
// that cannot be parsed is logged and ignored rather than failing the boot:
// an unreadable switch should not keep the server down.
func New(ctx context.Context, store storage.SettingsStore, defaults Defaults, log *slog.Logger) (*Service, error) {
if log == nil {
log = slog.Default()
}
s := &Service{
store: store,
log: log,
defaults: defaults,
current: Snapshot{
FederationEnabled: defaults.FederationEnabled,
RegistrationEnabled: defaults.RegistrationEnabled,
},
}
stored, err := store.GetAll(ctx)
if err != nil {
return nil, fmt.Errorf("load server settings: %w", err)
}
if defaults.FederationAllowRuntimeToggle {
s.current.FederationEnabled = s.boolOr(stored, KeyFederationEnabled, defaults.FederationEnabled)
}
if defaults.RegistrationAllowRuntimeToggle {
s.current.RegistrationEnabled = s.boolOr(stored, KeyRegistrationEnabled, defaults.RegistrationEnabled)
}
s.log.Info("runtime settings loaded",
"federation_enabled", s.current.FederationEnabled,
"registration_enabled", s.current.RegistrationEnabled)
return s, nil
}
func (s *Service) boolOr(stored map[string]string, key string, fallback bool) bool {
raw, ok := stored[key]
if !ok {
return fallback
}
v, err := strconv.ParseBool(raw)
if err != nil {
s.log.Warn("ignoring unparseable stored setting", "key", key, "value", raw, "error", err)
return fallback
}
return v
}
// Snapshot returns the current values.
func (s *Service) Snapshot() Snapshot {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current
}
// FederationEnabled reports whether the federation surface is live. It is
// called on every federation request, so it only takes a read lock.
func (s *Service) FederationEnabled() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.FederationEnabled
}
// RegistrationEnabled reports whether open registration is accepted.
func (s *Service) RegistrationEnabled() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.current.RegistrationEnabled
}
// FederationToggleable reports whether SetFederationEnabled is allowed.
func (s *Service) FederationToggleable() bool { return s.defaults.FederationAllowRuntimeToggle }
// RegistrationToggleable reports whether SetRegistrationEnabled is allowed.
func (s *Service) RegistrationToggleable() bool { return s.defaults.RegistrationAllowRuntimeToggle }
// SetFederationEnabled persists and applies the federation switch.
func (s *Service) SetFederationEnabled(ctx context.Context, enabled bool) error {
if !s.defaults.FederationAllowRuntimeToggle {
return fmt.Errorf("federation.enabled: %w (federation.allow_runtime_toggle is false)", ErrNotToggleable)
}
return s.set(ctx, KeyFederationEnabled, enabled, func(snap *Snapshot) { snap.FederationEnabled = enabled })
}
// SetRegistrationEnabled persists and applies the registration switch.
func (s *Service) SetRegistrationEnabled(ctx context.Context, enabled bool) error {
if !s.defaults.RegistrationAllowRuntimeToggle {
return fmt.Errorf("registration.enabled: %w (registration.allow_runtime_toggle is false)", ErrNotToggleable)
}
return s.set(ctx, KeyRegistrationEnabled, enabled, func(snap *Snapshot) { snap.RegistrationEnabled = enabled })
}
// set writes the store first: if persistence fails the in-memory value must
// not change, or a restart would silently undo what the admin API reported
// as done.
func (s *Service) set(ctx context.Context, key string, value bool, apply func(*Snapshot)) error {
// writeMu serialises the whole write path, so two concurrent toggles
// cannot persist in one order and notify subscribers in another.
s.writeMu.Lock()
defer s.writeMu.Unlock()
if err := s.store.Set(ctx, key, strconv.FormatBool(value)); err != nil {
return err
}
s.mu.Lock()
before := s.current
apply(&s.current)
after := s.current
listeners := make([]func(Snapshot), len(s.listeners))
copy(listeners, s.listeners)
s.mu.Unlock()
if before == after {
return nil
}
s.log.Info("runtime setting changed", "key", key, "value", value)
// Listeners are called outside s.mu: a federation module starting or
// stopping can take a while and must not block readers.
for _, fn := range listeners {
fn(after)
}
return nil
}
// Subscribe registers fn to be called after any setting changes. It is
// called with the new snapshot, never concurrently with itself.
func (s *Service) Subscribe(fn func(Snapshot)) {
s.mu.Lock()
defer s.mu.Unlock()
s.listeners = append(s.listeners, fn)
}