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 serves event browsing, recovery-SQL generation, status, and point-in-time reconstruction. The events API includes connection_id but never query_text/query_hash (see dto.go). No endpoint ever executes SQL; recover generates a script for the operator to review and apply by hand.
Index ¶
- Constants
- Variables
- func DefaultAuthPath() string
- func DefaultRegistryPath() string
- func NormalizeFlavor(s string) (string, error)
- func PGReplDSN(queryDSN string) (string, error)
- 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 FlashbackTarget
- 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) ResolveFlashback(ctx context.Context, selector string) (FlashbackTarget, error)
- 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
- type VerifyCellDiff
- type VerifyController
- type VerifyExplanation
- type VerifyMode
- type VerifyRequest
- type VerifyRowDiff
- type VerifyStatus
- type VerifySummary
- type VerifyTableResult
Constants ¶
const ( FlavorMySQL = "mysql" FlavorMariaDB = "mariadb" FlavorPostgres = "postgres" )
Source-family flavors. The registry records which capture engine a source needs: MySQL and MariaDB both index through the go-mysql binlog path (MariaDB differs only by GTID dialect at stream time, --source-flavor mariadb), while PostgreSQL indexes through pgstreamrun. The string values are deliberately the SAME literals stream_state.flavor stores (see internal/query.SourceFlavor and recovery.DialectForFlavor) so a registry-declared flavor and the index-read flavor never disagree. Empty is treated as MySQL so every pre-#1019 registry entry keeps working — the same additive discipline as an empty SSLMode.
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.
ErrExplainUnavailable is returned by VerifyController.Explain when there is no cached baseline pair to explain from: no completed baseline-anchored run exists for this server, the table was not reported as a mismatch by it, or a newer run has since replaced it. The handler maps it to 404.
var ErrVerifyRunning = errors.New("a verify run is already running for this server")
ErrVerifyRunning is returned by VerifyController.Trigger when a verify run for that server is already in flight (one at a time per server, mirroring BaselineController). 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 NormalizeFlavor ¶ added in v0.33.0
NormalizeFlavor maps a request/registry flavor string to a canonical value. Empty → mysql; "postgresql" is accepted as an alias for "postgres". An unrecognized value is an error so a create/update rejects a typo rather than silently mis-routing capture.
func PGReplDSN ¶ added in v0.33.0
PGReplDSN derives the replication (walsender) connection string from a stored PostgreSQL query DSN by adding replication=database. pgcapture/pgstreamrun and pgbaseline require a distinct replication connection (a CopyBoth conn that cannot run ordinary SQL); the console stores only the query DSN and derives this one, so the replication-param logic lives in exactly one place. Consumed by the monitor supervisor (#1020) and the baseline trigger (#1021).
Pure net/url — no pgx: internal/console is on the read side of the dependency-guard (internal/event/depguard_test.go) and must never link the PostgreSQL capture libraries. A postgres:// URL round-trips through net/url and re-parses cleanly in pgconn.ParseConfig downstream.
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
// Flavor selects the baseline producer: "postgres" runs internal/pgbaseline
// (COPY + the slot's pgoutput anchor); anything else runs mydumper. Plain
// strings keep internal/console pgx-free (the read-layer dependency guard).
Flavor string
// Slot / Publication are the PostgreSQL replication slot and publication
// (postgres flavor only); the producer stamps the snapshot at the slot's
// consistent-point LSN so Time-travel/reconstruct can replay deltas from it.
// Empty for MySQL/MariaDB.
Slot string
Publication 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
// ProfileActive is set by the caller whenever a profile NAME was supplied,
// even if it resolved to ZERO deny/redact rules (an empty profile). It forces
// query.Options.ProfileActive on every query so QueryText/QueryHash are
// withheld under EVERY named profile (#699), and — since Parquet archives do
// not run the redaction pass — couples with NoArchive so archive rows cannot
// leak that statement text either (#838). Independent of the rule count,
// which stays the signal for rbacActive() (cascade/reconstruct gating).
ProfileActive bool
// 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
// VerifyCtrl runs bintrail verify's engine in-process for a monitored
// server (#677). Wired in ONLY by `bintrail-console watch` when the
// operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1); nil otherwise,
// where the trigger endpoint refuses with 403 and /api/capabilities reports
// verify_trigger:false. Set together with MonitorCtrl (both control-plane).
VerifyCtrl VerifyController
// 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 FlashbackTarget ¶ added in v0.31.0
type FlashbackTarget struct {
// IndexDB is the open connection to the server's per-source index. It is
// owned by the connManager — the serving layer must not Close it.
IndexDB *sql.DB
// IndexDBName is the schema where binlog_events lives (the query planner
// scopes information_schema.PARTITIONS to it).
IndexDBName string
// BaselineDir / BaselineS3 are the resolved _snapshot baseline source, split
// by scheme (dir-preferred, mirroring the console's Time-travel tab). The
// #766 local→S3 fallback the console bundle also carries is intentionally
// dropped — a documented single-source-parity edge for the embedded port
// (see docs/time-travel-sql.md).
BaselineDir string
BaselineS3 string
// NoArchive disables archive auto-discovery for this server.
NoArchive bool
// DefaultSchema is the source database name (from the registry SourceDSN),
// seeded so USE-less `_flashback.<table>` queries resolve; empty for the
// boot entry (no registry SourceDSN).
DefaultSchema string
}
FlashbackTarget is the go-mysql-free resolution of a flashback connection's target server (issue #996): the per-source index handle, baseline source, and default schema the MySQL-protocol serving layer needs to build a shim handler.
It lives here — but the serving code does NOT — because internal/console is a read-layer package barred from linking go-mysql (#528, TestReadLayerDoesNotLinkGoMySQL). cmd/bintrail-console owns the protocol server and consumes this plain struct; nothing here imports the protocol or capture libraries.
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) ResolveFlashback ¶ added in v0.31.0
ResolveFlashback maps a flashback connection username to its target server's per-source index + baseline, opening the connection lazily via connManager. The username selects the server by registry ID, display Name, or "default" (the boot entry). Returns ErrUnknownServer when the selector matches no selectable server — the serving layer turns that into a MySQL "no such database" error on the client's first query. The registry is read live, so servers added in the console mid-session are reachable without a restart.
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"`
// ── Source family (#1019) ──
// Flavor selects the capture engine: "" or "mysql" (MySQL), "mariadb"
// (MySQL DSN, MariaDB GTID at stream), "postgres" (pgstreamrun). Empty →
// mysql keeps every pre-#1019 entry working (mirrors an empty SSLMode).
// This is the generic "which source family" field #623 (MariaDB) also
// needs — one implementation, not two. Accessed via SourceFlavor().
Flavor string `yaml:"flavor,omitempty"`
// SourceSlot / SourcePublication configure PostgreSQL logical replication
// (postgres flavor only): the operator-created publication and the
// replication slot the capturer streams from. Empty for MySQL/MariaDB.
SourceSlot string `yaml:"source_slot,omitempty"`
SourcePublication string `yaml:"source_publication,omitempty"`
// ── Source TLS (#879) ──
// SSLMode/SSLCA/SSLCert/SSLKey configure the TLS the supervisor uses when
// connecting to the SOURCE MySQL. Empty SSLMode = the daemon default
// ("preferred"), preserving pre-#879 behavior; SSLCA/SSLCert/SSLKey are file
// paths on the daemon host (verify-ca / mutual TLS), not secrets like the
// DSNs. On binaries that predate these they round-trip untouched via Extra.
SSLMode string `yaml:"ssl_mode,omitempty"`
SSLCA string `yaml:"ssl_ca,omitempty"`
SSLCert string `yaml:"ssl_cert,omitempty"`
SSLKey string `yaml:"ssl_key,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.
func (ServerEntry) IsPostgres ¶ added in v0.33.0
func (e ServerEntry) IsPostgres() bool
IsPostgres reports whether the entry's source is PostgreSQL.
func (ServerEntry) SourceFlavor ¶ added in v0.33.0
func (e ServerEntry) SourceFlavor() string
SourceFlavor returns the entry's normalized source family, defaulting a blank (pre-#1019) entry to mysql. Consumed by the monitor supervisor (#1020), the baseline trigger (#1021), and the console UI gating. A stored value that no longer normalizes (only reachable via a hand-edit — create/update validate on write) degrades to mysql, which fails loudly at doctor time rather than silently corrupting anything.
type VerifyCellDiff ¶ added in v0.26.0
type VerifyCellDiff struct {
Column string `json:"column"`
Recovery string `json:"recovery"`
Baseline string `json:"baseline"`
}
VerifyCellDiff is the wire view of verify.CellDiff.
type VerifyController ¶ added in v0.26.0
type VerifyController interface {
// Trigger starts a verify run in the background and returns immediately.
// Returns ErrVerifyRunning if one is already running for req.ServerID.
Trigger(req VerifyRequest) error
// Status reports the latest known run for a server (idle if never run in
// this process): overall state plus per-table results accumulated so far.
Status(serverID string) VerifyStatus
// Explain drills into one table the last completed baseline-anchored run
// reported as a mismatch, re-running the row-level diff on demand — never
// precomputed for every mismatch (internal/verify.ExplainBaselinePairMismatch
// re-reconstructs the table). Returns ErrExplainUnavailable when no such
// run/table exists (including: the last run was live-source, which has no
// explain support in the engine).
Explain(serverID, schema, table string) (*VerifyExplanation, error)
}
VerifyController runs bintrail verify's engine in-process for a monitored server (#677) — the same internal/verify functions internal/cli/verify.go calls, looped over a server's tables in a background goroutine so the console can poll per-table results as they land. It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1); nil on the standalone read-only console, where the endpoints refuse with 403 — mirroring how BaselineController gates in-process baseline creation.
type VerifyExplanation ¶ added in v0.26.0
type VerifyExplanation struct {
Schema string `json:"schema"`
Table string `json:"table"`
Anchor string `json:"anchor"`
Total int `json:"total"`
Diffs []VerifyRowDiff `json:"diffs"`
Rendered string `json:"rendered"`
}
VerifyExplanation is the wire view of verify.MismatchExplanation: the structured per-row diffs (Diffs/Total, capped and overflow-summarized exactly like the CLI) plus Rendered — the same text internal/verify's MismatchExplanation.Write produces (including the deferred-type caveat and overflow breakdown, which live in unexported fields and so are only available through the rendered text, not the structured Diffs).
type VerifyMode ¶ added in v0.26.0
type VerifyMode string
VerifyMode selects which internal/verify engine path a run uses.
const ( // VerifyModeBaselineAnchored compares the two most recent baselines, // drift-free — no live source read. The default. VerifyModeBaselineAnchored VerifyMode = "baseline-anchored" // VerifyModeLiveSource reconstructs each table to a consistent snapshot of // the live source and compares. Needs the server's source DSN and reads // the whole table off production — the console warns to run it off-peak, // matching docs/verify.md. VerifyModeLiveSource VerifyMode = "live-source" )
type VerifyRequest ¶ added in v0.26.0
type VerifyRequest struct {
ServerID string
ServerName string
Mode VerifyMode
Tables []string // optional "schema.table" filter; empty = all
IndexDSN string
SourceDSN string // only required/used for VerifyModeLiveSource
BaselineDir string
BaselineS3 string
NoArchive bool
}
VerifyRequest is the in-process job description the endpoint hands the controller. The index/source DSNs (secrets) stay inside the process — never written to disk or serialized to any HTTP response.
type VerifyRowDiff ¶ added in v0.26.0
type VerifyRowDiff struct {
PK string `json:"pk"`
Kind string `json:"kind"` // changed | missing | extra
Cells []VerifyCellDiff `json:"cells,omitempty"`
}
VerifyRowDiff is the wire view of verify.RowDiff.
type VerifyStatus ¶ added in v0.26.0
type VerifyStatus struct {
State string `json:"state"` // idle | running | succeeded | failed
Mode VerifyMode `json:"mode,omitempty"`
Since string `json:"since,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
LastError string `json:"last_error,omitempty"`
// Note carries a benign, non-error informational message — e.g. "only one
// baseline exists yet, nothing to compare" (a legitimate first run, not a
// failure; mirrors the CLI's zero-exit early return in that case).
Note string `json:"note,omitempty"`
Results []VerifyTableResult `json:"results,omitempty"`
Summary VerifySummary `json:"summary"`
}
VerifyStatus is the pollable state of a server's most recent verify run. Unlike BaselineStatus (one terminal struct overwritten at job end), Results grows as each table completes so the console can show progress mid-run — internal/verify has no progress callback of its own; the controller loops over tables itself and appends after each one returns.
type VerifySummary ¶ added in v0.26.0
type VerifySummary struct {
Match int `json:"match"`
Mismatch int `json:"mismatch"`
Inconclusive int `json:"inconclusive"`
Error int `json:"error"`
}
VerifySummary tallies VerifyStatus.Results by status, for a run's headline.
type VerifyTableResult ¶ added in v0.26.0
type VerifyTableResult struct {
Schema string `json:"schema"`
Table string `json:"table"`
Status string `json:"status"` // match | mismatch | inconclusive | error
Detail string `json:"detail,omitempty"`
SourceRows int64 `json:"source_rows,omitempty"`
ReconstructRows int64 `json:"reconstruct_rows,omitempty"`
Anchor string `json:"anchor,omitempty"`
// Explainable is true only for a baseline-anchored mismatch whose pair is
// still cached from the run that produced this result — the precondition
// for calling Explain on it.
Explainable bool `json:"explainable"`
}
VerifyTableResult is the wire view of one table's verify.TableResult.