Documentation
¶
Overview ¶
Package migration provides integration with Flyway for database migrations
Index ¶
- Constants
- Variables
- func CheckPGRoleFloor(ctx context.Context, db *sql.DB, role string) error
- func PGRoleProvisioningSQL(spec *PGRoleSpec) ([]string, error)
- func ProvisionPGRoles(ctx context.Context, db *sql.DB, spec *PGRoleSpec) error
- func ProvisionPGRolesTx(ctx context.Context, exec database.Executor, spec *PGRoleSpec) error
- type Action
- type AuditContext
- type AuditEvent
- type AuditEventType
- type AuditOutcome
- type AuditRecorder
- type Config
- type Emitter
- type ErrorClass
- type FlywayMigrator
- func (fm *FlywayMigrator) Close(ctx context.Context) error
- func (fm *FlywayMigrator) DefaultMigrationConfig() *Config
- func (fm *FlywayMigrator) DefaultMigrationConfigForVendor(vendor string) *Config
- func (fm *FlywayMigrator) Info(ctx context.Context, cfg *Config) error
- func (fm *FlywayMigrator) InfoFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) error
- func (fm *FlywayMigrator) Migrate(ctx context.Context, cfg *Config) (Result, error)
- func (fm *FlywayMigrator) MigrateFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) (Result, error)
- func (fm *FlywayMigrator) RunMigrationsAtStartup(ctx context.Context) error
- func (fm *FlywayMigrator) Validate(ctx context.Context, cfg *Config) error
- func (fm *FlywayMigrator) ValidateFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) error
- func (fm *FlywayMigrator) WithAuditRecorder(sink AuditRecorder) *FlywayMigrator
- func (fm *FlywayMigrator) WithSharedMigrator() *FlywayMigrator
- type MemoryQuiesceController
- func (c *MemoryQuiesceController) Clear(ctx context.Context, by string) (*QuiesceStatus, error)
- func (c *MemoryQuiesceController) CreateTable(_ context.Context) error
- func (c *MemoryQuiesceController) IsSet(_ context.Context) (bool, error)
- func (c *MemoryQuiesceController) Query(_ context.Context) (*QuiesceStatus, error)
- func (c *MemoryQuiesceController) Set(ctx context.Context, opts QuiesceSetOptions) (*QuiesceStatus, error)
- func (c *MemoryQuiesceController) WithAudit(em Emitter) *MemoryQuiesceController
- func (c *MemoryQuiesceController) WithClock(now func() time.Time) *MemoryQuiesceController
- type MigrateAllOptions
- type MigrateAllResult
- type MigratorIdentity
- type PGIdentifierChecker
- type PGIdentifierCheckerFunc
- type PGRoleSpec
- type PostgresQuiesceController
- func (c *PostgresQuiesceController) Clear(ctx context.Context, by string) (*QuiesceStatus, error)
- func (c *PostgresQuiesceController) CreateTable(ctx context.Context) error
- func (c *PostgresQuiesceController) IsSet(ctx context.Context) (bool, error)
- func (c *PostgresQuiesceController) Query(ctx context.Context) (*QuiesceStatus, error)
- func (c *PostgresQuiesceController) Set(ctx context.Context, opts QuiesceSetOptions) (*QuiesceStatus, error)
- func (c *PostgresQuiesceController) WithAudit(em Emitter) *PostgresQuiesceController
- func (c *PostgresQuiesceController) WithClock(now func() time.Time) *PostgresQuiesceController
- type QuiesceController
- type QuiesceGate
- type QuiesceSetOptions
- type QuiesceStatus
- type Result
- type SecretFetcher
- type SecretsProvider
- type TenantLister
- type TenantResult
Constants ¶
const ( // DefaultQuiesceScope is the single scope key used in v1 (single-region). // The scope column exists so per-region/per-pool scoping is additive. DefaultQuiesceScope = "global" // DefaultQuiesceTTL is applied when QuiesceSetOptions.TTL is zero. The TTL // is the auto-release horizon: a migration job that crashes after Set can // never block provisioning beyond this without an explicit renew. DefaultQuiesceTTL = 30 * time.Minute // MaxQuiesceTTL caps QuiesceSetOptions.TTL so a fat-fingered multi-day TTL // cannot brick provisioning. Values above the ceiling are clamped. MaxQuiesceTTL = 2 * time.Hour // DefaultQuiesceTable is the control-plane table name used when an empty // table name is supplied to NewPostgresQuiesceController. DefaultQuiesceTable = "quiesce_flags" )
const DefaultSecretsPrefix = "gobricks/migrate/"
DefaultSecretsPrefix is the default name prefix used to look up tenant database credentials in a secret store. The full secret name is DefaultSecretsPrefix + tenantID.
const PostgresQuiesceTableDDL = `` /* 324-byte string literal not displayed */
PostgresQuiesceTableDDL is the CREATE TABLE statement used by CreateTable. Exported so operators managing schema externally can run it via their own tooling. The %s placeholder is replaced with the validated, quoted table identifier. One row per scope (id); v1 uses the single "global" scope.
expires_at is the TTL hard stop: IsSet treats now >= expires_at as released even when cleared_at is NULL, so a migration job that crashes after Set can never block provisioning beyond the TTL (read-side auto-release, no sweeper).
const PrincipalUnspecified = "<unspecified>"
PrincipalUnspecified is the sentinel emitted when an operator does not supply AppliedByPrincipal. The audit event still fires (so the gap is itself auditable) and the emitter logs a warning. Operators MUST pass an explicit principal in well-behaved callers; the framework refuses to invent one from IAM/OS context per ADR-019.
Variables ¶
var ErrDatabasePasswordTooShort = errors.New("migration: database password too short to safely redact Flyway output")
ErrDatabasePasswordTooShort is returned by the migrate path when a non-empty database password is shorter than minRedactablePasswordLength. Such passwords cannot be safely redacted from Flyway output, so the migration is rejected before running rather than suppressing the output and hiding the outcome. Match with errors.Is. Empty passwords (trust/IAM auth) are exempt.
var ErrEmptyTenantID = errors.New("migration: tenantID is empty")
ErrEmptyTenantID is returned when DBConfig is invoked with a blank tenant ID.
var ErrEnvFieldHasControlChar = errors.New("migration: env field contains forbidden control character (CR/LF/NUL)")
ErrEnvFieldHasControlChar is returned when a DatabaseConfig field destined for the Flyway subprocess environment contains a forbidden control character (CR, LF, or NUL). Go's exec.Cmd.Env passes strings to execve(2) verbatim and does not split on newlines, so this isn't a known injection path — but rejecting at the boundary prevents a compromised secret writer from propagating multi-line surprises into downstream logs or env-parsing tools.
var ErrFleetSplit = errors.New("migration: fleet split")
ErrFleetSplit is the Verdict of a run that dispatched at least one tenant but left at least one listed tenant failed or never dispatched: the fleet may be at mixed versions and needs a re-run.
var ErrFlywayCanceled = errors.New("migration: flyway canceled")
ErrFlywayCanceled marks a run the framework killed via parent-context cancellation (a fail-fast sibling abort in a parallel MigrateAll, or operator Ctrl-C) rather than its own deadline. Like a timeout, the SIGKILL can leave the schema partially applied — schema state is unknown; do not auto-retry blindly.
var ErrFlywayOutputUnparsed = errors.New("flyway output could not be parsed")
ErrFlywayOutputUnparsed wraps the underlying parse failure (errEmptyFlywayOutput or a JSON decode error) so a zero-exit run whose output is empty, malformed, or redaction-suppressed surfaces as a non-nil error instead of a silent success. Match with errors.Is.
var ErrFlywayReportedFailure = errors.New("flyway reported a failed migration")
ErrFlywayReportedFailure is returned when Flyway emitted a well-formed JSON envelope that itself reports failure (Result.Success == false), including when the subprocess exited 0. Match with errors.Is.
var ErrFlywayTimeout = errors.New("migration: flyway timed out")
ErrFlywayTimeout marks a run the framework killed on its own deadline. The schema state is UNKNOWN: Flyway may have applied part of a migration before the process group died, so this is not a clean failure and an automatic retry can race a partially applied change. Match with errors.Is — the audit ErrorClass cannot carry this (classifyFlywayError matches on Flyway's stdout, which a SIGKILLed JVM never gets to write), so the machine-readable signals are this sentinel and the migration.timed_out audit attribute.
var ErrIncompleteMigrationTarget = errors.New(
"migration: PostgreSQL requires database.host and database.database (or database.connectionstring) " +
"so the framework can build the Flyway JDBC URL; a partially filled database block, or any " +
"database.tls setting the framework cannot put on a URL, is rejected rather than silently " +
"deferring to the URL in flyway.conf, which the framework does not read")
ErrIncompleteMigrationTarget rejects a PostgreSQL config the framework cannot build a URL from when deferring to the conf would lose a guarantee: either the block is PARTIALLY filled (some identity field set, but not a usable host AND database — a target broken in transit, e.g. a tenant whose host arrived blank from a secret store), or database.tls is set and would silently fail to reach the connection, which is the whole of #1047. It covers the DISCRETE-FIELD shapes only: a database.tls block beside a connectionstring loses the same guarantee but has its own remedy — the DSN for the runtime, flyway.conf for the migration — so it gets its own sentinel, ErrMigrationTLSWithConnectionString. A block naming NO identity field and no TLS is conf-owned by construction and still defers. The error names the fields but never echoes a value; the per-tenant caller pairs it with TenantResult.TenantID. Match with errors.Is.
var ErrInvalidMigrationHost = errors.New("migration: database.host is not a valid hostname or IP address")
ErrInvalidMigrationHost rejects a database.host that is neither an IP literal nor a plain DNS name. The host is the one URL component that cannot be percent-encoded (it must stay a routable address), so it is validated instead: unescaped, a value like `h/?sslmode=disable&x=` ends the authority early and pgjdbc reads the injected parameters, turning a verify-full config into a cleartext connection to a host of the value's choosing. The error names the field but never echoes it — a misconfigured host can hold a whole DSN, password included. Match with errors.Is.
var ErrInvalidMigrationPort = errors.New("migration: database.port must be between 1 and 65535, or 0 for the driver default")
ErrInvalidMigrationPort rejects a database.port outside the TCP range. Zero is the documented "unset" case — urlAuthority omits the port and the driver takes its default — but a NEGATIVE port took that same branch, so a config that clearly meant a port silently connected to pgjdbc's 5432 instead, and nothing observed a port above 65535 either. config.validateOptionalDatabasePort applies the same `< 0 || > 65535` rule with the same zero-is-unset carve-out; this is the migrator's own copy, for the per-tenant configs that never passed it. The error names the field but never echoes the value. Match with errors.Is.
var ErrInvalidMigrationTLSMode = errors.New("migration: database.tls.mode is not a supported sslmode")
ErrInvalidMigrationTLSMode rejects a database.tls.mode that is not one of the libpq set once surrounding whitespace is trimmed. buildPostgresJDBCURL copies the mode onto the URL verbatim, so an unsupported one reached Flyway and failed inside the driver — or, worse, was ignored by it — instead of failing here as a typed error. config.Validate already refuses it (ADR-062); this is the migrator's own copy, for the per-tenant configs from a dynamic DBConfigProvider or the CLI's tenants.yaml that never passed it, the same reason the host, port and connectionstring rules have one. The offending MODE is echoed — it is a fixed keyword, not caller data — but nothing else from the config is, since a database block carries a password and a DSN. Match with errors.Is.
var ErrInvalidMigratorIdentity = errors.New("migration: invalid migrator identity")
ErrInvalidMigratorIdentity is returned when MigrateAllOptions.MigratorIdentity is set with an empty username or password, a password too short to redact from Flyway output, or a CR/LF/NUL in either.
var ErrInvalidPGIdentifier = errors.New("migration: PostgreSQL identifier rejected")
ErrInvalidPGIdentifier is returned by Validate when a role or schema name fails the safe-identifier check enforced by ProvisionPGRoles.
var ErrInvalidPrefix = errors.New("migration: invalid secrets prefix (must end with '/')")
ErrInvalidPrefix indicates the configured prefix is unusable.
var ErrInvalidQuiesceTTL = errors.New("migration: quiesce TTL must not be negative")
ErrInvalidQuiesceTTL is returned by Set when QuiesceSetOptions.TTL is negative. Zero is valid (means DefaultQuiesceTTL); negative is rejected rather than silently defaulted, so a bad operator input fails loudly instead of pausing provisioning for an unintended duration.
var ErrInvalidQuiesceTable = errors.New("migration: invalid quiesce table name")
ErrInvalidQuiesceTable is returned by NewPostgresQuiesceController when the supplied table name fails the safe-identifier check.
var ErrInvalidTenantID = errors.New("migration: tenantID contains characters outside [A-Za-z0-9_-] or exceeds 128 characters")
ErrInvalidTenantID is returned when DBConfig is invoked with a tenant ID that contains characters outside the [A-Za-z0-9_-] allowlist or exceeds the 128-character length bound.
var ErrMigrationMTLSUnsupported = errors.New(
"migration: PostgreSQL client-certificate TLS (database.tls.cert/database.tls.key) is not supported for Flyway migrations: " +
"the framework does not forward them as the JDBC sslcert/sslkey parameters, so it refuses rather than " +
"migrating without the client certificate; use database.tls.mode + database.tls.ca for server-authenticated TLS, " +
"or migrate outside the framework")
ErrMigrationMTLSUnsupported rejects a migration whose config asks for PostgreSQL client-certificate authentication. The limit is OURS, not pgjdbc's: the framework does not forward `database.tls.cert`/`key` as the JDBC `sslcert`/`sslkey` parameters, so it refuses rather than migrating without the client certificate the config asked for. Match with errors.Is.
var ErrMigrationTLSCARequiresVerify = errors.New(
"migration: database.tls.ca requires database.tls.mode verify-ca or verify-full: pgjdbc reads sslrootcert " +
"only under those two modes, so under require/allow/prefer (or an unset mode, which is prefer) the CA " +
"would be ignored and the migration would not authenticate the server")
ErrMigrationTLSCARequiresVerify rejects a database.tls.ca that cannot actually authenticate the server. pgjdbc reads `sslrootcert` only under `verify-ca` and `verify-full`; `require`, `allow` and `prefer` all use a NON-VALIDATING socket factory, and an unset mode is `prefer`, which also falls back to plaintext. So a config naming a CA under any of those got an unverified — possibly unencrypted — migration while reading as though it pinned one.
This is deliberately STRICTER than config.Validate, which admits `require` beside a ca (ADR-062): pgx treats `require` + ca as verify-ca, a documented libpq inheritance, so the RUNTIME really does verify there. pgjdbc does not, and the migrator answers for pgjdbc. The divergence is the point — the same config is honored at runtime and refused for migration rather than silently unverified. Match with errors.Is.
var ErrMigrationTLSCASystemUnsupported = errors.New(
"migration: database.tls.ca: system is not supported for Flyway migrations: it is a libpq/pgx sentinel for " +
"the platform trust store and pgjdbc has no equivalent, treating the value as a file path; point " +
"database.tls.ca at the CA certificate file itself for the migration configuration")
ErrMigrationTLSCASystemUnsupported rejects the `ca: system` sentinel for migrations. It is a libpq/pgx spelling meaning "the platform trust store", and pgjdbc has no equivalent: LibPQFactory special-cases nothing and treats the value as a FILE PATH, so `sslrootcert=system` names a file that does not exist (verified against pgjdbc REL42.7.12). Mapping it to the JVM's own default trust store would not be equivalent either — `cacerts` is a different trust set from the one pgx consults, so the migration would authenticate against CAs the runtime does not, which is the silent divergence ADR-085 exists to remove. Name a real CA file for the migration instead. Match with errors.Is.
var ErrMigrationTLSWithConnectionString = errors.New(
"migration: database.tls is set alongside database.connectionstring: the framework does not parse DSNs, " +
"so the TLS settings cannot reach the migration connection; remove the database.tls block, putting its " +
"settings in the connection string for the RUNTIME pool, and set the migration's own TLS parameters on " +
"the JDBC url in flyway.conf, which owns the migration connection for a connectionstring config; that " +
"url must also name the same host and database as the connection string, or the migration is encrypted " +
"but applied to the wrong target")
ErrMigrationTLSWithConnectionString rejects a PostgreSQL config that sets both `database.connectionstring` and `database.tls.*`. The framework does not parse DSNs, so it cannot lift the TLS material onto a URL it builds; deferring to the conf would run the migration on the DSN with the configured TLS silently dropped. config.Validate already refuses this shape (ADR-062), but a per-tenant DatabaseConfig can reach MigrateFor without ever passing through it — a dynamic DBConfigProvider, or the CLI's tenants.yaml — so the migrator fails closed on its own rather than trusting the caller to have validated. Match with errors.Is.
var ErrNameForFailed = errors.New("migration: NameFor failed to compose a secret name")
ErrNameForFailed indicates the custom NameFor hook returned an error or a blank (empty/whitespace-only) name for a validated tenant ID.
var ErrNoConfigProvider = errors.New("migration: database.DBConfigProvider is nil")
ErrNoConfigProvider is returned when MigrateAll is called without a DBConfigProvider.
var ErrNoFetcher = errors.New("migration: SecretsProvider.Fetch is nil")
ErrNoFetcher indicates the SecretsProvider was constructed without a Fetch function.
var ErrNoLister = errors.New("migration: TenantLister is nil")
ErrNoLister is returned when MigrateAll is called without a TenantLister.
var ErrNothingAttempted = errors.New("migration: no tenant attempted")
ErrNothingAttempted is the Verdict of a run that dispatched no tenant (empty listing, listing failure, context done or quiesce set before the first dispatch): no schema was touched.
var ErrPGRoleFloorViolated = errors.New("migration: role holds attributes above the provisioning floor")
ErrPGRoleFloorViolated is returned by CheckPGRoleFloor when a role holds an attribute the provisioning floor denies; the message names each one.
var ErrPGRoleNotFound = errors.New("migration: role not found")
ErrPGRoleNotFound is returned by CheckPGRoleFloor when no role has the name.
var ErrPGRolePasswordHasControlChar = errors.New("migration: role password contains forbidden control character (CR/LF/NUL)")
ErrPGRolePasswordHasControlChar is returned by Validate when a role password contains CR, LF, or NUL. Such a password cannot be carried log-safely through the provisioning path: summarizeStmt collapses a failing statement to its first line, so an embedded newline would split a redacted summary apart. PostgreSQL itself accepts these passwords — the rejection is ours, at this API's boundary. Mirrors ErrEnvFieldHasControlChar, which guards the Flyway subprocess environment.
var ErrPGRoleSkippedMigratorHasPassword = errors.New("migration: MigratorPassword must be empty when SkipMigratorRole is set")
ErrPGRoleSkippedMigratorHasPassword is returned by Validate when SkipMigratorRole is set together with a non-empty MigratorPassword, so a leftover password can never alter a migrator role managed out of band.
var ErrQuiesceBlocked = errors.New("migration: paused by deployment quiesce flag")
ErrQuiesceBlocked is returned by MigrateAll when the deployment quiesce flag is set: dispatch of not-yet-started tenants stops and the partial result is returned. Distinguish a paused run from a failed one via errors.Is.
var ErrQuiesceNotSet = errors.New("migration: no active quiesce flag")
ErrQuiesceNotSet is returned by Clear when no flag has ever been set, or it was already explicitly cleared. An expired-but-uncleared flag is still clearable and does not trigger this error.
var ErrReservedPGIdentifier = errors.New("migration: identifier is reserved by PostgreSQL")
ErrReservedPGIdentifier is returned by Validate when a spec field names something PostgreSQL reserves, matched case-insensitively: "public" or a "pg_"-prefixed name in any of the three fields, plus "information_schema" for Schema alone. It is always wrapped with ErrInvalidPGIdentifier, so a caller matching the identifier sentinel keeps matching, and no IdentifierPolicy can waive it. Such a name passes every charset check while landing the tenant's tables in the schema every role on the instance can read (Schema "public") or granting that tenant's DML to every role on the instance (a role named "public", which PostgreSQL's RoleSpec maps onto the PUBLIC pseudo-role).
var ErrSecretMalformed = errors.New("migration: secret payload malformed")
ErrSecretMalformed indicates the secret payload could not be parsed into a usable DatabaseConfig in either canonical or RDS-rotation form.
"migration: shared migrator requires an explicit database.postgresql.schema; the framework does not read " +
"flyway.conf, so a conf-owned flyway.defaultSchema cannot aim this run")
ErrSharedMigratorSchemaRequired is returned by every verb when a runner built with WithSharedMigrator (see its godoc) targets a PostgreSQL DatabaseConfig whose database.postgresql.schema is empty. Match with errors.Is.
Functions ¶
func CheckPGRoleFloor ¶ added in v0.66.0
CheckPGRoleFloor reports whether role still sits at the attribute floor the provisioning template creates roles with: no SUPERUSER, CREATEDB, CREATEROLE, REPLICATION or BYPASSRLS. It only reads pg_catalog.pg_roles, so a non-superuser provisioner can run it. With PGRoleSpec.SkipFloorReassert set, provisioning no longer repairs drift; this reports it instead.
Returns an error for a nil db; ErrInvalidPGIdentifier when role fails the identifier floor or the reserved-name rule PGRoleSpec.Validate applies (before any query); ErrPGRoleNotFound when no role has the name; the wrapped driver error when the read fails; or ErrPGRoleFloorViolated naming every attribute held above the floor.
func PGRoleProvisioningSQL ¶ added in v0.32.0
func PGRoleProvisioningSQL(spec *PGRoleSpec) ([]string, error)
PGRoleProvisioningSQL returns the SQL statements that ProvisionPGRoles would execute for spec, in order. Use this when operators want to inspect or apply the provisioning manually via psql, or feed it into their own migration runner (Flyway, Liquibase) rather than the Go helper. The executing role needs the privileges listed on ProvisionPGRoles.
Returns Validate's error when spec fails it — ErrInvalidPGIdentifier, ErrPGRolePasswordHasControlChar or ErrPGRoleSkippedMigratorHasPassword. The returned slice does not include trailing semicolons; callers concatenating them into a single script should add separators themselves.
SECURITY: when spec.MigratorPassword or spec.RuntimePassword is non-empty, the returned statements include the password as an in-clear SQL literal (`ALTER ROLE "..." PASSWORD '<secret>'`). Treat the returned slice as a sensitive value: do not echo it to logs, CI build artifacts, or anywhere the original credential wouldn't be acceptable. Callers preparing scripts for review should redact the literal before persisting to disk.
func ProvisionPGRoles ¶ added in v0.32.0
ProvisionPGRoles applies the role-pair + schema described by spec to the PostgreSQL instance reachable via db. All statements are idempotent: a rerun against an already-provisioned tenant is a no-op, except that MigratorPassword / RuntimePassword (when non-empty) are reapplied on every call to support secret rotation.
db MUST be authenticated as a superuser, which can run every spec, or as a CREATEROLE NOSUPERUSER provisioner holding CREATE on the database, which needs three things on PostgreSQL 16+:
- spec.SkipFloorReassert (see that field for why);
- membership in MigratorRole WITH INHERIT TRUE, SET TRUE — SET for CREATE SCHEMA ... AUTHORIZATION, INHERIT for ALTER DEFAULT PRIVILEGES FOR ROLE and for the grants on the schema the migrator owns. A creator is granted a role with ADMIN alone, so for a migrator this call creates, set createrole_self_grant = 'set, inherit' on the provisioner beforehand; for one created out of band (SkipMigratorRole), a DBA runs GRANT <migrator> TO <provisioner> WITH INHERIT TRUE, SET TRUE once;
- ADMIN on every role it alters (a password or search_path), which it holds on the roles it created itself.
The migrator and runtime roles created here cannot self-provision: they are denied SUPERUSER, CREATEDB, CREATEROLE, BYPASSRLS, and REPLICATION per the deliverables of #378.
Each statement lands independently here: db is a connection, not a transaction, so a partial-progress failure leaves the steps that already succeeded in place. Callers should rerun the same spec to converge; the idempotent template makes that safe. Nothing in the emitted list forces that mode — use ProvisionPGRolesTx to run the same list inside a transaction the caller owns.
func ProvisionPGRolesTx ¶ added in v0.65.0
ProvisionPGRolesTx applies the same role-pair + schema list as ProvisionPGRoles, but against a caller-owned database.Executor, so provisioning can ride the transaction that also creates the tenant's tables, ledger and registry row. database.Tx and database.Interface both satisfy database.Executor as they stand, so the caller hands over the transaction (or the connection) it already holds without an adapter.
Every statement emitted here is ordinary transactional DDL, and CREATE ROLE is not among the statements PostgreSQL refuses inside a transaction block; the full list and the argument are in wiki/migration_provisioning.md.
exec MUST be authenticated as described on ProvisionPGRoles; a typed-nil executor is refused before any statement runs, like a nil interface. A plain database.Interface connection also satisfies database.Executor; passed one, each statement lands independently exactly as on the ProvisionPGRoles path, with the same rerun-to-converge guidance. On a real transaction that guidance does not apply at all: a failed statement puts the transaction in a failed block, every later command is rejected with 25P02 until the block is ended (or rolled back to a savepoint taken before the failure — the one way partial state can survive), and ending it discards it.
Types ¶
type Action ¶ added in v0.31.0
type Action int
Action selects which Flyway operation MigrateAll runs against each tenant.
type AuditContext ¶ added in v0.32.0
type AuditContext struct {
// Principal identifies who triggered the migration (operator username,
// service account name, pipeline identifier). Empty values emit with
// PrincipalUnspecified + a warning so the gap is itself auditable.
Principal string
// GitCommitSHA records the source-tree commit the migration was built
// from. Useful for correlating an audit event to a specific deployment.
GitCommitSHA string
// PipelineRunID is an opaque CI/CD run identifier (e.g. a GitHub
// Actions run ID, a Jenkins build number). Lets compliance reporting
// trace an audit event back to a pipeline run.
PipelineRunID string
// Target overrides the audit event's Target field. Defaults to the
// database name (db.Database) when empty. Useful for multi-tenant runs
// where the tenant ID is more informative for compliance correlation
// than the per-tenant schema name.
Target string
}
AuditContext groups the per-call audit fields that flow into every migration.applied event. Operators MUST supply Principal explicitly per ADR-019; GitCommitSHA, PipelineRunID, and Target are optional but strongly recommended for deployment-time runs.
type AuditEvent ¶ added in v0.32.0
type AuditEvent struct {
Type AuditEventType
Target string
AppliedByPrincipal string
StartedAt time.Time
CompletedAt time.Time
Outcome AuditOutcome
// Version is the Flyway version applied. Set on migration.applied.
Version string
// FromState / ToState describe a provisioning-state-machine transition.
// Set on state.transitioned.
FromState string
ToState string
// ErrorClass is set when Outcome == failed; one of the published
// constants above. Empty for success/skipped outcomes.
ErrorClass ErrorClass
// GitCommitSHA and PipelineRunID are optional but strongly recommended
// for deployment-time runs; sourced from explicit caller input.
GitCommitSHA string
PipelineRunID string
// Attributes is a free-form extension point for callsite-specific
// metadata. Keys SHOULD use dotted lowercase (e.g. "migration.vendor").
Attributes map[string]string
}
AuditEvent is the canonical payload that flows into both the OpenTelemetry emission path and the optional AuditRecorder. The two paths share this struct so schemas cannot drift. Backwards-compatible additions follow Go's struct-additive rules; removing a field is a breaking change.
Target is an opaque schema/database identifier (tenant ID or schema name) and MUST NOT be a DSN — credentials never appear in audit events.
auditEmitter.Emit snapshots this struct with a shallow copy; a future field of reference type (map/slice/pointer) MUST be added to Emit's clone step too, or the snapshot will still alias the caller's value.
type AuditEventType ¶ added in v0.32.0
type AuditEventType string
AuditEventType enumerates the four migration audit-event types defined by ADR-019. Engine-layer emission covers migration.applied; orchestrator-layer emission covers state.transitioned (provisioning.Executor); quiesce.* events land with the deployment quiesce gate (#380).
const ( // AuditEventTypeMigrationApplied marks a Flyway migration application // (successful or failed) against a target. AuditEventTypeMigrationApplied AuditEventType = "migration.applied" // AuditEventTypeStateTransitioned marks a provisioning-state-machine // transition. Emitted by provisioning.Executor for every persisted edge. AuditEventTypeStateTransitioned AuditEventType = "state.transitioned" // AuditEventTypeQuiesceSet marks an operator setting the deployment // quiesce flag. Emitted by a QuiesceController wired with WithAudit. AuditEventTypeQuiesceSet AuditEventType = "quiesce.set" // AuditEventTypeQuiesceCleared marks an operator clearing the deployment // quiesce flag. Emitted by a QuiesceController wired with WithAudit. AuditEventTypeQuiesceCleared AuditEventType = "quiesce.cleared" )
type AuditOutcome ¶ added in v0.32.0
type AuditOutcome string
AuditOutcome is the terminal outcome of the audited operation.
const ( AuditOutcomeSuccess AuditOutcome = "success" AuditOutcomeFailed AuditOutcome = "failed" AuditOutcomeSkipped AuditOutcome = "skipped" )
type AuditRecorder ¶ added in v0.32.0
type AuditRecorder interface {
Record(ctx context.Context, event *AuditEvent) error
}
AuditRecorder is the opt-in delivery path described in ADR-019. When wired (typically via FlywayMigrator.WithAuditRecorder), every AuditEvent fires to Record after the OTel emission, on a separate goroutine with a bounded send queue.
Record receives a non-nil *AuditEvent — the pointer matches the framework convention for medium-sized event payloads (see outbox.OutboxPublisher). The event is an isolated snapshot taken at Emit time (a shallow copy plus a deep clone of Attributes): caller-side reuse or mutation of the original event after Emit cannot affect delivered records, and the sink is free to mutate its own copy without synchronization.
The framework calls Record with a fresh background context that may be canceled by FlywayMigrator.Close. Implementations SHOULD respect ctx.Done() for prompt cancellation, but the framework does not retry on the sink's behalf — sink owners requiring zero-loss audit must back their implementation with a durable buffer (Kafka commit-log, S3 staging, etc.).
Errors returned from Record are logged as warnings and increment the migration.audit.sink_failures counter; they do NOT abort the migration. This is a deliberate trade-off per ADR-019: audit must not block business work.
type Config ¶
type Config struct {
FlywayPath string // Path to the Flyway executable
ConfigPath string // Path to the configuration file
MigrationPath string // Path to migration scripts
Timeout time.Duration // Timeout for migration operations
Environment string // Environment (e.g. "development", "staging", "production", or any org-specific alias such as "local"/"stg"/"prd" — not enum-validated per ADR-022)
DryRun bool // Only validate, do not execute
// Audit carries the per-call audit-event context required by ADR-019.
// Populated by operators (CLI flags) or pipelines (env vars or library
// call argument). The framework will NOT infer Principal from IAM/OS
// context — empty values flow through with a warning log.
Audit AuditContext
}
Config configuration for migrations
type Emitter ¶ added in v0.39.0
type Emitter interface {
// Emit dispatches a single AuditEvent through the always-on OpenTelemetry
// path and the optional AuditRecorder fan-out. Safe for concurrent use;
// never blocks on the sink.
Emit(ctx context.Context, ev *AuditEvent)
// Close drains the optional AuditRecorder queue and tears down the
// background consumer. Safe to call when no sink is configured (no-op).
Close(ctx context.Context) error
}
Emitter is the public emission seam for migration audit events. Both the engine layer (FlywayMigrator) and the orchestrator layer (provisioning.Executor) route AuditEvents through an Emitter so the OTel span + structured-log + optional-sink schema can never drift between migration.applied and state.transitioned events.
Construct one with NewEmitter. The zero value is not usable.
func NewEmitter ¶ added in v0.39.0
func NewEmitter(log logger.Logger, sink AuditRecorder) Emitter
NewEmitter constructs the public Emitter backing both audit layers. log must be non-nil; sink may be nil for OTel-only emission (span + structured log). When sink is non-nil it receives every event after the OTel emission, on a bounded-queue background goroutine per ADR-019 — call Close to drain it.
type ErrorClass ¶ added in v0.32.0
type ErrorClass string
ErrorClass is a stable string from a published taxonomy that downstream alerting can pin on. ADR-019 publishes seven values; the list is additive (new classes are non-breaking; removing one is breaking). Set only when Outcome == failed; otherwise leave empty.
const ( // ErrorClassChecksumMismatch — Flyway detected an applied script was // modified after the fact. ErrorClassChecksumMismatch ErrorClass = "checksum_mismatch" // ErrorClassLockTimeout — could not acquire the advisory / DBMS_LOCK // within the configured timeout. ErrorClassLockTimeout ErrorClass = "lock_timeout" // ErrorClassSchemaHistoryCorrupt — flyway_schema_history is in an // inconsistent state. ErrorClassSchemaHistoryCorrupt ErrorClass = "schema_history_corrupt" // ErrorClassTargetNotReady — the state-machine target is not in a state // that allows migration. Set by the orchestrator (#379), not the engine. ErrorClassTargetNotReady ErrorClass = "target_not_ready" // ErrorClassTargetUnreachable — the target database refused, timed out, // or DNS-failed. ErrorClassTargetUnreachable ErrorClass = "target_unreachable" // ErrorClassQuiesceBlocked — the quiesce flag was set; the run aborted // before any Flyway work. Set by the orchestrator (#380), not the engine. ErrorClassQuiesceBlocked ErrorClass = "quiesce_blocked" // ErrorClassInternal is the catch-all for unclassified panics and // unexpected errors. ErrorClassInternal ErrorClass = "internal_error" )
type FlywayMigrator ¶
type FlywayMigrator struct {
// contains filtered or unexported fields
}
FlywayMigrator handles database migrations using Flyway
func NewFlywayMigrator ¶
func NewFlywayMigrator(cfg *config.Config, log logger.Logger) *FlywayMigrator
NewFlywayMigrator creates a new instance of the migrator with the always-on OpenTelemetry audit-emission path wired up. Call WithAuditRecorder to add an optional compliance-grade durable delivery path per ADR-019.
func (*FlywayMigrator) Close ¶ added in v0.32.0
func (fm *FlywayMigrator) Close(ctx context.Context) error
Close drains the optional AuditRecorder queue and tears down the audit consumer goroutine. Safe to call when no sink is configured. Honors ctx for shutdown deadline; events still in flight when ctx expires are silently dropped (their OTel emission already succeeded).
func (*FlywayMigrator) DefaultMigrationConfig ¶ added in v0.19.0
func (fm *FlywayMigrator) DefaultMigrationConfig() *Config
DefaultMigrationConfig returns the default configuration for migrations
func (*FlywayMigrator) DefaultMigrationConfigForVendor ¶ added in v0.31.0
func (fm *FlywayMigrator) DefaultMigrationConfigForVendor(vendor string) *Config
DefaultMigrationConfigForVendor returns the default migration config for the given database vendor (e.g. "postgresql", "oracle"). Used by multi-tenant migrations where each tenant may run a different vendor than the migrator's own cfg.Database.Type. Unknown vendors fall back to the migrator's configured Database.Type so the vendor string never reaches filesystem path interpolation unvalidated; if even that is unknown, an "unknown" segment is used so callers see an obvious error rather than a path-traversal artifact.
func (*FlywayMigrator) Info ¶
func (fm *FlywayMigrator) Info(ctx context.Context, cfg *Config) error
Info shows information about the status of migrations against the migrator's database.
func (*FlywayMigrator) InfoFor ¶ added in v0.31.0
func (fm *FlywayMigrator) InfoFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) error
InfoFor shows migration status for the supplied database.
func (*FlywayMigrator) Migrate ¶
Migrate executes pending migrations against the migrator's configured database. It returns a non-nil error when the Flyway process fails, when its JSON output is empty/malformed/redaction-suppressed (errors.Is ErrFlywayOutputUnparsed), or when Flyway reports a failed migration via a success=false envelope even on a zero exit (errors.Is ErrFlywayReportedFailure). The Result is returned best-effort alongside any error.
func (*FlywayMigrator) MigrateFor ¶ added in v0.31.0
func (fm *FlywayMigrator) MigrateFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) (Result, error)
MigrateFor executes pending migrations against the supplied database. Used by multi-tenant migrations to target a tenant-specific DatabaseConfig. See Migrate for the Result and error contract.
func (*FlywayMigrator) RunMigrationsAtStartup ¶
func (fm *FlywayMigrator) RunMigrationsAtStartup(ctx context.Context) error
RunMigrationsAtStartup executes migrations automatically at application startup. The structured Result is discarded here; downstream consumers pick it up via the migration.applied audit event.
func (*FlywayMigrator) Validate ¶
func (fm *FlywayMigrator) Validate(ctx context.Context, cfg *Config) error
Validate validates migrations without executing them against the migrator's database.
func (*FlywayMigrator) ValidateFor ¶ added in v0.31.0
func (fm *FlywayMigrator) ValidateFor(ctx context.Context, db *config.DatabaseConfig, cfg *Config) error
ValidateFor validates migrations for the supplied database.
func (*FlywayMigrator) WithAuditRecorder ¶ added in v0.32.0
func (fm *FlywayMigrator) WithAuditRecorder(sink AuditRecorder) *FlywayMigrator
WithAuditRecorder registers an optional AuditRecorder for compliance-grade durable delivery alongside the always-on OpenTelemetry emission. Replaces any previously-configured sink. Returns the receiver for chaining.
Intended to be called once at startup. The sink runs on its own goroutine with a bounded send queue per ADR-019 — slow sinks cannot stall migrations, and sink errors are logged but do not abort the migration. Call Close to drain the queue on shutdown.
func (*FlywayMigrator) WithSharedMigrator ¶ added in v0.66.0
func (fm *FlywayMigrator) WithSharedMigrator() *FlywayMigrator
WithSharedMigrator declares that this runner must not rely on an implicit search_path, and makes an explicit Flyway schema target mandatory. Returns the receiver for chaining; intended to be called once at startup.
With it set, every PostgreSQL DatabaseConfig must aim Flyway explicitly: an empty database.postgresql.schema is refused with ErrSharedMigratorSchemaRequired before Flyway runs, for migrate, validate and info alike, instead of silently resolving against the connection's default schema (typically public) and reporting success. The framework never reads flyway.conf, so a conf-owned flyway.defaultSchema does not satisfy the requirement — postgresql.schema must carry the target. That covers the runner's own database.* config, not just per-tenant ones: Migrate, Validate and Info gate on it too, so RunMigrationsAtStartup on a runner built with this flag and an empty database.postgresql.schema fails startup. Oracle is unaffected: its schema is the connecting user, which is already per-tenant.
A migrator role shared across tenants is the motivating case — it is provisioned with PGRoleSpec.SkipMigratorRole, so it gets no role-level search_path default and the explicit -schemas/-defaultSchema args are the only thing aiming Flyway at a tenant. It is not the only one: a hand-provisioned migrator, a partially applied PGRoleProvisioningSQL script, and search_path drift under SkipFloorReassert leave the identical gap. Set this whenever the role's own default cannot be trusted to aim the run.
type MemoryQuiesceController ¶ added in v0.39.0
type MemoryQuiesceController struct {
// contains filtered or unexported fields
}
MemoryQuiesceController is an in-memory QuiesceController for unit tests and single-process use. The Set/Clear/IsSet/Query operations are safe for concurrent use (mu guards the rec). WithClock/WithAudit are builder methods — configure them before concurrent use, mirroring MemoryStore. Construct with NewMemoryQuiesceController.
func NewMemoryQuiesceController ¶ added in v0.39.0
func NewMemoryQuiesceController() *MemoryQuiesceController
NewMemoryQuiesceController returns an empty in-memory controller (not quiesced).
func (*MemoryQuiesceController) Clear ¶ added in v0.39.0
func (c *MemoryQuiesceController) Clear(ctx context.Context, by string) (*QuiesceStatus, error)
Clear deactivates an uncleared flag (active OR auto-released by TTL) — the unconditional operator override. Returns ErrQuiesceNotSet only when nothing has ever been set or the row was already explicitly cleared; an expired-but- uncleared row is still clearable so operators can tidy a crashed deploy's flag and emit the quiesce.cleared audit trail.
func (*MemoryQuiesceController) CreateTable ¶ added in v0.39.0
func (c *MemoryQuiesceController) CreateTable(_ context.Context) error
CreateTable is a no-op for the in-memory controller.
func (*MemoryQuiesceController) IsSet ¶ added in v0.39.0
func (c *MemoryQuiesceController) IsSet(_ context.Context) (bool, error)
IsSet reports whether the flag is currently active (uncleared and unexpired).
func (*MemoryQuiesceController) Query ¶ added in v0.39.0
func (c *MemoryQuiesceController) Query(_ context.Context) (*QuiesceStatus, error)
Query returns the full status snapshot.
func (*MemoryQuiesceController) Set ¶ added in v0.39.0
func (c *MemoryQuiesceController) Set(ctx context.Context, opts QuiesceSetOptions) (*QuiesceStatus, error)
Set activates (or renews) the flag.
func (*MemoryQuiesceController) WithAudit ¶ added in v0.39.0
func (c *MemoryQuiesceController) WithAudit(em Emitter) *MemoryQuiesceController
WithAudit enables quiesce.set / quiesce.cleared audit emission through the shared migration.Emitter. The audited principal is the operator who performed the action (QuiesceSetOptions.By / the Clear `by` argument), never inferred.
func (*MemoryQuiesceController) WithClock ¶ added in v0.39.0
func (c *MemoryQuiesceController) WithClock(now func() time.Time) *MemoryQuiesceController
WithClock installs a deterministic clock (tests). Passing nil restores time.Now. Builder method — call before concurrent use. Returns the controller for chaining.
type MigrateAllOptions ¶ added in v0.31.0
type MigrateAllOptions struct {
// BaseConfig supplies Flyway timeout / paths. ConfigPath and
// MigrationPath are auto-resolved per vendor when zero.
BaseConfig *Config
// ContinueOnError keeps iterating after the first per-tenant failure.
// Default false (fail-fast).
ContinueOnError bool
// Parallelism caps concurrent tenant migrations. 0 or 1 = sequential.
// Implementation caps the value to a reasonable maximum to avoid
// connection storms.
Parallelism int
// Logger receives progress updates. May be nil.
Logger logger.Logger
// Hook is invoked after each tenant completes (success or failure).
// Useful for streaming progress to the CLI / CI logs. May be nil.
Hook func(TenantResult)
// Quiesce, when set, gates tenant dispatch on the deployment quiesce flag:
// once the flag is observed set, no further tenants are dispatched (in-flight
// tenants drain) and MigrateAll returns ErrQuiesceBlocked with the partial
// result. Nil disables the check (fully opt-in). Check errors fail open.
Quiesce QuiesceGate
// MigratorIdentity, when set, replaces the username and password on a copy of
// every tenant's resolved database config before Flyway runs; host, port,
// database, schema targeting and TLS stay the tenant's. Nil keeps the
// provider's credentials.
MigratorIdentity *MigratorIdentity
}
MigrateAllOptions tunes per-tenant execution.
type MigrateAllResult ¶ added in v0.31.0
type MigrateAllResult struct {
Action Action
// Results holds one row per dispatched tenant. A listed tenant that was
// never dispatched has no row; it appears in NeverDispatched instead.
Results []TenantResult
// NeverDispatched holds the listed tenant IDs the run stopped before
// dispatching (context done, quiesce, fail-fast), in listing order.
NeverDispatched []string
}
MigrateAllResult aggregates per-tenant results from a MigrateAll run.
func MigrateAll ¶ added in v0.31.0
func MigrateAll( ctx context.Context, migrator *FlywayMigrator, lister TenantLister, configs database.DBConfigProvider, action Action, opts MigrateAllOptions, ) (*MigrateAllResult, error)
MigrateAll lists tenants via lister, resolves each tenant's database config via configs (the existing database.DBConfigProvider abstraction), and runs the chosen Flyway action against every one. Sequential fail-fast unless opts say otherwise.
func (*MigrateAllResult) Failed ¶ added in v0.31.0
func (r *MigrateAllResult) Failed() []TenantResult
Failed returns the dispatched tenant results whose Err is non-nil. Tenants that were never dispatched are not included; see NeverDispatched and Verdict.
func (*MigrateAllResult) Listed ¶ added in v0.66.0
func (r *MigrateAllResult) Listed() int
Listed returns how many tenant IDs the TenantLister returned: the dispatched rows plus the never-dispatched IDs.
func (*MigrateAllResult) Verdict ¶ added in v0.66.0
func (r *MigrateAllResult) Verdict() error
Verdict classifies the run as a whole: nil when at least one tenant was listed and every listed tenant was dispatched and succeeded, ErrFleetSplit, or ErrNothingAttempted. A nil result is ErrNothingAttempted. It is independent of MigrateAll's returned error.
type MigratorIdentity ¶ added in v0.66.0
MigratorIdentity is the shared role Flyway connects as across the fleet.
Its pair is FlywayMigrator.WithSharedMigrator, which refuses a tenant that does not aim Flyway at an explicit schema. Setting MigratorIdentity does NOT arm that guard: database-per-tenant PostgreSQL with one migrator role across every database and the target schema (typically public) in each is a legitimate deployment where the role-level search_path is correct everywhere, so inferring the requirement from this field would break real setups. The signal is explicit for that reason.
type PGIdentifierChecker ¶ added in v0.65.0
PGIdentifierChecker is a caller-supplied check layered on top of the identifier floor (database/identifier.Validate for PostgreSQL). Validate consults it once per identifier, after the floor and the reserved-name rule have accepted that identifier, so a policy can only refuse more — never admit a name either of those rejects. A returned error is wrapped with ErrInvalidPGIdentifier and the failing field name, so the policy itself does not need to identify the identifier it judged.
type PGIdentifierCheckerFunc ¶ added in v0.65.0
PGIdentifierCheckerFunc adapts a plain function to PGIdentifierChecker.
A typed nil of this type stored in PGRoleSpec.IdentifierPolicy is NOT the same as no policy: the interface value is non-nil, so Validate does consult it. Rather than panic on the nil call, the adapter refuses every identifier, so such a spec fails Validate instead of taking the process down. Leave the field unset for "no policy".
A func value is not comparable, so a PGRoleSpec holding one is not safely comparable either. == compares fields in order and stops at the first difference, so it panics only when every earlier field is equal and the comparison reaches IdentifierPolicy; using such a spec as a map key always panics, because hashing reads every field. Compare such specs field by field or hold them by pointer; a comparable PGIdentifierChecker implementation keeps the spec comparable.
func (PGIdentifierCheckerFunc) CheckPGIdentifier ¶ added in v0.65.0
func (f PGIdentifierCheckerFunc) CheckPGIdentifier(value string) error
CheckPGIdentifier calls f, or refuses when f is nil.
type PGRoleSpec ¶ added in v0.32.0
type PGRoleSpec struct {
// Schema is the per-tenant schema name (e.g. "tenant_a"). Owned by
// MigratorRole after provisioning.
Schema string
// MigratorRole owns Schema and is used exclusively by the migration runner.
// Must differ from RuntimeRole.
MigratorRole string
// MigratorPassword is optionally assigned to MigratorRole via ALTER ROLE
// PASSWORD on every call. Useful for the one-time bootstrap and for
// secret rotation. Leave empty when credentials are managed externally
// (e.g., the role is created out-of-band and password set via a
// privileged migration pipeline).
MigratorPassword string
// RuntimeRole is the per-tenant DML-only role consumed by the running
// service. Must differ from MigratorRole.
RuntimeRole string
// RuntimePassword is optionally assigned to RuntimeRole. Same semantics
// as MigratorPassword — passing it on every call makes secret rotation a
// no-op rerun.
RuntimePassword string
// SkipMigratorRole leaves MigratorRole untouched: no CREATE ROLE, attribute
// lockdown, password or search_path statement is emitted for it. Set it when
// the migrator is created out of band or shared across tenants. MigratorRole
// is still required — it remains the schema's AUTHORIZATION and the FOR ROLE
// target of the default privileges — and MigratorPassword must be empty.
SkipMigratorRole bool
// SkipFloorReassert drops the ALTER ROLE that re-applies the attribute floor
// to a role on every call. CREATE ROLE still carries the full floor, so a
// role this spec creates starts locked down, but later drift is no longer
// repaired — report it with CheckPGRoleFloor. A provisioner that is not a
// superuser needs it: of the five lockdown attributes, PostgreSQL lets a
// CREATEROLE-only role ALTER only NOCREATEROLE.
SkipFloorReassert bool
// IdentifierPolicy optionally tightens the identifier rule Validate
// applies to Schema, MigratorRole and RuntimeRole; nil means the floor alone.
// Leave the field unset for that — storing a typed nil
// PGIdentifierCheckerFunc is a non-nil interface, and is refused. A spec
// holding a PGIdentifierCheckerFunc is not comparable; see that type.
IdentifierPolicy PGIdentifierChecker
}
PGRoleSpec describes a PostgreSQL role-pair plus per-tenant schema for the migrator-vs-runtime role-separation model defined in issue #378.
Migrator role: owns the per-tenant schema, holds DDL privileges, used exclusively by the migration runner. Unless SkipMigratorRole is set, created with NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS so even a compromised migrator credential cannot escalate itself.
Runtime role: per-tenant LOGIN role granted only DML on the tenant schema. Does not own the schema, so PostgreSQL's default ownership model rejects ALTER/CREATE/DROP statements from this role without any explicit REVOKE. Granted SELECT/INSERT/UPDATE/DELETE on existing AND future tables via ALTER DEFAULT PRIVILEGES so subsequent migrations don't need per-script grants.
func (*PGRoleSpec) Validate ¶ added in v0.32.0
func (s *PGRoleSpec) Validate() error
Validate reports whether the spec's identifiers pass database/identifier.Validate for PostgreSQL (the shared bare-identifier grammar and 63-byte cap), the two roles differ, and neither password carries CR, LF, or NUL. Tenant IDs sourced from outside should be normalized to that grammar upstream; rejecting at the migration boundary gives a single forcing function rather than scattering input filters. Every identifier additionally passes the reserved-name rule: "public" and any "pg_"-prefixed name are refused case-insensitively with ErrReservedPGIdentifier, and "information_schema" is refused for Schema alone. A non-nil IdentifierPolicy is consulted once per identifier after the floor and the reserved-name rule have accepted it, in Schema → MigratorRole → RuntimeRole order, stopping at the first refusal — so a policy can never re-admit a reserved name. Returns ErrInvalidPGIdentifier wrapped with the offending field name, value and the identifier sentinel for an identifier failure, or ErrPGRolePasswordHasControlChar wrapped with the offending field name — never the value — for a password failure, or ErrPGRoleSkippedMigratorHasPassword when SkipMigratorRole is set with a non-empty MigratorPassword.
type PostgresQuiesceController ¶ added in v0.39.0
type PostgresQuiesceController struct {
// contains filtered or unexported fields
}
PostgresQuiesceController is a PostgreSQL-backed QuiesceController. It stores the flag in a control-plane table (zero new dependencies) so it survives a process restart and is queryable by operators / the CLI. Construct with NewPostgresQuiesceController.
func NewPostgresQuiesceController ¶ added in v0.39.0
func NewPostgresQuiesceController(db *sql.DB, tableName string) (*PostgresQuiesceController, error)
NewPostgresQuiesceController returns a controller backed by db. An empty tableName resolves to DefaultQuiesceTable. The name may be schema-qualified ("schema.table"). Returns ErrInvalidQuiesceTable if it fails the safe- identifier check.
func (*PostgresQuiesceController) Clear ¶ added in v0.39.0
func (c *PostgresQuiesceController) Clear(ctx context.Context, by string) (*QuiesceStatus, error)
Clear deactivates an uncleared flag (active OR auto-released by TTL) — the unconditional operator override, keyed on scope, not the setter's session. Returns ErrQuiesceNotSet only when no row exists or it was already cleared; an expired-but-uncleared row is still clearable so operators can tidy a crashed deploy's flag and produce the quiesce.cleared audit trail.
func (*PostgresQuiesceController) CreateTable ¶ added in v0.39.0
func (c *PostgresQuiesceController) CreateTable(ctx context.Context) error
CreateTable provisions the quiesce table idempotently.
func (*PostgresQuiesceController) IsSet ¶ added in v0.39.0
func (c *PostgresQuiesceController) IsSet(ctx context.Context) (bool, error)
IsSet reports whether the flag is currently active.
func (*PostgresQuiesceController) Query ¶ added in v0.39.0
func (c *PostgresQuiesceController) Query(ctx context.Context) (*QuiesceStatus, error)
Query returns the full status snapshot. A missing row is reported as the zero status (never quiesced).
func (*PostgresQuiesceController) Set ¶ added in v0.39.0
func (c *PostgresQuiesceController) Set(ctx context.Context, opts QuiesceSetOptions) (*QuiesceStatus, error)
Set activates (or renews) the flag. Idempotent on the scope id: an existing row is updated (renewing expires_at and un-clearing it).
func (*PostgresQuiesceController) WithAudit ¶ added in v0.39.0
func (c *PostgresQuiesceController) WithAudit(em Emitter) *PostgresQuiesceController
WithAudit enables quiesce.set / quiesce.cleared audit emission. The audited principal is the operator who performed the action (Set's By / Clear's by), never inferred. Returns the controller for chaining.
func (*PostgresQuiesceController) WithClock ¶ added in v0.39.0
func (c *PostgresQuiesceController) WithClock(now func() time.Time) *PostgresQuiesceController
WithClock installs a deterministic clock (tests). Passing nil restores time.Now. Builder method — call before concurrent use. Returns the controller for chaining.
type QuiesceController ¶ added in v0.39.0
type QuiesceController interface {
QuiesceGate
// Set activates quiesce for opts.TTL (defaulted + ceiling-clamped).
// Idempotent: calling Set on an already-active flag renews ExpiresAt
// (a heartbeat for long deploys). Records By/Reason for visibility + audit.
Set(ctx context.Context, opts QuiesceSetOptions) (*QuiesceStatus, error)
// Clear deactivates the active flag (unconditional operator override; not
// keyed on the setter's session, so it works even if the setter died).
// Returns ErrQuiesceNotSet only when nothing has ever been set or the flag
// was already explicitly cleared (an expired-but-uncleared flag is still
// clearable).
Clear(ctx context.Context, by string) (*QuiesceStatus, error)
// CreateTable provisions backing storage idempotently (no-op for the
// in-memory controller).
CreateTable(ctx context.Context) error
}
QuiesceController is the write side used by the deployment job and the CLI. It composes QuiesceGate so a single implementation serves both reads and writes.
type QuiesceGate ¶ added in v0.39.0
type QuiesceGate interface {
// IsSet reports whether provisioning is currently quiesced. Returns false
// for an expired (auto-released) or cleared flag.
IsSet(ctx context.Context) (bool, error)
// Query returns the full status for operator visibility.
Query(ctx context.Context) (*QuiesceStatus, error)
}
QuiesceGate is the read side consumed by provisioning workers (provisioning.Executor) and the deployment fan-out (MigrateAll). Workers depend on this narrow interface (Interface Segregation); a nil gate means "never quiesced" so the feature is fully opt-in.
type QuiesceSetOptions ¶ added in v0.39.0
QuiesceSetOptions parameterizes Set. By is the principal (explicit, never inferred — ADR-019); empty surfaces the PrincipalUnspecified sentinel on the audit path. TTL of zero defaults to DefaultQuiesceTTL and is clamped to MaxQuiesceTTL.
type QuiesceStatus ¶ added in v0.39.0
type QuiesceStatus struct {
Active bool // cleared_at IS NULL AND now < expires_at
SetAt time.Time // when the active/last flag was set
SetBy string // principal that set it (visibility + audit)
Reason string // operator-supplied "why" (deploy id, ticket)
ExpiresAt time.Time // TTL hard stop
ClearedAt *time.Time // nil while uncleared; set when explicitly cleared
Expired bool // cleared_at IS NULL AND now >= expires_at (stale, auto-released)
}
QuiesceStatus is the operator-facing snapshot of the quiesce flag returned by Query. Active reports whether provisioning is currently paused; Expired reports that an uncleared flag has passed its TTL and was auto-released (read-side, no sweeper) — a distinct signal from a deliberately-cleared flag.
Active and Expired are mutually-exclusive conveniences derived from ClearedAt + ExpiresAt + the current time; the controller keeps them consistent. Treat the struct as read-only.
type Result ¶ added in v0.32.0
type Result struct {
// Operation is the Flyway verb. Empty on the error envelope.
Operation string
// Success is false whenever Flyway emitted an error envelope, even if
// the JSON parsed cleanly.
Success bool
// AppliedVersions enumerates the migration versions Flyway applied in
// this run, in the order Flyway reported them. Empty on no-op reruns.
AppliedVersions []string
// StartingVersion is the schema version before this run (Flyway's
// initialSchemaVersion). Empty when Flyway reported it as null —
// typically on the first migrate against a fresh schema.
StartingVersion string
// EndingVersion is the schema version after this run. Flyway reports
// targetSchemaVersion as null on no-op runs; the parser falls back to
// StartingVersion in that case so callers always see a usable terminus.
EndingVersion string
// DurationMillis is Flyway's totalMigrationTime in milliseconds.
DurationMillis int64
// FlywayVersion is the engine version that produced this result.
FlywayVersion string
// DatabaseType is Flyway's databaseType field ("PostgreSQL", "Oracle").
DatabaseType string
// ErrorCode is Flyway's errorCode on the failure envelope (e.g.
// "VALIDATE_ERROR" for a checksum mismatch). Empty when Success is true.
ErrorCode string
// ErrorMessage is the human-readable error message from Flyway when
// Success is false. May contain embedded newlines from Flyway.
ErrorMessage string
}
Result captures the structured outcome of a single Flyway migrate invocation, populated from the engine's -outputType=json output. Fields are best-effort: an empty Result is returned when the subprocess crashed before emitting JSON or the payload was malformed. Callers that need an authoritative pass/fail signal should still consult the error returned alongside the Result.
type SecretFetcher ¶ added in v0.31.0
SecretFetcher resolves an opaque secret name to its raw payload bytes. The framework stays decoupled from any specific cloud SDK; callers wire AWS Secrets Manager, HashiCorp Vault, or another store behind this seam.
type SecretsProvider ¶ added in v0.31.0
type SecretsProvider struct {
// Prefix is prepended to each tenant ID when composing the secret name.
// Empty defaults to DefaultSecretsPrefix at lookup time.
Prefix string
// NameFor, when non-nil, composes the secret name for a tenant instead
// of the default Prefix + tenantID. The tenantID it receives has already
// been trimmed and allowlist-validated. Use it for grammars that place
// segments after the tenant ID (e.g. "/env/platform/<id>/db"). The
// returned name is used as-is apart from a blank-name check — the
// composer owns its correctness. When NameFor is set, Prefix is ignored
// for lookups but a non-empty Prefix is still checked by Validate (a
// malformed Prefix alongside NameFor fails fast rather than lingering).
NameFor func(tenantID string) (string, error)
// Fetch resolves a secret name to its payload. Required.
Fetch SecretFetcher
// contains filtered or unexported fields
}
SecretsProvider implements database.DBConfigProvider on top of a SecretFetcher. It composes the secret name as Prefix + tenantID, fetches the bytes, and parses them as either the canonical go-bricks DatabaseConfig shape or the AWS-managed RDS rotation shape.
func (*SecretsProvider) DBConfig ¶ added in v0.31.0
func (p *SecretsProvider) DBConfig(ctx context.Context, tenantID string) (*config.DatabaseConfig, error)
DBConfig satisfies database.DBConfigProvider. Looks up the tenant's secret, parses the payload, and returns the resulting DatabaseConfig.
func (*SecretsProvider) SecretName ¶ added in v0.31.0
func (p *SecretsProvider) SecretName(tenantID string) string
SecretName composes the full secret name for the given tenant ID using the provider's prefix (or DefaultSecretsPrefix when unset). When NameFor is set, DBConfig uses it instead and this method reflects only the default Prefix + tenantID composition.
func (*SecretsProvider) Validate ¶ added in v0.31.0
func (p *SecretsProvider) Validate() error
Validate checks that the provider is wired correctly. Callers may invoke it eagerly at startup; DBConfig also calls it lazily on first lookup so library callers who skip the explicit check still get a clear error before any tenant fetch.
type TenantLister ¶ added in v0.31.0
TenantLister enumerates the tenant IDs that should receive migrations. Implementations include the HTTP source (for control-plane APIs) and a static source backed by config.TenantStore.
type TenantResult ¶ added in v0.31.0
type TenantResult struct {
TenantID string
Vendor string
Err error
Duration time.Duration
// Result is the parsed Flyway outcome for ActionMigrate. Zero-valued
// for Validate / Info, or when Flyway crashed before emitting JSON.
Result Result
}
TenantResult captures the outcome of running an Action against one tenant.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package provisioning implements a durable, crash-recoverable state machine for dynamic per-tenant provisioning under the multi-tenant migration model defined in issue #379.
|
Package provisioning implements a durable, crash-recoverable state machine for dynamic per-tenant provisioning under the multi-tenant migration model defined in issue #379. |
|
testing
Package testing provides test utilities for the provisioning state machine.
|
Package testing provides test utilities for the provisioning state machine. |
|
source
|
|
|
http
Package http provides a TenantLister that pulls tenant IDs from a control-plane API conforming to the go-bricks pre-defined contract.
|
Package http provides a TenantLister that pulls tenant IDs from a control-plane API conforming to the go-bricks pre-defined contract. |
|
static
Package static provides a TenantLister that enumerates tenant IDs from a config-backed source (typically the YAML-driven multitenant.tenants block).
|
Package static provides a TenantLister that enumerates tenant IDs from a config-backed source (typically the YAML-driven multitenant.tenants block). |