Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organize DB Schema Migration Code and DB Init Checks #842

Merged
merged 34 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b4e7a9b
Create Schema Manager and propagate contexts into the DB calls downst…
talanknight Dec 17, 2020
add34f1
Add tests for schema manager library.
talanknight Dec 18, 2020
4e198d4
Check schema state before starting server and when running init.
talanknight Dec 21, 2020
1d38834
Fixing overflowed SchemaAccessLockid.
talanknight Dec 21, 2020
ce20fca
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Dec 21, 2020
3a05502
Reorganize the directory structure around schema migrations and manag…
talanknight Dec 22, 2020
890a9a1
Pull in some files from golang-migrations.
talanknight Jan 5, 2021
a595122
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 5, 2021
50ed3db
Add boundary domain errors and unexport functions that don't need to …
talanknight Jan 5, 2021
5039d42
Adding comments and and organizing tests for the schema directory.
talanknight Jan 5, 2021
8c8debc
Add test for migrating up from an existing initialized db.
talanknight Jan 5, 2021
a6994c1
Fixing error formatting error for the SharedLock and ExclusiveLock me…
talanknight Jan 5, 2021
1c1e0a2
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 5, 2021
0cb5c43
Init now checks for a previous bad initialization.
talanknight Jan 6, 2021
5f607c7
Adding tests for the new migration related error codes.
talanknight Jan 6, 2021
4c90aa6
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 6, 2021
a18f9b4
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 7, 2021
f5dbf8c
Schema manager now use's driver for db locking. Renamed and moved som…
talanknight Jan 11, 2021
85a95ce
Moved postgres interfacing specific logic to its own package and defi…
talanknight Jan 11, 2021
5acf2f9
Merge branch 'main' into toddknight-schema-management
talanknight Jan 11, 2021
cedc3a9
Fix int overflow for 386 platforms.
talanknight Jan 12, 2021
4d4e310
Adding tests and comments.
talanknight Jan 13, 2021
b6981cd
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 13, 2021
3243c92
Update internal/cmd/commands/database/init.go
talanknight Jan 14, 2021
790a1a1
Update internal/cmd/commands/database/init.go
talanknight Jan 14, 2021
c7ac37e
Adding context to server command.
talanknight Jan 14, 2021
8eb2c36
Set up `server` and `dev` commands to use a context created when the …
talanknight Jan 14, 2021
398cbe6
Passing context into CreateDevDatabase.
talanknight Jan 14, 2021
f8a450d
Passing context into CreateDevDatabase.
talanknight Jan 14, 2021
5722661
Fix comments and some variable naming.
talanknight Jan 15, 2021
793a428
stop exporting a number of test helper functions.
talanknight Jan 15, 2021
8af94f1
Explicitly release any locks the commands have captured.
talanknight Jan 19, 2021
7d0d784
Adding README to migrations directory.
talanknight Jan 19, 2021
a8f553c
Merge remote-tracking branch 'origin/main' into toddknight-schema-man…
talanknight Jan 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ perms-table:
gen: cleangen proto api migrations fmt

migrations:
$(MAKE) --environment-overrides -C internal/db/migrations/genmigrations migrations
$(MAKE) --environment-overrides -C internal/db/schema/migrations/generate migrations

