Documentation
¶
Overview ¶
Package console serves an embedded, read-only, single-operator web UI over a bintrail index. It is the MCP server with a web face: the same query, recovery, status, and metadata engines, reached over HTTP from a browser.
The console exposes only the free query_explorer surface — event browsing and recovery-SQL generation. It deliberately stays out of the paid forensics surface: see dto.go, where connection_id (actor attribution) is dropped on the way out. No endpoint ever executes SQL; recover generates a script for the operator to review and apply by hand.
Index ¶
- Variables
- func DefaultAuthPath() string
- func DefaultRegistryPath() string
- func SetAuthPassword(path, username, password string) error
- func ValidateNewPassword(password string) error
- type AuthFile
- type BaselineController
- type BaselineRequest
- type BaselineStatus
- type Config
- type DoctorCheck
- type DoctorReport
- type MonitorController
- type MonitorStatus
- type Registry
- func (r *Registry) Add(e ServerEntry) (ServerEntry, error)
- func (r *Registry) Delete(id string) error
- func (r *Registry) Get(id string) (ServerEntry, bool)
- func (r *Registry) Len() int
- func (r *Registry) List() []ServerEntry
- func (r *Registry) ReadOnly() bool
- func (r *Registry) Rotation() (RotationConfig, bool)
- func (r *Registry) SetRotation(rc RotationConfig) error
- func (r *Registry) Update(e ServerEntry) error
- type RotationConfig
- type RotationDefaults
- type Server
- func (s *Server) Handler() http.Handler
- func (s *Server) Listen() (net.Listener, error)
- func (s *Server) NeedsSetup() bool
- func (s *Server) PasswordLogin() bool
- func (s *Server) Run(ctx context.Context) error
- func (s *Server) Serve(ctx context.Context, ln net.Listener) error
- func (s *Server) Token() string
- func (s *Server) URL() string
- type ServerEntry
Constants ¶
This section is empty.
Variables ¶
var ( // ErrDuplicateName rejects two registry entries sharing a name — the name // is the operator-facing label in the UI switcher, so it must be unique. ErrDuplicateName = errors.New("a server with this name already exists") // ErrUnknownServer is returned for an id with no registry entry. ErrUnknownServer = errors.New("unknown server id") // ErrRegistryReadOnly is returned for mutating operations when the on-disk // file was written by a newer bintrail (version > registryVersion). ErrRegistryReadOnly = errors.New("server registry was written by a newer bintrail; upgrade bintrail to edit it") )
var ErrAuthFileReadOnly = errors.New("auth file was written by a newer bintrail; logins work but changes are refused")
ErrAuthFileReadOnly is returned by write paths when the on-disk file was written by a newer bintrail (version > authFileVersion).
var ErrBaselineRunning = errors.New("a baseline is already running for this server")
ErrBaselineRunning is returned by BaselineController.Trigger when a baseline for that server is already in flight (one at a time per server). The handler maps it to 409 Conflict.
Functions ¶
func DefaultAuthPath ¶ added in v0.11.0
func DefaultAuthPath() string
DefaultAuthPath returns ~/.config/bintrail/console-auth.yaml, with the same relative fallback as DefaultRegistryPath for homeless environments.
func DefaultRegistryPath ¶
func DefaultRegistryPath() string
DefaultRegistryPath returns ~/.config/bintrail/console-servers.yaml, mirroring generate-key's defaultKeyPath fallback behavior.
func SetAuthPassword ¶ added in v0.11.0
SetAuthPassword hashes password (bcrypt, cost consoleBcryptCost) and writes the credential file atomically, preserving any Extra fields a newer binary stored. username == "" keeps the existing username (default "admin" on first set). Refuses a newer-version file with ErrAuthFileReadOnly.
func ValidateNewPassword ¶ added in v0.11.0
ValidateNewPassword enforces the set-time policy: at least minPasswordChars characters and at most 72 bytes (bcrypt's hard limit — reject, never truncate).
Types ¶
type AuthFile ¶ added in v0.11.0
type AuthFile struct {
Version int `yaml:"version"`
Username string `yaml:"username"`
PasswordBcrypt string `yaml:"password_bcrypt"`
UpdatedAt string `yaml:"updated_at"`
// Extra preserves unknown top-level fields a newer binary wrote, across
// this binary's load→save cycle (registry.go precedent; inline requires
// map[string]any).
Extra map[string]any `yaml:",inline"`
// contains filtered or unexported fields
}
AuthFile is the on-disk console credential: one username and one bcrypt hash. The console is single-user by design (multi-user/RBAC is dbtrail territory); the versioned envelope and Extra leave room for additive fields without a format break.
func LoadAuthFile ¶ added in v0.11.0
LoadAuthFile reads the credential file at path. A missing file returns (nil, nil): password login not configured. A present-but-unreadable or unparseable file is a loud error — silently degrading to token-only when the operator configured a password would be an auth downgrade.
func (*AuthFile) ReadOnly ¶ added in v0.11.0
ReadOnly reports whether write paths must refuse (newer-version file).
func (*AuthFile) VerifyPassword ¶ added in v0.11.0
VerifyPassword checks username+password against the stored credential in constant time: the username compare is subtle.ConstantTimeCompare and a username mismatch STILL runs a full bcrypt compare (against the dummy hash) so timing cannot enumerate the username. Safe on a nil receiver (false).
type BaselineController ¶ added in v0.21.0
type BaselineController interface {
// Trigger starts a baseline in the background and returns immediately.
// Returns ErrBaselineRunning if one is already running for req.ServerID.
Trigger(req BaselineRequest) error
// Status reports the latest known state for a server (idle if never run in
// this process — the durable record is the snapshot itself, listed by
// /api/baselines).
Status(serverID string) BaselineStatus
}
BaselineController runs a baseline snapshot for a monitored server entirely in-process — dump → convert → upload — so the console never shells a sibling container and never mounts the docker socket (#613). It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil on the standalone read-only console, where the endpoint refuses with 403 — mirroring how MonitorController gates the monitor verbs.
type BaselineRequest ¶ added in v0.21.0
type BaselineRequest struct {
ServerID string
ServerName string
SourceDSN string
Schemas []string
// LocalDir is the per-server baseline directory (entry.BaselineDir). When
// set, the snapshot is written there persistently and NOT uploaded. Empty
// means S3-only: stage in a temp dir, upload to S3, discard the staging.
LocalDir string
// S3 is the per-server baseline S3 prefix (entry.BaselineS3, s3://…). When
// set, the staged snapshot is uploaded there. Region/credentials come from
// the ambient AWS chain (env / ~/.aws / IAM role), like the rest of the
// console's S3 access.
S3 string
}
BaselineRequest is the in-process job description the endpoint hands the controller. The source DSN (a secret) stays inside the process — it is never written to disk or serialized to any HTTP response.
type BaselineStatus ¶ added in v0.21.0
type BaselineStatus struct {
State string `json:"state"` // idle | running | succeeded | failed
Since string `json:"since,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
LastError string `json:"last_error,omitempty"`
Tables int `json:"tables,omitempty"`
Rows int64 `json:"rows,omitempty"`
Uploaded int `json:"uploaded,omitempty"`
}
BaselineStatus is the pollable state of a server's most recent baseline job.
type Config ¶
type Config struct {
// DB is the boot index database, from the command-line DSN. May be nil
// when the console is registry-only (servers come from Registry) or in
// unit tests that exercise only middleware/asset routes.
DB *sql.DB
// DBName is the boot index database name (from the DSN), used by the
// query planner and status collection.
DBName string
// BootDSN is the boot entry's DSN, used ONLY to render the masked
// host/user/dbname view in /api/servers. Optional.
BootDSN string
// HideBoot removes the boot entry from the UI entirely (selector, server
// list, default selection). Set by source-less `bintrail-console watch`:
// its boot index is only the control plane's anchor database — no stream
// ever writes to it — and surfacing it as a "server" made fresh installs
// look pre-populated. Header-less requests still resolve to the boot
// bundle underneath, so the views render before the first server exists.
HideBoot bool
// Registry is the named-server store (a local YAML file — the only thing
// the console ever writes). nil means an empty in-memory registry.
Registry *Registry
// Listen is the bind address (host:port). Default 127.0.0.1:8090.
Listen string
// Token is an OPT-IN static automation credential — set it explicitly for
// scripts/curl. It is never generated. Password login is the primary path:
// with no token and no password, a loopback bind (or AllowSetup) enters
// first-run setup, and a non-loopback bind is refused.
Token string
// NoArchive disables Parquet archive auto-discovery on the boot entry.
// The caller forces this true when a profile is active (archives do not
// enforce RBAC). Registry entries carry their own per-server flag.
NoArchive bool
// DenyTables and RedactColumns are the resolved profile RBAC rules,
// applied to every query the console runs — on EVERY server.
DenyTables []query.SchemaTable
RedactColumns []query.SchemaTableColumn
// AllowedHosts is an optional allowlist of hostnames accepted in the Host
// header (in addition to IP literals and localhost), for operators who
// front the console with a DNS name.
AllowedHosts []string
// MonitorCtrl is the control-plane supervisor, wired in ONLY by
// `bintrail-console watch` (the write-capable daemon). nil on the
// standalone read-only console: /api/capabilities reports monitor:false
// there and every monitor verb refuses at the endpoint with 403,
// mirroring how reconstruct gates on baselineConfigured.
MonitorCtrl MonitorController
// BaselineCtrl runs in-process baseline snapshots (dump→convert→upload) for
// a monitored server (#613). Wired in ONLY by `bintrail-console watch` when
// the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil otherwise,
// where the trigger endpoint refuses with 403 and /api/capabilities reports
// baseline_trigger:false. Set together with MonitorCtrl (both control-plane).
BaselineCtrl BaselineController
// BaselineDir / BaselineS3 enable point-in-time reconstruct (Phase 2) on
// the boot entry. When either is set (and no RBAC profile is active), the
// "Reconstruct" surface is exposed. BaselineDir takes precedence;
// BaselineS3 is an s3:// prefix. Registry entries carry their own.
BaselineDir string
BaselineS3 string
// AuthPath locates the console credential file (username + bcrypt hash,
// written by `bintrail-console user set-password`). Empty means
// DefaultAuthPath(). A missing file means password login is not
// configured; a corrupt one fails New loudly.
AuthPath string
// TLSCert / TLSKey serve the console over HTTPS (both-or-neither). Static
// files only — rotation is a restart; ACME is out of scope.
TLSCert string
TLSKey string
// AllowSetup permits browser first-run password setup on a NON-loopback
// bind — an assertion that the bind is access-controlled by other means.
// The compose stack sets it because it binds 0.0.0.0 inside the container
// but publishes the port on the host's loopback only; the container can't
// see that host mapping, so the operator asserts it. Loopback binds always
// allow setup regardless. Off by default: an unguarded setup endpoint on a
// truly public bind would let the first stranger claim the password.
AllowSetup bool
// RotationDefaults carries the daemon's --rotate-* flag/env values so
// GET /api/rotation can report the effective policy (and the console panel
// prefill it) before any console override is saved. Set by the watch
// daemon; zero on the standalone serve (which runs no rotation loop and
// hides the panel).
RotationDefaults RotationDefaults
}
Config configures a console Server. The caller (cmd/bintrail/console.go) is responsible for connecting the boot DB (from --index-dsn), running EnsureSchema on it, and resolving the profile's RBAC rules before constructing the server. Registry servers are connected lazily by the console itself — and never schema-migrated (see connManager).
type DoctorCheck ¶
type DoctorCheck struct {
Name string `json:"name"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
Remediation string `json:"remediation,omitempty"`
}
DoctorCheck is one preflight check result, JSON-shaped for the UI's remediation cards. Status is pass|fail|warn|skip.
type DoctorReport ¶
type DoctorReport struct {
Checks []DoctorCheck `json:"checks"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Warnings int `json:"warnings"`
Skipped int `json:"skipped"`
}
DoctorReport aggregates the preflight checks for one source.
type MonitorController ¶
type MonitorController interface {
// DeriveIndexDSN returns the index DSN a monitored entry should use — a
// dedicated per-source database on the daemon's index MySQL server. It
// does not create anything; Start does.
DeriveIndexDSN(entryID string) (string, error)
// Doctor runs the preflight checks against the entry's source (and its
// index DSN, which may not exist yet — that is a pass, init creates it).
Doctor(ctx context.Context, e ServerEntry) (*DoctorReport, error)
// Start provisions the entry's index database (CREATE DATABASE + tables +
// schema migration — the supervisor is a WRITER, the same role the cmd
// layer plays for the boot DSN, so the console's never-migrates invariant
// holds) and launches the supervised stream. Idempotent for an already
// running entry. ctx bounds only the synchronous provisioning; the stream
// itself lives on the daemon's lifecycle.
Start(ctx context.Context, e ServerEntry) error
// Stop cancels the entry's stream and releases its advisory lock.
// Idempotent for an already stopped entry.
Stop(ctx context.Context, entryID string) error
// Status reports the entry's current monitor state.
Status(entryID string) MonitorStatus
}
MonitorController is the control-plane supervisor as the console sees it. All methods must be safe for concurrent use. Errors returned to handlers are written into HTTP responses — implementations must pre-scrub DSN secrets out of them.
type MonitorStatus ¶
type MonitorStatus struct {
// State: "stopped" | "pending" | "running" | "stalled" | "lost_position"
// | "failed".
// - "pending" covers launch through the stream's first checkpoint —
// connecting, snapshotting, discovering the start position. Only a
// saved checkpoint (or indexed batch) flips it to "running" (#407).
// - "stalled" and "lost_position" are running variants derived at read
// time: the stream goroutine is alive, but it has made no progress
// for several minutes (stalled) or an unfillable binlog gap forced
// it to skip permanently lost events (lost_position — durable,
// persisted in stream_state, re-hydrated on Start, cleared only by
// an explicit Stop).
// - "failed" carries LastError; the supervisor keeps retrying with
// backoff — "unhealthy, recovering", not terminal — unless the
// circuit breaker gave up after hours of continuous crash-looping
// (the message says so; Start re-arms it).
State string `json:"state"`
LastError string `json:"last_error,omitempty"`
// Since is when the underlying STORED state was entered (RFC3339), empty
// for stopped. For the derived stalled/lost_position presentations it
// still reflects the running transition, not when the stream stalled or
// lost its position.
Since string `json:"since,omitempty"`
}
MonitorStatus is the supervisor's view of one entry's stream.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the console's named-server store: a local YAML file, the ONLY thing the console ever writes. All mutations rewrite the file atomically (temp file + fsync + rename) under a mutex — this is bintrail's first programmatically-mutated config file, so a plain os.WriteFile (which can interleave concurrent writers) is not enough.
func LoadRegistry ¶
LoadRegistry reads the registry file at path. A missing or empty file is an empty registry, not an error. The parse is deliberately NON-strict (unlike shim.yaml's UnmarshalStrict): an older binary must tolerate top-level fields a newer one added. path == "" creates an in-memory registry that never touches disk — for unit tests and callers without persistence.
func (*Registry) Add ¶
func (r *Registry) Add(e ServerEntry) (ServerEntry, error)
Add validates, mints an id, appends, and persists the entry. The returned entry carries the generated ID.
func (*Registry) Get ¶
func (r *Registry) Get(id string) (ServerEntry, bool)
Get returns the entry with the given id. Like List, the entry's Extra map is shared with the registry — treat it as read-only.
func (*Registry) List ¶
func (r *Registry) List() []ServerEntry
List returns a copy of the entries, in file order. The copy is shallow: each entry's Extra map is shared with the registry — treat returned entries as read-only (nothing in the console mutates Extra off a copy today).
func (*Registry) ReadOnly ¶
ReadOnly reports whether mutating operations are refused (newer-version file).
func (*Registry) Rotation ¶ added in v0.13.0
func (r *Registry) Rotation() (RotationConfig, bool)
Rotation returns the saved global rotation policy, or false when none is set (the daemon's --rotate-* flags/env are in force). The rotation loop's settings provider reads this every cycle, so an override applies live.
func (*Registry) SetRotation ¶ added in v0.13.0
func (r *Registry) SetRotation(rc RotationConfig) error
SetRotation persists the global rotation policy. Like every registry mutation it rewrites the file atomically and is refused on a newer-version (read-only) file. The caller validates the field grammar.
func (*Registry) Update ¶
func (r *Registry) Update(e ServerEntry) error
Update replaces the entry with e.ID and persists. The caller is responsible for keep-password merging — the registry stores exactly what it is given. Unknown phase-2 fields survive: the stored entry's Extra is carried over unless the caller supplied its own.
type RotationConfig ¶ added in v0.13.0
type RotationConfig struct {
Retain string `yaml:"retain"`
Interval string `yaml:"interval"`
AddFuture int `yaml:"add_future"`
}
RotationConfig is the daemon-global built-in-rotation policy, editable from the console UI. It is stored once in the registry envelope, NOT per-server: the rotation loop is a single shared ticker, so retain/interval/add-future necessarily apply to every index the daemon rotates. Absent (nil) = the daemon's --rotate-* flags / BINTRAIL_ROTATE_* env stay in force. The fields hold the operator-typed strings (e.g. "30d", "1h") so they round-trip exactly and the engine parses them with the same grammar as the flags.
type RotationDefaults ¶ added in v0.13.0
type RotationDefaults struct {
Retain string
Interval string
AddFuture int
// Enabled is false when the daemon was started with rotation off
// (--rotate-retain off). The loop is not running then, so a console-saved
// override would need a restart — the panel warns in that state.
Enabled bool
}
RotationDefaults is the daemon-side built-in-rotation policy, surfaced to the console read-only as the fallback shown when no override is saved.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is a configured, ready-to-run console HTTP server. It holds only process-global state; everything per-server (db, engine, resolver, baseline gates) lives in a connManager bundle resolved per request from the X-Bintrail-Server header.
func New ¶
New validates the config, seeds the boot connection bundle, and assembles the middleware/route tree. It does no network I/O — call Run to listen.
func (*Server) Listen ¶
Listen binds the configured address and returns the listener synchronously, so a caller can fail fast on a port conflict before reporting the server as ready (the standalone command blocks on Run, but `bintrail-console watch` starts the server in a goroutine and must surface a bind error up front).
func (*Server) NeedsSetup ¶ added in v0.12.0
NeedsSetup reports whether the console is in first-run setup: no credential at all, on a loopback bind (or a non-loopback bind the operator marked access-controlled via --allow-setup), so the operator must create a password in the browser (via the unauthenticated /api/auth/setup endpoint). The cmd layer uses it to print "create your password" instead of a URL with a token.
func (*Server) PasswordLogin ¶ added in v0.11.0
PasswordLogin reports whether a console password was configured at boot — the cmd layer keys its banner wording on it.
func (*Server) Run ¶
Run binds and serves, blocking until ctx is cancelled, then shuts down gracefully. Returns the bind/serve error, or nil on a clean shutdown.
func (*Server) Serve ¶
Serve serves on ln and blocks until ctx is cancelled, then shuts the server down gracefully (5s drain). It takes ownership of ln (Shutdown closes it). Lazily-opened registry connections are closed on the way out; the boot entry's db belongs to the cmd layer and its deferred Close.
func (*Server) Token ¶
Token returns the active access token (supplied or generated). Empty in password-only mode.
type ServerEntry ¶
type ServerEntry struct {
// ID is a server-generated stable 8-byte hex key; it survives renames and
// is what the browser sends in the X-Bintrail-Server header.
ID string `yaml:"id"`
// Name is the mutable operator-facing label, unique across the registry.
Name string `yaml:"name"`
// DSN is the full index DSN, password included — a secret at rest (the
// file is 0600, same class as shim.yaml's mysql_password).
DSN string `yaml:"index_dsn"`
BaselineDir string `yaml:"baseline_dir,omitempty"`
BaselineS3 string `yaml:"baseline_s3,omitempty"`
NoArchive bool `yaml:"no_archive,omitempty"`
// SourceDSN is the source MySQL to monitor — replication credentials,
// a secret exactly like DSN, never serialized to any HTTP response.
// Empty = a view-only entry (no monitoring configured).
SourceDSN string `yaml:"source_dsn,omitempty"`
// SourceServerID overrides the auto-derived replica server id (0 = derive
// from the source DSN, the same rule as `bintrail up`).
SourceServerID uint32 `yaml:"source_server_id,omitempty"`
// Schemas is the optional comma-separated schema filter for monitoring.
Schemas string `yaml:"schemas,omitempty"`
// ArchiveS3 is the S3 destination (s3://bucket/prefix/) the daemon's
// built-in rotation uploads this source's rotated Parquet partitions to
// BEFORE dropping them — so the forensic record survives retention and
// stays queryable (the console auto-discovers it). Empty = drop-only.
// Region/credentials come from the ambient AWS chain (env / ~/.aws / IAM
// role); a local staging dir is used transiently. Non-secret (a bucket
// URL): unlike DSNs it is serialized to the masked HTTP responses.
ArchiveS3 string `yaml:"archive_s3,omitempty"`
// MonitorDesired records the operator's intent to monitor this source.
// The supervisor reconciles running streams against it at boot and on
// every edit; nothing reads it until phase 3.
MonitorDesired bool `yaml:"monitor_desired,omitempty"`
// Extra is the forward-compat catch-all: unknown fields written by a NEWER
// bintrail (e.g. the phase-2 control plane's source_dsn / server_id /
// monitor_state) land here on load and re-emit verbatim on save, so an
// older binary editing the file never drops them. yaml:",inline" requires
// map[string]any specifically; non-strict Unmarshal alone would only
// tolerate unknown fields on read — a re-marshal would lose them.
Extra map[string]any `yaml:",inline"`
}
ServerEntry is one named index connection in the registry. DSN holds the full secret (including the password) and is NEVER serialized into an HTTP response — see serverDTO in servers_api.go for the masked wire view.