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

console

package
v0.25.0 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 39 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 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

Constants

This section is empty.

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.

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

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
}

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

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) 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"`
	// 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.

Jump to

Keyboard shortcuts

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