### oplog requires protoc-gen-go v1.20.0 or later
# GO111MODULE=on go get -u github.com/golang/protobuf/[email protected]
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ replace github.com/hashicorp/boundary/sdk => ./sdk
require (
github.com/armon/go-metrics v0.3.5
github.com/bufbuild/buf v0.33.0
github.com/dhui/dktest v0.3.3
github.com/fatih/color v1.10.0
github.com/favadi/protoc-go-inject-tag v1.1.0
github.com/go-bindata/go-bindata/v3 v3.1.3
Expand Down
38 changes: 28 additions & 10 deletions internal/cmd/base/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/docker"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/types/scope"
Expand Down Expand Up @@ -442,7 +443,7 @@ func (b *Server) ConnectToDatabase(dialect string) error {
return nil
}

func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
func (b *Server) CreateDevDatabase(ctx context.Context, dialect string, opt ...Option) error {
opts := getOpts(opt...)

var container, url string
Expand Down Expand Up @@ -470,17 +471,25 @@ func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
return fmt.Errorf("unable to start dev database with dialect %s: %w", dialect, err)
}

_, err := db.InitStore(dialect, c, url)
_, err := schema.InitStore(ctx, dialect, url)
if err != nil {
return fmt.Errorf("unable to initialize dev database with dialect %s: %w", dialect, err)
err = fmt.Errorf("unable to initialize dev database with dialect %s: %w", dialect, err)
if c != nil {
err = multierror.Append(err, c())
}
return err
}

b.DevDatabaseCleanupFunc = c
b.DatabaseUrl = url

default:
if _, err := db.InitStore(dialect, c, b.DatabaseUrl); err != nil {
return fmt.Errorf("error initializing store: %w", err)
if _, err := schema.InitStore(ctx, dialect, b.DatabaseUrl); err != nil {
err = fmt.Errorf("error initializing store: %w", err)
if c != nil {
err = multierror.Append(err, c())
}
return err
}
}

Expand All @@ -492,16 +501,25 @@ func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
}

if err := b.ConnectToDatabase(dialect); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}

b.Database.LogMode(true)

if err := b.CreateGlobalKmsKeys(context.Background()); err != nil {
if err := b.CreateGlobalKmsKeys(ctx); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}

if _, err := b.CreateInitialLoginRole(context.Background()); err != nil {
if _, err := b.CreateInitialLoginRole(ctx); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}

Expand All @@ -512,7 +530,7 @@ func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
return nil
}

if _, _, err := b.CreateInitialAuthMethod(context.Background()); err != nil {
if _, _, err := b.CreateInitialAuthMethod(ctx); err != nil {
return err
}

Expand All @@ -523,7 +541,7 @@ func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
return nil
}

