Phase 0: bootstrap the Warp homeserver
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

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>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-10-15 17:01:13 +02:00
commit b4fb60a8ab
50 files changed
+5672

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
// 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)
}
+200
View File
@@ -0,0 +1,200 @@
package settings_test
import (
"context"
"errors"
"sync"
"testing"
"github.com/dariuszjaniak/warp/server/internal/settings"
)
// memStore is an in-memory storage.SettingsStore.
type memStore struct {
mu sync.Mutex
values map[string]string
failSet error
}
func newMemStore(initial map[string]string) *memStore {
values := map[string]string{}
for k, v := range initial {
values[k] = v
}
return &memStore{values: values}
}
func (m *memStore) Get(_ context.Context, key string) (string, bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.values[key]
return v, ok, nil
}
func (m *memStore) GetAll(context.Context) (map[string]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := map[string]string{}
for k, v := range m.values {
out[k] = v
}
return out, nil
}
func (m *memStore) Set(_ context.Context, key, value string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failSet != nil {
return m.failSet
}
m.values[key] = value
return nil
}
func (m *memStore) Delete(_ context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.values, key)
return nil
}
func togglableDefaults() settings.Defaults {
return settings.Defaults{
FederationEnabled: false,
FederationAllowRuntimeToggle: true,
RegistrationEnabled: false,
RegistrationAllowRuntimeToggle: true,
}
}
func TestStoredValueOverridesConfig(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyFederationEnabled: "true"})
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if !svc.FederationEnabled() {
t.Error("the persisted value should win over the config default")
}
}
func TestPinnedSettingIgnoresStoredValue(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyFederationEnabled: "true"})
defaults := togglableDefaults()
defaults.FederationAllowRuntimeToggle = false
svc, err := settings.New(t.Context(), store, defaults, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if svc.FederationEnabled() {
t.Error("with allow_runtime_toggle false the config file is authoritative")
}
if err := svc.SetFederationEnabled(t.Context(), true); !errors.Is(err, settings.ErrNotToggleable) {
t.Errorf("SetFederationEnabled = %v, want ErrNotToggleable", err)
}
if _, found, _ := store.Get(t.Context(), settings.KeyFederationEnabled); !found {
t.Error("the stored value should be kept for when the toggle is re-enabled")
}
}
func TestUnparseableStoredValueFallsBackToConfig(t *testing.T) {
store := newMemStore(map[string]string{settings.KeyRegistrationEnabled: "maybe"})
defaults := togglableDefaults()
defaults.RegistrationEnabled = true
svc, err := settings.New(t.Context(), store, defaults, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if !svc.RegistrationEnabled() {
t.Error("a corrupt stored value must fall back to the config value, not to false")
}
}
func TestSetPersistsAndNotifies(t *testing.T) {
store := newMemStore(nil)
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
var got []settings.Snapshot
svc.Subscribe(func(s settings.Snapshot) { got = append(got, s) })
if err := svc.SetFederationEnabled(t.Context(), true); err != nil {
t.Fatalf("SetFederationEnabled: %v", err)
}
if !svc.FederationEnabled() {
t.Error("the cached value was not updated")
}
if v, _, _ := store.Get(t.Context(), settings.KeyFederationEnabled); v != "true" {
t.Errorf("stored value = %q, want \"true\"", v)
}
if len(got) != 1 || !got[0].FederationEnabled {
t.Fatalf("subscriber saw %v, want one snapshot with federation on", got)
}
// Setting the same value again changes nothing and must not notify.
if err := svc.SetFederationEnabled(t.Context(), true); err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Errorf("a no-op change notified subscribers: %v", got)
}
}
func TestSetKeepsCacheOnStoreFailure(t *testing.T) {
store := newMemStore(nil)
store.failSet = errors.New("disk on fire")
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
if err := svc.SetFederationEnabled(t.Context(), true); err == nil {
t.Fatal("expected the store failure to surface")
}
if svc.FederationEnabled() {
t.Error("the cached value must not change when persistence fails")
}
}
func TestSnapshotIsConsistent(t *testing.T) {
store := newMemStore(map[string]string{
settings.KeyFederationEnabled: "true",
settings.KeyRegistrationEnabled: "true",
})
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
snap := svc.Snapshot()
if !snap.FederationEnabled || !snap.RegistrationEnabled {
t.Errorf("Snapshot() = %+v, want both enabled", snap)
}
}
func TestConcurrentReadsAndWrites(t *testing.T) {
store := newMemStore(nil)
svc, err := settings.New(t.Context(), store, togglableDefaults(), nil)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
for i := range 8 {
wg.Add(1)
go func() {
defer wg.Done()
if i%2 == 0 {
_ = svc.SetFederationEnabled(t.Context(), i%4 == 0)
return
}
_ = svc.FederationEnabled()
_ = svc.Snapshot()
}()
}
wg.Wait()
}