diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index fe29936c3..817880371 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -21,10 +21,15 @@ type Options struct { withSkipScopesCreation bool withSkipHostResourcesCreation bool withSkipTargetCreation bool + withContainerImage string + withDialect string } func getDefaultOptions() Options { - return Options{} + return Options{ + withContainerImage: "postgres", + withDialect: "postgres", + } } // WithNoTokenScope tells the client not to set a scope for the client from a @@ -83,3 +88,17 @@ func WithSkipTargetCreation() Option { o.withSkipTargetCreation = true } } + +// WithContainerImage tells the command which container image +// to start a dev database with +func WithContainerImage(name string) Option { + return func(o *Options) { + o.withContainerImage = name + } +} + +func withDialect(dialect string) Option { + return func(o *Options) { + o.withDialect = dialect + } +} diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index 239415feb..6cf5c12ee 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -462,16 +462,25 @@ func (b *Server) ConnectToDatabase(dialect string) error { return nil } -func (b *Server) CreateDevDatabase(ctx context.Context, dialect string, opt ...Option) error { - opts := getOpts(opt...) +func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error { - var container, url string + var container, url, dialect string var err error var c func() error + opts := getOpts(opt...) + + //We should only get back postgres for now, but laying the foundation for non-postgres + switch opts.withDialect { + case "": + b.Logger.Error("unsupported dialect. wanted: postgres, got: %v", opts.withDialect) + default: + dialect = opts.withDialect + } + switch b.DatabaseUrl { case "": - c, url, container, err = docker.StartDbInDocker(dialect) + c, url, container, err = docker.StartDbInDocker(dialect, docker.WithContainerImage(opts.withContainerImage)) // In case of an error, run the cleanup function. If we pass all errors, c should be set to a noop // function before returning from this method defer func() { @@ -502,7 +511,6 @@ func (b *Server) CreateDevDatabase(ctx context.Context, dialect string, opt ...O b.DevDatabaseCleanupFunc = c b.DatabaseUrl = url - default: // Let migrate store manage the dirty bit since dev DBs should be ephemeral anyways. if _, err := schema.MigrateStore(ctx, dialect, b.DatabaseUrl); err != nil { diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index d5365b9b1..95316d6c4 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -14,7 +14,6 @@ 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/docker" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" @@ -68,6 +67,7 @@ type Command struct { flagPassthroughDirectory string flagRecoveryKey string flagDatabaseUrl string + flagContainerImage string flagDisableDatabaseDestruction bool } @@ -247,6 +247,11 @@ func (c *Command) Flags() *base.FlagSets { Target: &c.flagDatabaseUrl, Usage: `If set, specifies the URL used to connect to the database for initialization (otherwise a Docker container will be started). This can refer to a file on disk (file://) from which a URL will be read; an env var (env://) from which the URL will be read; or a direct database URL.`, }) + f.StringVar(&base.StringVar{ + Name: "container-image", + Target: &c.flagContainerImage, + Usage: `Specifies a container image to be utilized. Must be in : format`, + }) return set } @@ -271,8 +276,6 @@ func (c *Command) Run(args []string) int { return base.CommandUserError } - // childShutdownCh := make(chan struct{}) - c.Config, err = config.DevCombined() if err != nil { c.UI.Error(fmt.Errorf("Error creating controller dev config: %w", err).Error()) @@ -433,20 +436,20 @@ func (c *Command) Run(args []string) int { } }() + var opts []base.Option switch c.flagDatabaseUrl { case "": - var opts []base.Option if c.flagDisableDatabaseDestruction { opts = append(opts, base.WithSkipDatabaseDestruction()) } - 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 base.CommandCliError - } - c.UI.Error(fmt.Errorf("Error creating dev database container: %w", err).Error()) + if c.flagContainerImage != "" { + opts = append(opts, base.WithContainerImage(c.flagContainerImage)) + } + if err := c.CreateDevDatabase(c.Context, opts...); err != nil { + c.UI.Error(fmt.Errorf("Error creating dev database container %w", err).Error()) return base.CommandCliError } + if !c.flagDisableDatabaseDestruction { c.ShutdownFuncs = append(c.ShutdownFuncs, c.DestroyDevDatabase) } @@ -456,7 +459,7 @@ func (c *Command) Run(args []string) int { c.UI.Error(fmt.Errorf("Error parsing database url: %w", err).Error()) return base.CommandUserError } - if err := c.CreateDevDatabase(c.Context, "postgres"); err != nil { + if err := c.CreateDevDatabase(c.Context, opts...); err != nil { c.UI.Error(fmt.Errorf("Error connecting to database: %w", err).Error()) return base.CommandCliError } diff --git a/internal/cmd/commands/server/server_test.go b/internal/cmd/commands/server/server_test.go index 500efde12..91372adad 100644 --- a/internal/cmd/commands/server/server_test.go +++ b/internal/cmd/commands/server/server_test.go @@ -36,7 +36,8 @@ func testServerCommand(t *testing.T, controllerKey string) *Command { require.NoError(err) require.NoError(cmd.SetupKMSes(cmd.UI, parsedKmsConfig)) - err = cmd.CreateDevDatabase(cmd.Context, "postgres") + opts := base.WithContainerImage("postgres") + err = cmd.CreateDevDatabase(cmd.Context, opts) if err != nil { if cmd.DevDatabaseCleanupFunc != nil { require.NoError(cmd.DevDatabaseCleanupFunc()) diff --git a/internal/db/schema/manager_test.go b/internal/db/schema/manager_test.go index 7337ccf08..3213bcb13 100644 --- a/internal/db/schema/manager_test.go +++ b/internal/db/schema/manager_test.go @@ -16,6 +16,7 @@ import ( func TestNewManager(t *testing.T) { dialect := "postgres" + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { @@ -37,6 +38,7 @@ func TestNewManager(t *testing.T) { func TestCurrentState(t *testing.T) { dialect := "postgres" + c, u, _, err := docker.StartDbInDocker(dialect) t.Cleanup(func() { if err := c(); err != nil { @@ -78,6 +80,7 @@ func TestCurrentState(t *testing.T) { func TestRollForward(t *testing.T) { dialect := "postgres" + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { @@ -140,6 +143,7 @@ func TestRollForward_NotFromFresh(t *testing.T) { func TestRunMigration_canceledContext(t *testing.T) { dialect := "postgres" + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { @@ -191,6 +195,7 @@ func TestRollForward_BadSQL(t *testing.T) { func TestManager_ExclusiveLock(t *testing.T) { ctx := context.Background() dialect := "postgres" + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { @@ -214,7 +219,9 @@ func TestManager_ExclusiveLock(t *testing.T) { func TestManager_SharedLock(t *testing.T) { ctx := context.Background() - c, u, _, err := docker.StartDbInDocker("postgres") + dialect := "postgres" + + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -241,7 +248,9 @@ func TestManager_SharedLock(t *testing.T) { func Test_GetMigrationLog(t *testing.T) { t.Parallel() ctx := context.Background() - c, u, _, err := docker.StartDbInDocker("postgres") + dialect := "postgres" + + c, u, _, err := docker.StartDbInDocker(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) diff --git a/internal/docker/docker.go b/internal/docker/docker.go index f6394377a..25c66d9f6 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -6,13 +6,13 @@ import ( ) var ( - StartDbInDocker func(string) (func() error, string, string, error) = startDbInDockerUnsupported + StartDbInDocker func(dialect string, opt ...Option) (func() error, string, string, error) = startDbInDockerUnsupported ErrDockerUnsupported = errors.New("docker is not currently supported on this platform") mx = sync.Mutex{} ) -func startDbInDockerUnsupported(dialect string) (cleanup func() error, retURL, container string, err error) { +func startDbInDockerUnsupported(dialect string, opt ...Option) (cleanup func() error, retURL, container string, err error) { return nil, "", "", ErrDockerUnsupported } diff --git a/internal/docker/option.go b/internal/docker/option.go new file mode 100644 index 000000000..35e4ed5f5 --- /dev/null +++ b/internal/docker/option.go @@ -0,0 +1,30 @@ +package docker + +// GetOpts - iterate the inbound Options and return a struct. +func GetOpts(opt ...Option) Options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. +type Option func(*Options) + +// Options - how Options are represented. +type Options struct { + withContainerImage string +} + +func getDefaultOptions() Options { + return Options{} +} + +// WithContainerImage tells the command which container image +// to start a dev database with +func WithContainerImage(image string) Option { + return func(o *Options) { + o.withContainerImage = image + } +} diff --git a/internal/docker/supported.go b/internal/docker/supported.go index 0c96d89bd..ca6fc98bf 100644 --- a/internal/docker/supported.go +++ b/internal/docker/supported.go @@ -15,7 +15,7 @@ func init() { StartDbInDocker = startDbInDockerSupported } -func startDbInDockerSupported(dialect string) (cleanup func() error, retURL, container string, err error) { +func startDbInDockerSupported(dialect string, opt ...Option) (cleanup func() error, retURL, container string, err error) { // TODO: Debug what part of this method is actually causing race condition issues with our test and fix. mx.Lock() defer mx.Unlock() @@ -25,15 +25,30 @@ func startDbInDockerSupported(dialect string) (cleanup func() error, retURL, con } var resource *dockertest.Resource - var url string + var url, tag, repository string + + opts := GetOpts(opt...) + if opts.withContainerImage != "" { + repository, tag, err = splitImage(opts) + if err != nil { + return func() error { return nil }, "", "", fmt.Errorf("error parsing reference: %w", err) + } + } + switch dialect { case "postgres": switch { case os.Getenv("BOUNDARY_TESTING_PG_URL") != "": url = os.Getenv("BOUNDARY_TESTING_PG_URL") return func() error { return nil }, url, "", nil + case repository != "": + resource, err = pool.Run(repository, tag, []string{"POSTGRES_PASSWORD=password", "POSTGRES_DB=boundary"}) + url = "postgres://postgres:password@localhost:%s?sslmode=disable" + if err == nil { + url = fmt.Sprintf("postgres://postgres:password@%s/boundary?sslmode=disable", resource.GetHostPort("5432/tcp")) + } default: - resource, err = pool.Run("postgres", "11", []string{"POSTGRES_PASSWORD=password", "POSTGRES_DB=boundary"}) + resource, err = pool.Run(dialect, tag, []string{"POSTGRES_PASSWORD=password", "POSTGRES_DB=boundary"}) url = "postgres://postgres:password@localhost:%s?sslmode=disable" if err == nil { url = fmt.Sprintf("postgres://postgres:password@%s/boundary?sslmode=disable", resource.GetHostPort("5432/tcp")) @@ -81,5 +96,30 @@ func cleanupDockerResource(pool *dockertest.Pool, resource *dockertest.Resource) if strings.Contains(err.Error(), "No such container") { return nil } - return fmt.Errorf("Failed to cleanup local container: %s", err) + return fmt.Errorf("failed to cleanup local container: %s", err) +} + +// splitImage takes the WithDatabaseImage option and separates +// it into repo + tag. If a tag is not found, it sets the tag to latest +func splitImage(opts Options) (string, string, error) { + + separated := strings.Split(opts.withContainerImage, ":") + separatedlen := len(separated) + + switch separatedlen { + case 1: + if separated[0] == "postgres" { + return separated[0], "11", nil + } + return "", "", fmt.Errorf("valid reference format is repo:tag, if"+ + " no tag provided then repo must be postgres, got: %s", opts.withContainerImage) + + case 2: + return separated[0], separated[1], nil + + default: + return "", "", fmt.Errorf("valid reference format is repo:tag, got: %s", opts.withContainerImage) + + } + } diff --git a/internal/oplog/testing.go b/internal/oplog/testing.go index d294a709d..054fec0e0 100644 --- a/internal/oplog/testing.go +++ b/internal/oplog/testing.go @@ -56,6 +56,7 @@ func testId(t *testing.T) string { func testInitDbInDocker(t *testing.T) (cleanup func() error, retURL string, err error) { t.Helper() + cleanup, retURL, _, err = docker.StartDbInDocker("postgres") if err != nil { t.Fatal(err) diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index a2cf5978e..ebbcd10f3 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -523,7 +523,7 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if opts.DisableAuthMethodCreation { createOpts = append(createOpts, base.WithSkipAuthMethodCreation()) } - if err := tc.b.CreateDevDatabase(ctx, "postgres", createOpts...); err != nil { + if err := tc.b.CreateDevDatabase(ctx, createOpts...); err != nil { t.Fatal(err) } }