if _, _, err := b.CreateInitialScopes(context.Background()); err != nil {
if _, _, err := b.CreateInitialScopes(ctx); err != nil {
return err
}

Expand All @@ -545,7 +563,7 @@ func (b *Server) CreateDevDatabase(dialect string, opt ...Option) error {
return nil
}

if _, err := b.CreateInitialTarget(context.Background()); err != nil {
if _, err := b.CreateInitialTarget(ctx); err != nil {
return err
}

Expand Down
10 changes: 2 additions & 8 deletions internal/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,14 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Commands = map[string]cli.CommandFactory{
"server": func() (cli.Command, error) {
return &server.Command{
Server: base.NewServer(&base.Command{
UI: serverCmdUi,
ShutdownCh: base.MakeShutdownCh(),
}),
Server: base.NewServer(base.NewCommand(serverCmdUi)),
SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
}, nil
},
"dev": func() (cli.Command, error) {
return &dev.Command{
Server: base.NewServer(&base.Command{
UI: serverCmdUi,
ShutdownCh: base.MakeShutdownCh(),
}),
Server: base.NewServer(base.NewCommand(serverCmdUi)),
SighupCh: MakeSighupCh(),
SigUSR2Ch: MakeSigUSR2Ch(),
}, nil
Expand Down
81 changes: 55 additions & 26 deletions internal/cmd/commands/database/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import (

"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/migrations"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/sdk/wrapper"
wrapping "github.com/hashicorp/go-kms-wrapping"
Expand Down Expand Up @@ -186,8 +184,10 @@ func (c *InitCommand) Run(args []string) (retCode int) {
}()
}

if migrations.DevMigration != c.flagAllowDevMigrations {
if migrations.DevMigration {
dialect := "postgres"

if schema.DevMigration(dialect) != c.flagAllowDevMigrations {
if schema.DevMigration(dialect) {
c.UI.Error(base.WrapAtLength("This version of the binary has " +
"dev database schema updates which may not be supported in the " +
"next official release. To proceed anyways please use the " +
Expand Down Expand Up @@ -263,34 +263,63 @@ func (c *InitCommand) Run(args []string) (retCode int) {
return 1
}

migrationUrl, err := config.ParseAddress(migrationUrlToParse)
if err != nil && err != config.ErrNotAUrl {
c.UI.Error(fmt.Errorf("Error parsing migration url: %w", err).Error())
// This database is used to keep an exclusive lock on the database for the
// remainder of the command
dBase, err := sql.Open(dialect, dbaseUrl)
if err != nil {
c.UI.Error(fmt.Errorf("Error establishing db connection for locking: %w", err).Error())
return 1
}
man, err := schema.NewManager(c.Context, dialect, dBase)
if err != nil {
c.UI.Error(fmt.Errorf("Error setting up schema manager for locking: %w", err).Error())
return 1
}

// Core migrations using the migration URL
{
c.srv.DatabaseUrl = strings.TrimSpace(migrationUrl)
ldb, err := sql.Open("postgres", c.srv.DatabaseUrl)
st, err := man.CurrentState(c.Context)
if err != nil {
c.UI.Error(fmt.Errorf("Error opening database to check init status: %w", err).Error())
c.UI.Error(fmt.Errorf("Error getting database state: %w", err).Error())
return 1
}
_, err = ldb.QueryContext(c.Context, "select version from schema_migrations")
switch {
case err == nil:
if base.Format(c.UI) == "table" {
c.UI.Info("Database already initialized.")
return 0
}
case errors.IsMissingTableError(err):
// Doesn't exist so we continue on
default:
c.UI.Error(fmt.Errorf("Error querying database for init status: %w", err).Error())
if st.Dirty {
c.UI.Error(base.WrapAtLength("Database is in a bad initialization " +
"state. Please revert back to the last known good state."))
return 1
}
if st.InitializationStarted {
// TODO: Separate from the "dirty" bit maintained by the schema
// manager maintain a bit which indicates that this full command
// was completed successfully (with all default resources being created).
// Use that bit to determine if a previous init was completed
// successfully or not.
c.UI.Error(base.WrapAtLength("Database has already been " +
"initialized. If the initialization did not complete successfully " +
"please revert the database to its fresh state."))
return 1
}
ran, err := db.InitStore("postgres", nil, c.srv.DatabaseUrl)
}

// This is an advisory locks on the DB which is released when the db session ends.
if err := man.ExclusiveLock(c.Context); err != nil {
c.UI.Error(fmt.Errorf("Error capturing an exclusive lock: %w", err).Error())
return 1
}
defer func() {
if err := man.ExclusiveUnlock(c.Context); err != nil {
c.UI.Error(fmt.Errorf("Unable to release exclusive lock to the database: %w", err).Error())
}
}()

migrationUrl, err := config.ParseAddress(migrationUrlToParse)
if err != nil && err != config.ErrNotAUrl {
c.UI.Error(fmt.Errorf("Error parsing migration url: %w", err).Error())
return 1
}

// Core migrations using the migration URL
{
migrationUrl = strings.TrimSpace(migrationUrl)
ran, err := schema.InitStore(c.Context, dialect, migrationUrl)
if err != nil {
c.UI.Error(fmt.Errorf("Error running database migrations: %w", err).Error())
return 1
Expand All @@ -308,7 +337,7 @@ func (c *InitCommand) Run(args []string) (retCode int) {

// Everything after is done with normal database URL and is affecting actual data
c.srv.DatabaseUrl = strings.TrimSpace(dbaseUrl)
if err := c.srv.ConnectToDatabase("postgres"); err != nil {
if err := c.srv.ConnectToDatabase(dialect); err != nil {
c.UI.Error(fmt.Errorf("Error connecting to database after migrations: %w", err).Error())
return 1
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/commands/dev/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ func (c *Command) Run(args []string) int {
if c.flagDisableDatabaseDestruction {
opts = append(opts, base.WithSkipDatabaseDestruction())
}
if err := c.CreateDevDatabase("postgres", opts...); err != nil {
if err := c.CreateDevDatabase(c.Context, "postgres", opts...); err != nil {
if err == docker.ErrDockerUnsupported {
c.UI.Error("Automatically starting a Docker container running Postgres is not currently supported on this platform. Please use -database-url to pass in a URL (or an env var or file reference to a URL) for connecting to an existing empty database.")
return 1
Expand All @@ -417,7 +417,7 @@ func (c *Command) Run(args []string) int {
return 1
}
c.DatabaseUrl = strings.TrimSpace(dbaseUrl)
if err := c.CreateDevDatabase("postgres"); err != nil {
if err := c.CreateDevDatabase(c.Context, "postgres"); err != nil {
c.UI.Error(fmt.Errorf("Error connecting to database: %w", err).Error())
return 1
}
Expand Down
41 changes: 41 additions & 0 deletions internal/cmd/commands/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/servers/controller"
"github.com/hashicorp/boundary/internal/servers/worker"
"github.com/hashicorp/boundary/sdk/wrapper"
Expand Down Expand Up @@ -341,6 +342,46 @@ func (c *Command) Run(args []string) int {
c.UI.Error(fmt.Errorf("Error connecting to database: %w", err).Error())
return 1
}

sMan, err := schema.NewManager(c.Context, "postgres", c.Database.DB())
if err != nil {
c.UI.Error(fmt.Errorf("Can't get schema manager: %w.", err).Error())
return 1
}
// This is an advisory locks on the DB which is released when the db session ends.
if err := sMan.SharedLock(c.Context); err != nil {
c.UI.Error(fmt.Errorf("Unable to gain shared access to the database: %w", err).Error())
return 1
}
defer func() {
if err := sMan.SharedUnlock(c.Context); err != nil {
c.UI.Error(fmt.Errorf("Unable to release shared lock to the database: %w", err).Error())
}
}()
ckState, err := sMan.CurrentState(c.Context)
if err != nil {
c.UI.Error(fmt.Errorf("Error checking schema state: %w", err).Error())
return 1
}
if !ckState.InitializationStarted {
c.UI.Error("Database has not been initialized. Please run `boundary database init`.")
return 1
}
if ckState.Dirty {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, when would this happen, vs. the transaction containing the migration rolling back?

Copy link
Contributor Author

@talanknight talanknight Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently never rollback. If there is an error we stop where it failed and leave the dirty bit set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the transaction failing cause the changes from that migration to roll back? Isn't each migration its own transaction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, but that is only because the content of the migration issued the rollback and not because the migration library itself enforces it. This does get close to a topic I plan on trying to fix soon. When we fail after the the schema update has completed successfully but before all the default resources are created. IMO we should still mark the migration as being in a dirty state. Maybe the solution is pulling the dirty bit out to be managed through an interface in the schema manager so the caller can decide if the migration is dirty or not. I'm happy to talk more about this face to face or on slack.

c.UI.Error(base.WrapAtLength("Database is in a bad state. Please revert the database into the last known good state."))
return 1
}
if ckState.BinarySchemaVersion > ckState.DatabaseSchemaVersion {
// TODO: Add the command to migrate up the schema version once that command exists.
c.UI.Error("Older schema version is than is expected from this binary.")
return 1
}
if ckState.BinarySchemaVersion < ckState.DatabaseSchemaVersion {
c.UI.Error(base.WrapAtLength(fmt.Sprintf("Newer schema version (%d) "+
"than this binary expects. Please use a newer version of the boundary "+
"binary.", ckState.DatabaseSchemaVersion)))
return 1
}
}

defer func() {
Expand Down
Loading