feat(broker): migrations-based install, roles, RLS, and job dependency groups

Replace the ad-hoc tables/procedures install layout with versioned,
ordered SQL migrations tracked in broker_schema_migrations. Add
optional least-privilege role provisioning (--with-roles), multi-tenant
row-level security, lease-based job claiming with stale-lease recovery,
and job dependencies -- both by job id and by fan-in job group. Add
Docker/Compose support for running the broker and its test suite.
This commit is contained in:
2026-09-17 22:09:41 +02:00
parent 602997bcdb
commit 4c8e1066d4
53 changed files with 2911 additions and 1016 deletions
+113 -6
View File
@@ -6,9 +6,12 @@ import (
"log/slog"
"os"
"os/signal"
"runtime/debug"
"syscall"
"github.com/spf13/cobra"
"golang.org/x/term"
"git.warky.dev/wdevs/pgsql-broker/pkg/broker"
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/adapter"
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/config"
@@ -25,6 +28,13 @@ var (
cfgFile string
logLevel string
verifyOnly bool
withRoles bool
adminUser string
adminPassword string
brokerAdminPassword string
brokerRuntimePassword string
brokerEnqueuePassword string
)
func main() {
@@ -83,9 +93,59 @@ func init() {
// Install command flags
installCmd.Flags().BoolVar(&verifyOnly, "verify-only", false, "only verify installation without installing")
installCmd.Flags().BoolVar(&withRoles, "with-roles", false, "also create/update the broker_admin, broker_runtime, and broker_enqueue roles")
installCmd.Flags().StringVar(&adminUser, "admin-user", "", "superuser/CREATEROLE login used only for --with-roles (falls back to PGUSER/PG_USER env)")
installCmd.Flags().StringVar(&adminPassword, "admin-password", "", "password for --admin-user (falls back to PGPASSWORD/PG_PASS env, then an interactive prompt)")
installCmd.Flags().StringVar(&brokerAdminPassword, "broker-admin-password", "", "password to set for broker_admin (falls back to BROKER_ADMIN_PASSWORD env, then an interactive prompt)")
installCmd.Flags().StringVar(&brokerRuntimePassword, "broker-runtime-password", "", "password to set for broker_runtime (falls back to BROKER_RUNTIME_PASSWORD env, then an interactive prompt)")
installCmd.Flags().StringVar(&brokerEnqueuePassword, "broker-enqueue-password", "", "password to set for broker_enqueue (falls back to BROKER_ENQUEUE_PASSWORD env, then an interactive prompt)")
}
func runBroker() error {
// resolveCredential returns the first non-empty value among the flag value,
// the given environment variables (checked in order), and -- if none are
// set -- an interactive masked prompt. It errors rather than prompting when
// stdin is not a terminal, since a hang in a non-interactive context (CI,
// systemd) is worse than a clear failure.
func resolveCredential(flagVal string, envNames []string, promptLabel string) (string, error) {
if flagVal != "" {
return flagVal, nil
}
for _, name := range envNames {
if v := os.Getenv(name); v != "" {
return v, nil
}
}
fd := int(os.Stdin.Fd())
if !term.IsTerminal(fd) {
return "", fmt.Errorf(
"%s not provided and stdin is not a terminal to prompt on; pass it via flag or one of %v",
promptLabel, envNames,
)
}
fmt.Fprintf(os.Stderr, "%s: ", promptLabel)
b, err := term.ReadPassword(fd)
fmt.Fprintln(os.Stderr)
if err != nil {
return "", fmt.Errorf("failed to read %s: %w", promptLabel, err)
}
if len(b) == 0 {
return "", fmt.Errorf("%s must not be empty", promptLabel)
}
return string(b), nil
}
func runBroker() (err error) {
// Top-level safety net: an unrecovered panic anywhere in startup or the
// shutdown wait must not crash the process with a raw trace -- log it
// and return a normal error instead.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic in runBroker: %v\n%s", r, debug.Stack())
}
}()
// Load configuration
cfg, err := config.LoadConfig(cfgFile)
if err != nil {
@@ -136,12 +196,50 @@ func runInstall() error {
ctx := context.Background()
var rolePasswords install.RolePasswords
var adminUserVal, adminPasswordVal string
if withRoles {
if verifyOnly {
return fmt.Errorf("--with-roles cannot be combined with --verify-only")
}
var err error
adminUserVal, err = resolveCredential(adminUser, []string{"PGUSER", "PG_USER"}, "admin user (superuser/CREATEROLE login for --with-roles)")
if err != nil {
return err
}
adminPasswordVal, err = resolveCredential(adminPassword, []string{"PGPASSWORD", "PG_PASS"}, "admin password")
if err != nil {
return err
}
rolePasswords.AdminPassword, err = resolveCredential(brokerAdminPassword, []string{"BROKER_ADMIN_PASSWORD"}, "broker_admin password")
if err != nil {
return err
}
rolePasswords.RuntimePassword, err = resolveCredential(brokerRuntimePassword, []string{"BROKER_RUNTIME_PASSWORD"}, "broker_runtime password")
if err != nil {
return err
}
rolePasswords.EnqueuePassword, err = resolveCredential(brokerEnqueuePassword, []string{"BROKER_ENQUEUE_PASSWORD"}, "broker_enqueue password")
if err != nil {
return err
}
}
// Install/verify on all configured databases
for i, dbCfg := range cfg.Databases {
logger.Info("processing database", "index", i, "name", dbCfg.Name, "host", dbCfg.Host, "database", dbCfg.Database)
// Create database adapter
dbAdapter := adapter.NewPostgresAdapter(dbCfg.ToPostgresConfig(), logger)
// Create database adapter. With --with-roles, the config file's own
// user (typically the least-privilege broker_runtime) may not exist
// yet on a fresh cluster -- migrations and role creation both run as
// the admin login instead, since broker_admin must own the schema.
pgCfg := dbCfg.ToPostgresConfig()
if withRoles {
pgCfg.User = adminUserVal
pgCfg.Password = adminPasswordVal
}
dbAdapter := adapter.NewPostgresAdapter(pgCfg, logger)
// Connect to database
if err := dbAdapter.Connect(ctx); err != nil {
@@ -161,9 +259,9 @@ func runInstall() error {
}
logger.Info("database schema verified successfully", "database", dbCfg.Name)
} else {
// Install schema
logger.Info("installing database schema", "database", dbCfg.Name)
if err := installer.InstallSchema(ctx); err != nil {
// Apply migrations
logger.Info("applying database migrations", "database", dbCfg.Name)
if err := installer.ApplyMigrations(ctx); err != nil {
dbAdapter.Close()
logger.Error("installation failed", "database", dbCfg.Name, "error", err)
return fmt.Errorf("installation failed for %s: %w", dbCfg.Name, err)
@@ -180,6 +278,15 @@ func runInstall() error {
logger.Info("database schema installed and verified successfully", "database", dbCfg.Name)
}
if withRoles && !verifyOnly {
logger.Info("applying roles", "database", dbCfg.Name)
if err := installer.InstallRoles(ctx, rolePasswords); err != nil {
dbAdapter.Close()
return fmt.Errorf("failed to install roles for %s: %w", dbCfg.Name, err)
}
logger.Info("roles installed successfully", "database", dbCfg.Name)
}
dbAdapter.Close()
}