Documentation
¶
Overview ¶
Package fleet is the MT-STRUCT-S1 orchestrator: ONE server serving N DISTINCT apps (different schemas → different APIs) as N engine processes — the Option-A architecture of docs/design/MT-STRUCT.md. Each app is today's engine, unmodified on its hot path; the fleet adds only a supervisor (spawn/health/restart-on-exit) and a Host-routing reverse proxy in front.
Taxonomy (MT-STRUCT §1): an APP is one schema compiled into one API surface; a TENANT is an isolated data instance INSIDE an app; the FLEET is the set of apps on one server. A request resolves app (Host → proxy) then tenant (subdomain → engine middleware), two independent axes.
Index ¶
- func AddAppToManifestFile(path string, spec *AppSpec) error
- func BootstrapControlPlane(ctx context.Context, dsn string) error
- func CreateDatabase(ctx context.Context, adminDSN, dbName string) (created bool, err error)
- func DBNameOf(dsn string) string
- func DeriveDSN(baseDSN, dbName string) (string, error)
- func DropDatabase(ctx context.Context, adminDSN, dbName string) error
- func EditAppInManifestFile(path string, spec *AppSpec) error
- func RemoveAppFromManifestFile(path, name string) error
- func StatusHandler(s *Supervisor) http.Handler
- func SuggestDBName(app string) string
- func ValidDBName(name string) bool
- type AppSpec
- type AppStatus
- type DBInstance
- type DBTestResult
- type Manifest
- func (m *Manifest) AppByName(name string) *AppSpec
- func (m *Manifest) DBInstanceByName(name string) *DBInstance
- func (m *Manifest) OperatorAdmin() (string, string)
- func (m *Manifest) Path() string
- func (m *Manifest) SafeDBInstances() []SafeDBInstance
- func (m *Manifest) ValidateNewApp(a *AppSpec) error
- type Proxy
- type SafeDBInstance
- type Supervisor
- func (s *Supervisor) Port(name string) (int, bool)
- func (s *Supervisor) RestartApp(name string) error
- func (s *Supervisor) Shutdown()
- func (s *Supervisor) Start(ctx context.Context) error
- func (s *Supervisor) StartApp(name string) error
- func (s *Supervisor) Status() []AppStatus
- func (s *Supervisor) StopApp(name string) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AddAppToManifestFile ¶
AddAppToManifestFile appends spec to the manifest file's apps array (atomically). The caller has already validated spec (ValidateNewApp).
func BootstrapControlPlane ¶
BootstrapControlPlane applies the canonical control-plane DDL (embedded from migrations/001_control_plane.sql — public.tenants, tenant_policies, migration_log, the schema_updated trigger) to one app's database.
Rationale: in the single-app deployment docker-compose's initdb applies this file; a fleet gives EACH app its own fresh database, so provisioning it is the orchestrator's job (both the multi-process supervisor and the in-process ServeFleet runtime call this). Idempotent (IF NOT EXISTS / OR REPLACE) — a no-op on an already-initialized database. The engine itself stays untouched.
func CreateDatabase ¶
CreateDatabase creates dbName on the server the ADMIN DSN points at, IF it does not already exist. Returns created=true only when it actually ran the CREATE (created=false + nil ⇒ the database already existed, reused, never overwritten). The name is validated AND Sanitize()'d — CREATE DATABASE cannot run inside a transaction, and this issues that one statement only. The caller audits the outcome.
func DeriveDSN ¶
DeriveDSN returns baseDSN with its database name (the URL path) replaced by dbName — the app's runtime DSN, reusing the instance's host/credentials. The operator can still edit the result before submitting (e.g. a limited user).
func DropDatabase ¶
DropDatabase drops dbName — used ONLY to roll back a database this operation just created when the subsequent app-add fails (all-or-nothing). It is never exposed as an operator action: the fleet's vocabulary never destroys a pre-existing database (that is a deliberate, out-of-band act).
func EditAppInManifestFile ¶
EditAppInManifestFile replaces the manifest entry named spec.Name with spec (atomically) — used after EditApp (FLEET-EDIT-S1) resolves a new merged env: the app's env now lives in a (possibly freshly written) env_file, so the entry's env_file pointer is updated and any old inline env is cleared. This is EDIT, not create: it errors if the manifest does not already declare the app (mirrors RemoveAppFromManifestFile's not-found handling; see AddAppToManifestFile for the create path).
func RemoveAppFromManifestFile ¶
RemoveAppFromManifestFile removes the app named name from the manifest file (atomically). Returns an error if the manifest does not declare it.
func StatusHandler ¶
func StatusHandler(s *Supervisor) http.Handler
StatusHandler serves the fleet's internal status/control API:
GET /fleet/status → {apps: [AppStatus…]}
POST /fleet/apps/{name}/stop → graceful stop (no auto-restart)
POST /fleet/apps/{name}/start → start a stopped app
POST /fleet/apps/{name}/restart → deliberate stop+start of ONE app
It binds StatusAddr (default loopback). Like the engine control plane it is an INTERNAL surface — never expose it publicly.
func SuggestDBName ¶
SuggestDBName derives the conventional database name for an app: app_<name>, lowercasing and mapping '-' to '_' so a valid app name always yields a valid database name (mirrors deriveAppDSN / fleet-init).
func ValidDBName ¶
ValidDBName reports whether name is a safe database name.
Types ¶
type AppSpec ¶
type AppSpec struct {
// Name identifies the app (lowercase, ^[a-z][a-z0-9_-]*$, unique).
Name string `json:"name"`
// Schema is the path to the app's schema JSON (the engine's --schema).
// Relative paths resolve against the manifest file's directory.
Schema string `json:"schema"`
// Domains are the public hostnames of this app. A request whose Host is a
// domain OR any subdomain of it routes here — subdomains stay free for the
// engine's tenant resolution (acme.crm.example.com → app crm, tenant acme).
Domains []string `json:"domains"`
// Port / ControlPort pin the app's internal data/control ports; 0 (the
// default) auto-allocates a free port. Internal only — the proxy is the
// public face.
Port int `json:"port,omitempty"`
ControlPort int `json:"control_port,omitempty"`
// EnvFile is an optional KEY=VALUE file (e.g. the app's secrets) loaded
// first; Env entries override it. Relative to the manifest directory.
EnvFile string `json:"env_file,omitempty"`
// Env is the app's environment: DATABASE_URL, JWT_SECRET, ADMIN_KEY are
// REQUIRED (the engine refuses to boot without them); anything else the
// engine reads (APPXIMO_*, RATE_LIMIT_*, …) may be set per app.
Env map[string]string `json:"env,omitempty"`
// contains filtered or unexported fields
}
AppSpec declares one app of the fleet: its schema, the domains the proxy routes to it, and its OWN config/secrets (per-app by design — MT-STRUCT §7: a shared JWT_SECRET would let a token minted for app X validate on app Y).
func (*AppSpec) MergedEnv ¶
MergedEnv returns the app's resolved environment (EnvFile overlaid by Env).
func (*AppSpec) SetMergedEnv ¶
SetMergedEnv overwrites the resolved-env cache MergedEnv returns (FLEET-EDIT-S1): after EditApp writes a new env file for this app, the in-memory spec must reflect the change immediately — a concurrent JWT_SECRET-uniqueness check against a DIFFERENT app's edit, or the next read of THIS app, must see the new values without a manifest reload.
type AppStatus ¶
type AppStatus struct {
Name string `json:"name"`
Domains []string `json:"domains"`
Schema string `json:"schema"`
Port int `json:"port"`
ControlPort int `json:"control_port"`
PID int `json:"pid"`
Running bool `json:"running"`
Healthy bool `json:"healthy"`
Health string `json:"health"` // ready | draining_or_down | unreachable | stopped
Restarts int `json:"restarts"`
UptimeS int64 `json:"uptime_s"`
LastExit string `json:"last_exit,omitempty"`
Log string `json:"log"`
}
AppStatus is one app's row in the fleet status API.
type DBInstance ¶
type DBInstance struct {
// Name identifies the instance (^[a-z][a-z0-9_-]*$, unique). Referenced by
// the console when it asks the server to suggest/test/create — the DSN
// itself never travels to the browser.
Name string `json:"name"`
// Label is the human-friendly text shown in the console's instance picker.
Label string `json:"label,omitempty"`
// AdminDSNEnv is the NAME of the env var holding the privileged DSN. The
// secret is never a manifest key (committable-safe). Required and must be
// set — a declared-but-unwired instance fails the load loudly.
AdminDSNEnv string `json:"admin_dsn_env"`
// contains filtered or unexported fields
}
DBInstance is one operator-declared Postgres server the console may create databases on (FLEET-DB-ASSIST). AdminDSNEnv names an env var holding a PRIVILEGED DSN (a role that may CREATE DATABASE, pointing at a maintenance database such as `postgres`); the console derives the app's runtime DSN from it by swapping the database name. Declaring an instance is the explicit, auditable grant of create-power on that server — the credentials stay in the env-file, never the committable manifest.
func (*DBInstance) AdminDSN ¶
func (i *DBInstance) AdminDSN() string
AdminDSN returns the resolved privileged DSN (empty until LoadManifest ran).
type DBTestResult ¶
type DBTestResult struct {
OK bool `json:"ok"` // connected AND the target database exists
DBExists bool `json:"db_exists"` // the DSN's database exists (false ⇒ needs creating)
CanCreateDB bool `json:"can_create_db"` // the connected role may CREATE DATABASE
ServerVersion string `json:"server_version,omitempty"`
Code string `json:"code,omitempty"` // pg SQLSTATE (or "" for a network error)
Error string `json:"error,omitempty"` // actionable, client-safe message
}
DBTestResult is the structured verdict of a connection test — enough for the console to be actionable without leaking server internals.
func TestDSN ¶
func TestDSN(ctx context.Context, dsn string) DBTestResult
TestDSN connects with dsn, reports a structured verdict, and CLOSES — it is a pure probe with zero administrative effect. It classifies the common failures into actionable messages (database missing vs auth vs unreachable) rather than surfacing a raw driver error.
type Manifest ¶
type Manifest struct {
// Listen is the proxy's public address (default ":8080").
Listen string `json:"listen,omitempty"`
// StatusAddr serves the fleet status/control API (default "127.0.0.1:9601").
// Internal-only, like the engine control plane — keep it off the internet.
StatusAddr string `json:"status_addr,omitempty"`
// DataDir roots per-app state the fleet assigns when the app's env does not
// set it: <DataDir>/<app>/obs.db, <DataDir>/<app>/files, logs. Default
// "/var/lib/appximo/fleet".
DataDir string `json:"data_dir,omitempty"`
// OperatorKey is the FLEET-OPERATOR credential (MT-STRUCT-S5): it gates the
// unified fleet console (`/fleet` on the in-process runtime's process-level
// handler) — the server owner's view over ALL apps. It is a level ABOVE the
// per-app credentials and is deliberately DISTINCT from every app's
// ADMIN_KEY/JWT_SECRET: holding one app's keys never reveals the fleet, and
// the fleet key opens NO app API (per-app JWT/RBAC/admin auth still applies
// underneath — the S3 isolation is not bypassable from the console). Empty
// falls back to APPXIMO_FLEET_OPERATOR_KEY; still empty ⇒ the console is
// DISABLED (safe by default).
OperatorKey string `json:"operator_key,omitempty"`
// OperatorAdminEmail enables the UNIFIED OPERATOR IDENTITY
// (FLEET-CONSOLE-S2): the in-process runtime ensures a platform
// super-admin with this email exists in EVERY app's database (idempotent,
// never overwriting an existing account), so ONE login works on every
// app's /admin — without weakening the S3 isolation (each app keeps its
// own admin row, DB and tokens). The PASSWORD is deliberately NOT a
// manifest key: it comes from the APPXIMO_FLEET_ADMIN_PASSWORD env var
// (an env-file, like the app secrets), so the manifest stays committable.
// Empty email falls back to APPXIMO_FLEET_ADMIN_EMAIL; both empty ⇒
// feature off (each app manages its own admins, the pre-S2 behavior).
OperatorAdminEmail string `json:"operator_admin_email,omitempty"`
// DBInstances declares the Postgres servers the console's Add-app form may
// suggest DSNs from and create databases on (FLEET-DB-ASSIST). The engine
// NEVER discovers Postgres on its own — this list, and only this list, is
// what the console offers. Committable: it holds no credentials, only each
// instance's name/label and the NAME of the env var carrying its privileged
// DSN (the secret lives in the env-file). Absent/empty ⇒ the form is
// manual-DSN + test-connection only (no create power). See DBInstance.
DBInstances []DBInstance `json:"db_instances,omitempty"`
// Apps are the fleet's apps (≥1).
Apps []AppSpec `json:"apps"`
// contains filtered or unexported fields
}
Manifest is the fleet config file: the proxy listen address, the status endpoint, the per-app data root, and the apps.
func LoadManifest ¶
LoadManifest reads, resolves and validates a fleet manifest. Every error is actionable (names the app and the rule) — the same load-fails-loud contract as the engine's schema validation.
func (*Manifest) DBInstanceByName ¶
func (m *Manifest) DBInstanceByName(name string) *DBInstance
DBInstanceByName returns the declared instance named name, or nil.
func (*Manifest) OperatorAdmin ¶
OperatorAdmin returns the unified operator identity (email, password), or ("", "") when disabled. Password always from env — never the manifest.
func (*Manifest) Path ¶
Path returns the absolute path of the file this manifest was loaded from (empty for a manifest constructed in code).
func (*Manifest) SafeDBInstances ¶
func (m *Manifest) SafeDBInstances() []SafeDBInstance
SafeDBInstances returns the credential-free instance list for the console.
func (*Manifest) ValidateNewApp ¶
ValidateNewApp checks a candidate app against this manifest with the SAME rules LoadManifest applies, resolving the candidate's env (env_file + env) as a side effect. The candidate is NOT added; callers add it after the whole add operation (compile, registry, persist) succeeds.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the fleet's public face: it routes by Host to the owning app's engine and does NOTHING else — pure transport (MVP choice documented in docs/FLEET.md: a single-binary httputil.ReverseProxy; production TLS/HTTP2 termination can sit in front — Caddy/nginx — with the same domain table). Auth, RBAC, tenancy, rate limiting all happen in the destination engine, exactly as without a proxy.
Matching: a request Host matches an app if it equals one of its domains or is a SUBDOMAIN of one (longest domain wins). Subdomain labels stay free for the engine's tenant resolution: acme.crm.example.com → app "crm" (here) → tenant "acme" (engine middleware).
func NewProxy ¶
NewProxy builds the routing table from the manifest apps and their assigned ports. The table is immutable for the run (ports are fixed per fleet run).
func (*Proxy) ServeHTTP ¶
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP routes by Host. Lookup walks the host's label suffixes so the LONGEST matching domain wins (api.crm.example.com prefers crm.example.com over example.com), each step one map hit — O(labels), no allocation.
type SafeDBInstance ¶
type SafeDBInstance struct {
Name string `json:"name"`
Label string `json:"label"`
CanCreateDB bool `json:"can_create_db"`
}
SafeDBInstance is the console-facing view of an instance — never the DSN.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor runs one engine process per app and keeps it alive.
The one reconciliation rule that matters: the supervisor restarts an app ONLY when its process EXITS — never because health looks bad. The engine's own self-restart (UI-F4-S2) re-execs with syscall.Exec: same PID, the child never exits, so from here a self-restart is invisible except as a ~6 s window where /readyz answers 503 ("draining"). Restart-on-unhealthy would fight that drain and kill a healthy self-restart mid-flight; restart-on-exit composes with it for free.
func NewSupervisor ¶
func NewSupervisor(mf *Manifest, bin string) *Supervisor
NewSupervisor prepares (but does not start) a supervisor for the manifest. bin is the engine binary to spawn — normally the fleet's own executable.
func (*Supervisor) Port ¶
func (s *Supervisor) Port(name string) (int, bool)
Port returns an app's internal data port (for the proxy table).
func (*Supervisor) RestartApp ¶
func (s *Supervisor) RestartApp(name string) error
RestartApp is a deliberate stop+start of ONE app; the others are untouched.
func (*Supervisor) Shutdown ¶
func (s *Supervisor) Shutdown()
Shutdown stops every app in parallel (graceful, bounded by StopApp's 15 s).
func (*Supervisor) Start ¶
func (s *Supervisor) Start(ctx context.Context) error
Start allocates ports, spawns every app, and launches the health poller. It returns once all apps are spawned (readiness is reported via Status).
func (*Supervisor) StartApp ¶
func (s *Supervisor) StartApp(name string) error
StartApp starts a previously stopped app.
func (*Supervisor) Status ¶
func (s *Supervisor) Status() []AppStatus
Status snapshots every app, sorted by name.
func (*Supervisor) StopApp ¶
func (s *Supervisor) StopApp(name string) error
StopApp gracefully stops one app (SIGTERM → engine drain; SIGKILL after 15 s) and marks it so onExit does not restart it.