Help us shape the future of Go! Take the 2026 Go Developer Survey

console

package
v0.35.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 41 Imported by: 0

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

View Source
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

View Source
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")
)
View Source
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).

View Source
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.

View Source
var ErrExplainUnavailable = errors.New("no explainable mismatch for this table — run a baseline-anchored verify first, or this table was not reported as a mismatch by the last run")

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.

View Source
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

func NormalizeFlavor(s string) (string, error)

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

func PGReplDSN(queryDSN string) (string, error)

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

func SetAuthPassword(path, username, password string) error

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

func ValidateNewPassword(password string) error

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

func LoadAuthFile(path string) (*AuthFile, error)

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

func (a *AuthFile) ReadOnly() bool

ReadOnly reports whether write paths must refuse (newer-version file).

func (*AuthFile) VerifyPassword added in v0.11.0

func (a *AuthFile) VerifyPassword(username, password string) bool

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

func LoadRegistry(path string) (*Registry, error)

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) Delete

func (r *Registry) Delete(id string) error

Delete removes the entry with the given id and persists.

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) Len

func (r *Registry) Len() int

Len reports the number of registry entries.

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

func (r *Registry) ReadOnly() bool

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

func New(cfg Config) (*Server, error)

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) Handler

func (s *Server) Handler() http.Handler

Handler returns the fully assembled HTTP handler. Exposed for tests.

func (*Server) Listen

func (s *Server) Listen() (net.Listener, error)

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

func (s *Server) NeedsSetup() bool

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

func (s *Server) PasswordLogin() bool

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

func (s *Server) ResolveFlashback(ctx context.Context, selector string) (FlashbackTarget, error)

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

func (s *Server) Run(ctx context.Context) error

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

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

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

func (s *Server) Token() string

Token returns the active access token (supplied or generated). Empty in password-only mode.

func (*Server) URL

func (s *Server) URL() string

URL returns the bootstrap URL the operator opens in a browser. Token mode includes the jupyter-style ?token=; password mode omits it (printing a live credential into logs and shell history serves no one when the login form is the entry point). The scheme follows TLS.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL