2021-01-19 10:40:41 -07:00
package schema
import (
"bytes"
"context"
"database/sql"
"fmt"
"io"
2021-04-08 20:54:32 -04:00
"time"
2021-01-19 10:40:41 -07:00
"github.com/hashicorp/boundary/internal/db/schema/postgres"
"github.com/hashicorp/boundary/internal/errors"
2021-01-28 14:30:08 -07:00
"github.com/hashicorp/go-multierror"
2021-01-19 10:40:41 -07:00
)
// driver provides functionality to a database.
type driver interface {
TrySharedLock ( context . Context ) error
TryLock ( context . Context ) error
Lock ( context . Context ) error
Unlock ( context . Context ) error
UnlockShared ( context . Context ) error
2021-01-27 15:58:22 -07:00
// Either starts a transactioon internal to the driver or sets a dirty
// bit so if the Run fails the CurrentState reflects it.
StartRun ( context . Context ) error
// Either commits the transaction or clears the dirty bit.
CommitRun () error
// Performs the mutation on the driver. This should always be
// wrapped by StartRun and CommitRun. The driver must properly
// handle the transaction or dirty bit in case of error when
// executing Run.
Run ( context . Context , io . Reader , int ) error
// A version of -1 indicates no version is set.
2021-01-28 14:30:08 -07:00
CurrentState ( context . Context ) ( ver int , everRan bool , dirty bool , err error )
EnsureVersionTable ( ctx context . Context ) error
2021-01-19 10:40:41 -07:00
}
// Manager provides a way to run operations and retrieve information regarding
// the underlying boundary database schema.
// Manager is not thread safe.
type Manager struct {
2021-04-08 20:54:32 -04:00
db * sql . DB
driver driver
dialect string
migrationStates map [ string ] migrationState
2021-01-19 10:40:41 -07:00
}
// NewManager creates a new schema manager. An error is returned
// if the provided dialect is unrecognized or if the passed in db is unreachable.
2021-04-08 20:54:32 -04:00
func NewManager ( ctx context . Context , dialect string , db * sql . DB , opt ... Option ) ( * Manager , error ) {
2021-01-19 10:40:41 -07:00
const op = "schema.NewManager"
dbM := Manager { db : db , dialect : dialect }
2021-04-08 20:54:32 -04:00
opts := getOpts ( opt ... )
if opts . withMigrationStates != nil {
dbM . migrationStates = opts . withMigrationStates
} else {
// intentionally set it to the reference, so changes to the global var
// will be reflected in the manager instance
dbM . migrationStates = migrationStates
}
2021-01-19 10:40:41 -07:00
switch dialect {
case "postgres" :
var err error
dbM . driver , err = postgres . New ( ctx , db )
if err != nil {
return nil , errors . Wrap ( err , op )
}
default :
return nil , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "unknown dialect %q" , dialect ))
}
return & dbM , nil
}
// State contains information regarding the current state of a boundary database's schema.
type State struct {
2021-01-28 14:30:08 -07:00
// InitializationStarted indicates if the current database has been initialized previously.
2021-01-19 10:40:41 -07:00
InitializationStarted bool
// Dirty is set to true if the database failed in a previous migration/initialization.
Dirty bool
// DatabaseSchemaVersion is the schema version that is currently running in the database.
DatabaseSchemaVersion int
// BinarySchemaVersion is the schema version which this boundary binary supports.
BinarySchemaVersion int
}
// CurrentState provides the state of the boundary schema contained in the backing database.
func ( b * Manager ) CurrentState ( ctx context . Context ) ( * State , error ) {
2021-01-28 14:30:08 -07:00
const op = "schema.(Manager).CurrentState"
2021-01-19 10:40:41 -07:00
dbS := State {
BinarySchemaVersion : BinarySchemaVersion ( b . dialect ),
}
2021-01-28 14:30:08 -07:00
v , initialized , dirty , err := b . driver . CurrentState ( ctx )
2021-01-19 10:40:41 -07:00
if err != nil {
2021-01-28 14:30:08 -07:00
return nil , errors . Wrap ( err , op )
2021-01-19 10:40:41 -07:00
}
2021-01-28 14:30:08 -07:00
dbS . InitializationStarted = initialized
2021-01-25 14:43:50 -07:00
dbS . DatabaseSchemaVersion = v
dbS . Dirty = dirty
2021-01-19 10:40:41 -07:00
return & dbS , nil
}
2021-01-25 14:43:50 -07:00
// SharedLock attempts to obtain a shared lock on the database. This can fail
// if an exclusive lock is already held. If the lock can't be obtained an
// error is returned.
2021-01-19 10:40:41 -07:00
func ( b * Manager ) SharedLock ( ctx context . Context ) error {
const op = "schema.(Manager).SharedLock"
if err := b . driver . TrySharedLock ( ctx ); err != nil {
return errors . Wrap ( err , op )
}
return nil
}
// SharedUnlock releases a shared lock on the database. If this
// fails for whatever reason an error is returned. Unlocking a lock
// that is not held is not an error.
func ( b * Manager ) SharedUnlock ( ctx context . Context ) error {
const op = "schema.(Manager).SharedUnlock"
if err := b . driver . UnlockShared ( ctx ); err != nil {
return errors . Wrap ( err , op )
}
return nil
}
// ExclusiveLock attempts to obtain an exclusive lock on the database.
// An error is returned if a lock was unable to be obtained.
func ( b * Manager ) ExclusiveLock ( ctx context . Context ) error {
const op = "schema.(Manager).ExclusiveLock"
if err := b . driver . TryLock ( ctx ); err != nil {
return errors . Wrap ( err , op )
}
return nil
}
// ExclusiveUnlock releases a shared lock on the database. If this
// fails for whatever reason an error is returned. Unlocking a lock
// that is not held is not an error.
func ( b * Manager ) ExclusiveUnlock ( ctx context . Context ) error {
const op = "schema.(Manager).ExclusiveUnlock"
if err := b . driver . Unlock ( ctx ); err != nil {
return errors . Wrap ( err , op )
}
return nil
}
// RollForward updates the database schema to match the latest version known by
// the boundary binary. An error is not returned if the database is already at
// the most recent version.
func ( b * Manager ) RollForward ( ctx context . Context ) error {
const op = "schema.(Manager).RollForward"
// Capturing a lock that this session to the db already possesses is okay.
if err := b . driver . Lock ( ctx ); err != nil {
return errors . Wrap ( err , op )
}
defer func () {
2021-04-08 20:54:32 -04:00
if err := b . driver . Unlock ( ctx ); err != nil {
// I'm not sure this is ideal, but we have to rollback the current
// transaction if we're unable to release the lock
panic ( errors . Wrap ( err , op ))
}
2021-01-19 10:40:41 -07:00
}()
2021-01-28 14:30:08 -07:00
curVersion , _ , dirty , err := b . driver . CurrentState ( ctx )
2021-01-19 10:40:41 -07:00
if err != nil {
return errors . Wrap ( err , op )
}
if dirty {
return errors . New ( errors . NotSpecificIntegrity , op , fmt . Sprintf ( "schema is dirty with version %d" , curVersion ))
}
2021-04-08 20:54:32 -04:00
if err = b . runMigrations ( ctx , newStatementProvider ( b . dialect , curVersion , WithMigrationStates ( b . migrationStates ))); err != nil {
2021-01-28 14:30:08 -07:00
return errors . Wrap ( err , op )
}
return nil
}
type rollbacker interface {
Rollback () error
2021-01-19 10:40:41 -07:00
}
// runMigrations passes migration queries to a database driver and manages
2021-05-14 16:26:02 -04:00
// the version and dirty bit. Cancellation or deadline/timeout is managed
2021-01-19 10:40:41 -07:00
// through the passed in context.
func ( b * Manager ) runMigrations ( ctx context . Context , qp * statementProvider ) error {
const op = "schema.(Manager).runMigrations"
2021-01-27 15:58:22 -07:00
if err := b . driver . StartRun ( ctx ); err != nil {
2021-01-28 14:30:08 -07:00
return errors . Wrap ( err , op )
}
if err := b . driver . EnsureVersionTable ( ctx ); err != nil {
return errors . Wrap ( err , op )
2021-01-27 15:58:22 -07:00
}
2021-01-19 10:40:41 -07:00
for qp . Next () {
select {
case <- ctx . Done ():
2021-01-28 14:30:08 -07:00
err := ctx . Err ()
if d , ok := b . driver .( rollbacker ); ok {
if rbErr := d . Rollback (); rbErr != nil {
err = multierror . Append ( err , rbErr )
}
}
return errors . Wrap ( err , op )
2021-01-19 10:40:41 -07:00
default :
// context is not done yet. Continue on to the next query to execute.
}
2021-01-27 15:58:22 -07:00
if err := b . driver . Run ( ctx , bytes . NewReader ( qp . ReadUp ()), qp . Version ()); err != nil {
2021-01-19 10:40:41 -07:00
return errors . Wrap ( err , op )
}
}
2021-01-27 15:58:22 -07:00
if err := b . driver . CommitRun (); err != nil {
2021-01-28 14:30:08 -07:00
return errors . Wrap ( err , op )
2021-01-27 15:58:22 -07:00
}
2021-01-19 10:40:41 -07:00
return nil
}
2021-04-08 20:54:32 -04:00
// LogEntry represents a log entry generated during migrations.
type LogEntry struct {
Id int
MigrationVersion string
CreateTime time . Time
Entry string
}
// GetMigrationLog will retrieve the migration logs from the db for the last
// migration. Once it's read the entries, it will delete them from the database.
// The WithDeleteLog option is supported and will remove all log entries when provided.
func GetMigrationLog ( ctx context . Context , d * sql . DB , opt ... Option ) ([] LogEntry , error ) {
const op = "schema.GetMigrationLog"
const sql = "select id, create_time, migration_version, entry from log_migration where migration_version in (select max(version) from boundary_schema_version)"
if d == nil {
return nil , errors . New ( errors . InvalidParameter , op , "missing sql db" )
}
rows , err := d . QueryContext ( ctx , sql )
if err != nil {
return nil , errors . Wrap ( err , op )
}
defer rows . Close ()
var entries [] LogEntry
for rows . Next () {
var e LogEntry
if err := rows . Scan ( & e . Id , & e . CreateTime , & e . MigrationVersion , & e . Entry ); err != nil {
return nil , errors . Wrap ( err , op )
}
entries = append ( entries , e )
}
if rows . Err () != nil {
return nil , errors . Wrap ( err , op )
}
opts := getOpts ( opt ... )
if opts . withDeleteLog {
// this truncate could change to a delete if FKs are needed in the future
_ , err = d . ExecContext ( ctx , "truncate log_migration" )
if err != nil {
return nil , errors . Wrap ( err , op )
}
}
return entries , nil
}