diff --git a/.circleci/config.yml b/.circleci/config.yml index 781135b83..049f97ccb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ version: 2 jobs: test-sql-13-alpine: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202107-02 steps: - checkout - run: @@ -249,7 +249,7 @@ jobs: - .buildcache/packages/store test-sql-11-alpine: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202107-02 steps: - checkout - run: @@ -887,7 +887,7 @@ jobs: - .buildcache/packages/store test-sql-12-alpine: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202107-02 steps: - checkout - run: @@ -969,14 +969,26 @@ jobs: - .buildcache/packages/store build: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202107-02 + resource_class: large steps: - checkout + - run: + command: | + which pg_isready || sudo apt-get update && sudo apt-get install -y postgresql-client + make DOCKER_ARGS='-d' -C testing/dbtest/docker database-up + until pg_isready -h 127.0.0.1; do docker container inspect boundary-sql-tests &> /dev/null || exit -1; sleep 1; done + name: Initialize Test Database - run: command: | make test-ci name: Run Acceptance Tests no_output_timeout: 15m + - run: + command: | + make -C testing/dbtest/docker clean + name: Cleanup + when: always - run: command: | echo 'export SLACK_BUILD_STATUS="fail"' >> $BASH_ENV @@ -1467,7 +1479,7 @@ jobs: - .buildcache/packages/store test-sql-latest: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202107-02 steps: - checkout - run: diff --git a/.circleci/config/jobs/build.yml b/.circleci/config/jobs/build.yml index 2289ec8c1..87a76947a 100644 --- a/.circleci/config/jobs/build.yml +++ b/.circleci/config/jobs/build.yml @@ -1,13 +1,25 @@ +resource_class: large machine: - image: 'ubuntu-1604:201903-01' + image: 'ubuntu-2004:202107-02' working_directory: ~/boundary steps: - checkout +- run: + name: "Initialize Test Database" + command: | + which pg_isready || sudo apt-get update && sudo apt-get install -y postgresql-client + make DOCKER_ARGS='-d' -C testing/dbtest/docker database-up + until pg_isready -h 127.0.0.1; do docker container inspect boundary-sql-tests &> /dev/null || exit -1; sleep 1; done - run: name: "Run Acceptance Tests" no_output_timeout: 15m command: | make test-ci +- run: + name: "Cleanup" + when: always + command: | + make -C testing/dbtest/docker clean - slack/status: fail_only: true only_for_branches: master diff --git a/.circleci/config/jobs/test-sql.yml b/.circleci/config/jobs/test-sql.yml index c651d2d5e..f82933aa1 100644 --- a/.circleci/config/jobs/test-sql.yml +++ b/.circleci/config/jobs/test-sql.yml @@ -1,5 +1,5 @@ machine: - image: 'ubuntu-1604:201903-01' + image: 'ubuntu-2004:202107-02' working_directory: ~/boundary parameters: postgres-version: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b06d3bfb8..6cd3dfcd6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,40 @@ Any bug fixes fall into this section. ## Testing -To run the entire test suite run this command in the root of the project: +Most tests require a postgres database instance to successfully run. +This is provided via docker +by running a customized postgres image that is optimized for boundary tests. + +Before running the test suite, this docker container must be started: + +``` +$ make test-database-up +``` + +This can take a few seconds to initialize as it will +create a template database with the boundary migrations. +The progress can be checked b running: + +``` +$ docker logs -f boundary-sql-tests +``` + +Once a log line like the following is seen, the container is ready for running +tests: + +``` +PostgreSQL init process complete; ready for start up. +``` + +Alternatively if the `pg_isready` command is installed, it can be used to +determine if the container is ready, i.e.: + +``` +$ until pg_isready -h 127.0.0.1; do sleep 1; done +``` + +To run the entire test suite run this command in the root of the project +once the test database is ready: ``` $ make test @@ -88,3 +121,65 @@ run: ``` $ go test -run TestAuthTokenAuthenticator -v ./internal/auth ``` + +### Stopping the test database container + +The test database container can be shutdown using: + +``` +$ make test-database-down +``` + +Note that the container does *not* need to be shutdown between each run of +`make test` or `go test`. + +### Test database container options + +By default the container uses the host port of 5432. +This can changed using an environment variable: + +``` +$ export TEST_DB_PORT=5433 +$ make test-database-up +$ make test +``` + +The default docker image is built using the `postgres:11` base image. +The image can be changed using a make option to test against other versions: + +``` +$ make IMAGE_TAG=docker.io/hashicorpboundary/postgres:12-alpine test-database-up +$ make IMAGE_TAG=docker.io/hashicorpboundary/postgres:13-alpine test-database-up +$ make IMAGE_TAG=docker.io/hashicorpboundary/postgres:alpine test-database-up +``` + +Additional options can be passed to postgres to customize and override the +configuration in the config file of the docker image. +See the troubleshooting section below for more details. + +### Troubleshooting test database container + +The postgres configuration file included in the image +is optimized to support running the full test suite in parallel in CI. +As such, there may be issues starting the container locally, +especially in cases where the container has less then 4GB of memory. + +This is likely the case if the output of `docker logs boundary-sql-tests` shows: + +``` +pg_ctl: could not start server +``` + +In this case adjust the +[max_connections](https://www.postgresql.org/docs/11/runtime-config-connection.html#GUC-MAX-CONNECTIONS) +and/or +[shared_buffers](https://www.postgresql.org/docs/11/runtime-config-resource.html#GUC-SHARED-BUFFERS): + +``` +make PG_OPTS="-c max_connections=1000" test-database-up +``` + +Note that if `max_connections` is set too low, it may result in sporadic test +failures if a connection cannot be established. In this case, reduce the number +of concurrent tests via `GOMAXPROCS` or selectively run tests. + diff --git a/Makefile b/Makefile index b0bc24aef..9a5e4c2e4 100644 --- a/Makefile +++ b/Makefile @@ -157,14 +157,20 @@ website-install: website-start: @npm start --prefix website/ +test-database-up: + make -C testing/dbtest/docker database-up + +test-database-down: + make -C testing/dbtest/docker clean + test-ci: install-go ~/.go/bin/go test ./... -v $(TESTARGS) -timeout 120m test-sql: $(MAKE) -C internal/db/sqltest/ test -test: - ~/.go/bin/go test ./... -timeout 30m +test: + go test ./... -timeout 30m install-go: ./ci/goinstall.sh @@ -207,7 +213,7 @@ docker-publish: docker push $(IMAGE_TAG) docker push hashicorp/boundary:latest -.PHONY: api cli tools gen migrations proto website ci-config ci-verify set-ui-version docker docker-build docker-build-dev docker-publish +.PHONY: api cli tools gen migrations proto website ci-config ci-verify set-ui-version docker docker-build docker-build-dev docker-publish test-database-up test-database-down .NOTPARALLEL: diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index fa7d7e4f9..90db26906 100644 --- a/internal/cmd/base/dev.go +++ b/internal/cmd/base/dev.go @@ -10,13 +10,14 @@ import ( "strings" "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/cmd/base/internal/docker" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/testing/dbtest" capoidc "github.com/hashicorp/cap/oidc" "github.com/hashicorp/go-multierror" ) @@ -39,7 +40,11 @@ func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error { switch b.DatabaseUrl { case "": - c, url, container, err = docker.StartDbInDocker(dialect, docker.WithContainerImage(opts.withContainerImage)) + if opts.withDatabaseTemplate != "" { + c, url, _, err = dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(opts.withDatabaseTemplate)) + } else { + 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() { diff --git a/internal/docker/docker.go b/internal/cmd/base/internal/docker/docker.go similarity index 100% rename from internal/docker/docker.go rename to internal/cmd/base/internal/docker/docker.go diff --git a/internal/docker/option.go b/internal/cmd/base/internal/docker/option.go similarity index 100% rename from internal/docker/option.go rename to internal/cmd/base/internal/docker/option.go diff --git a/internal/docker/supported.go b/internal/cmd/base/internal/docker/supported.go similarity index 100% rename from internal/docker/supported.go rename to internal/cmd/base/internal/docker/supported.go diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index d83119f53..f528d6e28 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -28,6 +28,7 @@ type Options struct { withSkipTargetCreation bool withContainerImage string withDialect string + withDatabaseTemplate string withEventerConfig *event.EventerConfig withEventFlags *EventFlags withAttributeFieldPrefix string @@ -149,3 +150,11 @@ func WithStatusCode(statusCode int) Option { o.withStatusCode = statusCode } } + +// WithDatabaseTemplate allows for using an existing database template for +// initializing the boundary database. +func WithDatabaseTemplate(template string) Option { + return func(o *Options) { + o.withDatabaseTemplate = template + } +} diff --git a/internal/cmd/commands/database/funcs_test.go b/internal/cmd/commands/database/funcs_test.go index 2ff72110f..fe41b4656 100644 --- a/internal/cmd/commands/database/funcs_test.go +++ b/internal/cmd/commands/database/funcs_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/hashicorp/boundary/internal/cmd/base" - "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/mitchellh/cli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,7 +15,7 @@ import ( func TestMigrateDatabase(t *testing.T) { ctx := context.Background() - dialect := "postgres" + dialect := dbtest.Postgres cases := []struct { name string @@ -29,7 +29,7 @@ func TestMigrateDatabase(t *testing.T) { name: "not_initialized_expected_not_intialized", initialized: false, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -43,7 +43,7 @@ func TestMigrateDatabase(t *testing.T) { name: "basic_initialized_expects_initialized", initialized: true, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -69,7 +69,7 @@ func TestMigrateDatabase(t *testing.T) { name: "basic_initialized_expects_not_initialized", initialized: false, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -95,7 +95,7 @@ func TestMigrateDatabase(t *testing.T) { name: "old_version_table_used_intialized", initialized: true, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -115,7 +115,7 @@ func TestMigrateDatabase(t *testing.T) { name: "old_version_table_used_not_intialized", initialized: false, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -149,7 +149,7 @@ func TestMigrateDatabase(t *testing.T) { name: "cant_get_lock_initialized", initialized: true, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -172,7 +172,7 @@ func TestMigrateDatabase(t *testing.T) { name: "cant_get_lock_not_initialized", initialized: false, urlProvider: func() string { - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -210,7 +210,7 @@ func TestVerifyOplogIsEmpty(t *testing.T) { dialect := "postgres" ctx := context.Background() - c, u, _, err := db.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) diff --git a/internal/cmd/commands/server/server_test.go b/internal/cmd/commands/server/server_test.go index 0b4f81ce7..2c51f3890 100644 --- a/internal/cmd/commands/server/server_test.go +++ b/internal/cmd/commands/server/server_test.go @@ -67,7 +67,7 @@ func testServerCommand(t *testing.T, opts testServerCommandOpts) *Command { cmd.Server.DevTargetId = defaultTestTargetId } - err = cmd.CreateDevDatabase(cmd.Context, base.WithContainerImage("postgres"), base.WithSkipOidcAuthMethodCreation()) + err = cmd.CreateDevDatabase(cmd.Context, base.WithDatabaseTemplate("boundary_template"), base.WithSkipOidcAuthMethodCreation()) if err != nil { if cmd.DevDatabaseCleanupFunc != nil { require.NoError(cmd.DevDatabaseCleanupFunc()) diff --git a/internal/db/db.go b/internal/db/db.go index d3fd54b57..f506c9845 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -7,7 +7,6 @@ import ( "math" "time" - "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-hclog" "github.com/jinzhu/gorm" @@ -23,8 +22,6 @@ func init() { pq.EnableInfinityTs(NegativeInfinityTS, PositiveInfinityTS) } -var StartDbInDocker = docker.StartDbInDocker - type DbType int const ( diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 75fc50758..7329ceb17 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -3,12 +3,13 @@ package db import ( "testing" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/lib/pq" "github.com/stretchr/testify/assert" ) func TestOpen(t *testing.T) { - cleanup, url, _, err := StartDbInDocker("postgres") + cleanup, url, _, err := dbtest.StartUsingTemplate(dbtest.Postgres) if err != nil { t.Fatal(err) } diff --git a/internal/db/schema/manager_test.go b/internal/db/schema/manager_test.go index 3213bcb13..84b522d56 100644 --- a/internal/db/schema/manager_test.go +++ b/internal/db/schema/manager_test.go @@ -8,16 +8,16 @@ import ( "testing" "github.com/hashicorp/boundary/internal/db/schema/postgres" - "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewManager(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -37,18 +37,15 @@ func TestNewManager(t *testing.T) { } func TestCurrentState(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) t.Cleanup(func() { if err := c(); err != nil { t.Fatalf("Got error at cleanup: %v", err) } }) require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, c()) - }) ctx := context.Background() d, err := sql.Open(dialect, u) require.NoError(t, err) @@ -79,9 +76,9 @@ func TestCurrentState(t *testing.T) { } func TestRollForward(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -104,13 +101,13 @@ func TestRollForward(t *testing.T) { } func TestRollForward_NotFromFresh(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres oState := migrationStates[dialect] nState := createPartialMigrationState(oState, 8) migrationStates[dialect] = nState - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -142,9 +139,9 @@ func TestRollForward_NotFromFresh(t *testing.T) { } func TestRunMigration_canceledContext(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -163,7 +160,7 @@ func TestRunMigration_canceledContext(t *testing.T) { } func TestRollForward_BadSQL(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres oState := migrationStates[dialect] defer func() { migrationStates[dialect] = oState }() @@ -172,7 +169,7 @@ func TestRollForward_BadSQL(t *testing.T) { nState.upMigrations[10] = []byte("SELECT 1 FROM NonExistantTable;") migrationStates[dialect] = nState - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -194,9 +191,9 @@ func TestRollForward_BadSQL(t *testing.T) { func TestManager_ExclusiveLock(t *testing.T) { ctx := context.Background() - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -219,9 +216,9 @@ func TestManager_ExclusiveLock(t *testing.T) { func TestManager_SharedLock(t *testing.T) { ctx := context.Background() - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -248,9 +245,9 @@ func TestManager_SharedLock(t *testing.T) { func Test_GetMigrationLog(t *testing.T) { t.Parallel() ctx := context.Background() - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) diff --git a/internal/db/schema/migrations/postgres/11/01_server_type_enum_test.go b/internal/db/schema/migrations/postgres/11/01_server_type_enum_test.go index bd7706841..6d8779519 100644 --- a/internal/db/schema/migrations/postgres/11/01_server_type_enum_test.go +++ b/internal/db/schema/migrations/postgres/11/01_server_type_enum_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/docker" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/stretchr/testify/require" ) @@ -40,10 +40,10 @@ func Test_ServerEnumChanges(t *testing.T) { require := require.New(t) const priorMigration = 10007 const serverEnumMigration = 11001 - dialect := "postgres" + dialect := dbtest.Postgres ctx := context.Background() - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(err) t.Cleanup(func() { require.NoError(c()) diff --git a/internal/db/schema/migrations/postgres/12/01_timestamp_sub_funcs_test.go b/internal/db/schema/migrations/postgres/12/01_timestamp_sub_funcs_test.go index 3eda4f90f..eb9fac8d7 100644 --- a/internal/db/schema/migrations/postgres/12/01_timestamp_sub_funcs_test.go +++ b/internal/db/schema/migrations/postgres/12/01_timestamp_sub_funcs_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/docker" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/stretchr/testify/require" ) @@ -34,9 +34,9 @@ func testSetupDb(ctx context.Context, t *testing.T) *sql.DB { t.Helper() require := require.New(t) - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(err) t.Cleanup(func() { require.NoError(c()) diff --git a/internal/db/schema/migrations/postgres/14/warehouse_user_dim_test.go b/internal/db/schema/migrations/postgres/14/warehouse_user_dim_test.go index cb7410b49..1b40812f2 100644 --- a/internal/db/schema/migrations/postgres/14/warehouse_user_dim_test.go +++ b/internal/db/schema/migrations/postgres/14/warehouse_user_dim_test.go @@ -9,12 +9,12 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" - "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" "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/testing/dbtest" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" @@ -30,9 +30,9 @@ func TestMigrations_UserDimension(t *testing.T) { t.Parallel() assert, require := assert.New(t), require.New(t) ctx := context.Background() - dialect := "postgres" + dialect := dbtest.Postgres - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(err) t.Cleanup(func() { require.NoError(c()) diff --git a/internal/db/schema/migrations/postgres/2/07_iam_test.go b/internal/db/schema/migrations/postgres/2/07_iam_test.go index 97b1dbb69..e24549b33 100644 --- a/internal/db/schema/migrations/postgres/2/07_iam_test.go +++ b/internal/db/schema/migrations/postgres/2/07_iam_test.go @@ -8,8 +8,8 @@ import ( "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,10 +30,10 @@ func Test_PrimaryAuthMethodChanges(t *testing.T) { // // 4) asserting some bits about the state of the db. assert, require := assert.New(t), require.New(t) - dialect := "postgres" + dialect := dbtest.Postgres ctx := context.Background() - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(err) t.Cleanup(func() { require.NoError(c()) diff --git a/internal/db/schema/migrations/postgres/2/10_auth_test.go b/internal/db/schema/migrations/postgres/2/10_auth_test.go index 8d7fc74b4..9fe69827b 100644 --- a/internal/db/schema/migrations/postgres/2/10_auth_test.go +++ b/internal/db/schema/migrations/postgres/2/10_auth_test.go @@ -18,7 +18,7 @@ func Test_AuthMethodSubtypes(t *testing.T) { t.Parallel() assert, require := assert.New(t), require.New(t) ctx := context.Background() - conn, _ := db.TestSetup(t, "postgres") + conn, _ := db.TestSetup(t, "postgres", db.WithTemplate("template1")) rw := db.New(conn) rootWrapper := db.TestWrapper(t) kmsCache := kms.TestKms(t, conn, rootWrapper) diff --git a/internal/db/schema/migrations/postgres/8/08_connection_test.go b/internal/db/schema/migrations/postgres/8/08_connection_test.go index bdbb09324..35367ce9f 100644 --- a/internal/db/schema/migrations/postgres/8/08_connection_test.go +++ b/internal/db/schema/migrations/postgres/8/08_connection_test.go @@ -38,7 +38,7 @@ func TestMigration(t *testing.T) { selectQuery = `select session_id, server_id from session_connection_testing order by session_id` ) - conn, _ := db.TestSetup(t, "postgres") + conn, _ := db.TestSetup(t, "postgres", db.WithTemplate("template1")) db := conn.DB() _, err := db.Exec(createTables) require.NoError(err) diff --git a/internal/db/schema/postgres/postgres_test.go b/internal/db/schema/postgres/postgres_test.go index b56c8010f..2b16c7317 100644 --- a/internal/db/schema/postgres/postgres_test.go +++ b/internal/db/schema/postgres/postgres_test.go @@ -33,490 +33,352 @@ package postgres import ( "bytes" "context" - "database/sql" - sqldriver "database/sql/driver" "fmt" - "io" - "log" "strconv" "strings" "testing" - "github.com/dhui/dktest" - "github.com/golang-migrate/migrate/v4/dktesting" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const ( - pgPassword = "postgres" -) - -var ( - opts = dktest.Options{ - Env: map[string]string{"POSTGRES_PASSWORD": pgPassword}, - PortRequired: true, ReadyFunc: isReady, - } - // Supported versions: https://www.postgresql.org/support/versioning/ - specs = []dktesting.ContainerSpec{ - {ImageName: "postgres:12", Options: opts}, - } -) - -func pgConnectionString(host, port string) string { - return fmt.Sprintf("postgres://postgres:%s@%s:%s/postgres?sslmode=disable", pgPassword, host, port) -} - -func isReady(ctx context.Context, c dktest.ContainerInfo) bool { - ip, port, err := c.FirstPort() - if err != nil { - return false - } - - db, err := sql.Open("postgres", pgConnectionString(ip, port)) - if err != nil { - return false - } - defer func() { - if err := db.Close(); err != nil { - log.Println("close error:", err) - } - }() - if err = db.PingContext(ctx); err != nil { - switch err { - case sqldriver.ErrBadConn, io.EOF: - return false - default: - log.Println(err) - } - return false - } - - return true -} - func TestDbStuff(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := d.close(t); err != nil { - t.Error(err) - } - }() - test(t, d, []byte("SELECT 1")) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + test(t, d, []byte("SELECT 1")) } func TestCurrentState_NoVersionTable(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := d.close(t); err != nil { - t.Error(err) - } - }() - // Drop the version table so calls to CurrentState don't rely on that - require.NoError(t, d.drop(ctx)) - - v, alreadyRan, dirt, err := d.CurrentState(ctx) - assert.NoError(t, err) - assert.False(t, alreadyRan) - assert.Equal(t, v, nilVersion) - assert.False(t, dirt) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + // Drop the version table so calls to CurrentState don't rely on that + require.NoError(t, d.drop(ctx)) + v, alreadyRan, dirt, err := d.CurrentState(ctx) + assert.NoError(t, err) + assert.False(t, alreadyRan) + assert.Equal(t, v, nilVersion) + assert.False(t, dirt) } func TestCurrentState_ToManyTables(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := d.close(t); err != nil { - t.Error(err) - } - }() - - // Create the most recent table - require.NoError(t, d.EnsureVersionTable(ctx)) - - // Create the legacy version of the table. - oldTableCreate := `create table if not exists schema_migrations (version bigint primary key, dirty boolean not null)` - _, err = d.conn.ExecContext(ctx, oldTableCreate) - require.NoError(t, err) - v, alreadyRan, dirt, err := d.CurrentState(ctx) - assert.Error(t, err) - assert.True(t, alreadyRan) - assert.Equal(t, v, nilVersion) - assert.False(t, dirt) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + // Create the most recent table + require.NoError(t, d.EnsureVersionTable(ctx)) + + // Create the legacy version of the table. + oldTableCreate := `create table if not exists schema_migrations (version bigint primary key, dirty boolean not null)` + _, err = d.conn.ExecContext(ctx, oldTableCreate) + require.NoError(t, err) + v, alreadyRan, dirt, err := d.CurrentState(ctx) + assert.Error(t, err) + assert.True(t, alreadyRan) + assert.Equal(t, v, nilVersion) + assert.False(t, dirt) } func TestMultiStatement(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := d.close(t); err != nil { - t.Error(err) - } - }() - if err := d.EnsureVersionTable(ctx); err != nil { - t.Fatalf("expected err to be nil, got %v", err) - } - - if err := d.Run(ctx, strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);"), 2); err != nil { - t.Fatalf("expected err to be nil, got %v", err) - } - - // make sure second table exists - var exists bool - if err := d.conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { - t.Fatal(err) - } - if !exists { - t.Fatalf("expected table bar to exist") - } + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + if err := d.EnsureVersionTable(ctx); err != nil { + t.Fatalf("expected err to be nil, got %v", err) + } + + if err := d.Run(ctx, strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);"), 2); err != nil { + t.Fatalf("expected err to be nil, got %v", err) + } + + // make sure second table exists + var exists bool + if err := d.conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { + t.Fatal(err) + } + if !exists { + t.Fatalf("expected table bar to exist") + } } func TestTransaction(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := d.close(t); err != nil { - t.Error(err) - } - }() - - v, alreadyRan, dirty, err := d.CurrentState(ctx) - assert.NoError(t, err) - assert.False(t, alreadyRan) - assert.False(t, dirty) - assert.Equal(t, -1, v) - - // Fail the initial setup of the db. - assert.NoError(t, d.StartRun(ctx)) - assert.NoError(t, d.EnsureVersionTable(ctx)) - assert.Error(t, d.Run(ctx, strings.NewReader("SELECT 1 from nonExistantTable"), 3)) - assert.Error(t, d.CommitRun()) - - v, alreadyRan, dirty, err = d.CurrentState(ctx) - assert.NoError(t, err) - assert.False(t, alreadyRan) - assert.False(t, dirty) - assert.Equal(t, -1, v) - - assert.NoError(t, d.StartRun(ctx)) - assert.NoError(t, d.EnsureVersionTable(ctx)) - assert.NoError(t, d.Run(ctx, strings.NewReader("CREATE TABLE foo (foo text);"), 2)) - assert.NoError(t, d.Run(ctx, strings.NewReader("SELECT 1;"), 3)) - assert.NoError(t, d.CommitRun()) - - v, alreadyRan, dirty, err = d.CurrentState(ctx) - assert.NoError(t, err) - assert.True(t, alreadyRan) - assert.False(t, dirty) - assert.Equal(t, 3, v) - - assert.NoError(t, d.StartRun(ctx)) - assert.NoError(t, d.Run(ctx, strings.NewReader("CREATE TABLE bar (bar text);"), 20)) - assert.Error(t, d.Run(ctx, strings.NewReader("SELECT 1 FROM NonExistingTable"), 30)) - assert.Error(t, d.CommitRun()) - - v, alreadyRan, dirty, err = d.CurrentState(ctx) - assert.NoError(t, err) - assert.True(t, alreadyRan) - assert.False(t, dirty) - assert.Equal(t, 3, v) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + v, alreadyRan, dirty, err := d.CurrentState(ctx) + assert.NoError(t, err) + assert.False(t, alreadyRan) + assert.False(t, dirty) + assert.Equal(t, -1, v) + + // Fail the initial setup of the db. + assert.NoError(t, d.StartRun(ctx)) + assert.NoError(t, d.EnsureVersionTable(ctx)) + assert.Error(t, d.Run(ctx, strings.NewReader("SELECT 1 from nonExistantTable"), 3)) + assert.Error(t, d.CommitRun()) + + v, alreadyRan, dirty, err = d.CurrentState(ctx) + assert.NoError(t, err) + assert.False(t, alreadyRan) + assert.False(t, dirty) + assert.Equal(t, -1, v) + + assert.NoError(t, d.StartRun(ctx)) + assert.NoError(t, d.EnsureVersionTable(ctx)) + assert.NoError(t, d.Run(ctx, strings.NewReader("CREATE TABLE foo (foo text);"), 2)) + assert.NoError(t, d.Run(ctx, strings.NewReader("SELECT 1;"), 3)) + assert.NoError(t, d.CommitRun()) + + v, alreadyRan, dirty, err = d.CurrentState(ctx) + assert.NoError(t, err) + assert.True(t, alreadyRan) + assert.False(t, dirty) + assert.Equal(t, 3, v) + + assert.NoError(t, d.StartRun(ctx)) + assert.NoError(t, d.Run(ctx, strings.NewReader("CREATE TABLE bar (bar text);"), 20)) + assert.Error(t, d.Run(ctx, strings.NewReader("SELECT 1 FROM NonExistingTable"), 30)) + assert.Error(t, d.CommitRun()) + + v, alreadyRan, dirty, err = d.CurrentState(ctx) + assert.NoError(t, err) + assert.True(t, alreadyRan) + assert.False(t, dirty) + assert.Equal(t, 3, v) } func TestWithSchema(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - require.NoError(t, err) - - addr := pgConnectionString(ip, port) - d, err := open(t, ctx, addr) - require.NoError(t, err) - defer func() { - if err := d.close(t); err != nil { - t.Fatal(err) - } - }() - require.NoError(t, d.EnsureVersionTable(ctx)) - - // create foobar schema - require.NoError(t, d.Run(ctx, strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres"), 1)) - - // re-connect using that schema - d2, err := open(t, ctx, fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&search_path=foobar", - pgPassword, ip, port)) - require.NoError(t, err) - defer func() { - if err := d2.close(t); err != nil { - t.Fatal(err) - } - }() - - version, alreadyRan, _, err := d2.CurrentState(ctx) - require.NoError(t, err) - require.Equal(t, nilVersion, version) - assert.False(t, alreadyRan) - - // now update CurrentState and compare - require.NoError(t, d2.EnsureVersionTable(ctx)) - require.NoError(t, d2.setVersion(ctx, 2, false)) - version, alreadyRan, _, err = d2.CurrentState(ctx) - require.NoError(t, err) - require.Equal(t, 2, version) - assert.True(t, alreadyRan) - - // meanwhile, the public schema still has the other CurrentState - version, alreadyRan, _, err = d.CurrentState(ctx) - require.NoError(t, err) - require.Equal(t, 1, version) - assert.True(t, alreadyRan) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + require.NoError(t, d.EnsureVersionTable(ctx)) + + // create foobar schema + require.NoError(t, d.Run(ctx, strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres"), 1)) + + // re-connect using that schema + d2, err := open(t, ctx, fmt.Sprintf("%s&search_path=foobar", u)) + require.NoError(t, err) + defer func() { + if err := d2.close(t); err != nil { + t.Fatal(err) + } + }() + + version, alreadyRan, _, err := d2.CurrentState(ctx) + require.NoError(t, err) + require.Equal(t, nilVersion, version) + assert.False(t, alreadyRan) + + // now update CurrentState and compare + require.NoError(t, d2.EnsureVersionTable(ctx)) + require.NoError(t, d2.setVersion(ctx, 2, false)) + version, alreadyRan, _, err = d2.CurrentState(ctx) + require.NoError(t, err) + require.Equal(t, 2, version) + assert.True(t, alreadyRan) + + // meanwhile, the public schema still has the other CurrentState + version, alreadyRan, _, err = d.CurrentState(ctx) + require.NoError(t, err) + require.Equal(t, 1, version) + assert.True(t, alreadyRan) } func TestPostgres_Lock(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - ps, err := open(t, ctx, addr) - if err != nil { - t.Fatal(err) - } - - test(t, ps, []byte("SELECT 1")) - - err = ps.Lock(ctx) - if err != nil { - t.Fatal(err) - } - - err = ps.Unlock(ctx) - if err != nil { - t.Fatal(err) - } - - err = ps.Lock(ctx) - if err != nil { - t.Fatal(err) - } - - err = ps.Unlock(ctx) - if err != nil { - t.Fatal(err) - } - - // make sure we call call Unlock in an idempotent manner. - err = ps.Unlock(ctx) - if err != nil { - t.Fatal(err) - } + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + test(t, d, []byte("SELECT 1")) + + err = d.Lock(ctx) + if err != nil { + t.Fatal(err) + } + + err = d.Unlock(ctx) + if err != nil { + t.Fatal(err) + } + + err = d.Lock(ctx) + if err != nil { + t.Fatal(err) + } + + err = d.Unlock(ctx) + if err != nil { + t.Fatal(err) + } + + // make sure we call call Unlock in an idempotent manner. + err = d.Unlock(ctx) + if err != nil { + t.Fatal(err) + } } func TestEnsureTable_Fresh(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - p, err := open(t, ctx, addr) - if err != nil { - require.NoError(t, err) - } - t.Cleanup(func() { - require.NoError(t, p.close(t)) - }) - - tableCreated := false - query := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = '" + defaultMigrationsTable + "')" - assert.NoError(t, p.db.QueryRowContext(ctx, query).Scan(&tableCreated)) - assert.False(t, tableCreated) - - assert.NoError(t, p.EnsureVersionTable(ctx)) - assert.NoError(t, p.db.QueryRowContext(ctx, query).Scan(&tableCreated)) - assert.True(t, tableCreated) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + tableCreated := false + query := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = '" + defaultMigrationsTable + "')" + assert.NoError(t, d.db.QueryRowContext(ctx, query).Scan(&tableCreated)) + assert.False(t, tableCreated) + + assert.NoError(t, d.EnsureVersionTable(ctx)) + assert.NoError(t, d.db.QueryRowContext(ctx, query).Scan(&tableCreated)) + assert.True(t, tableCreated) } func TestEnsureTable_ExistingTable(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - p, err := open(t, ctx, addr) - if err != nil { - require.NoError(t, err) - } - t.Cleanup(func() { - require.NoError(t, p.close(t)) - }) - assert.NoError(t, p.EnsureVersionTable(ctx)) - - oldTableCreate := `CREATE TABLE IF NOT EXISTS schema_migrations (version bigint primary key, dirty boolean not null)` - _, err = p.db.ExecContext(ctx, oldTableCreate) - assert.NoError(t, err) - - assert.NoError(t, p.EnsureVersionTable(ctx)) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + assert.NoError(t, d.EnsureVersionTable(ctx)) + + oldTableCreate := `CREATE TABLE IF NOT EXISTS schema_migrations (version bigint primary key, dirty boolean not null)` + _, err = d.db.ExecContext(ctx, oldTableCreate) + assert.NoError(t, err) + + assert.NoError(t, d.EnsureVersionTable(ctx)) } func TestEnsureTable_OldTable(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - p, err := open(t, ctx, addr) - if err != nil { - require.NoError(t, err) - } - t.Cleanup(func() { - require.NoError(t, p.close(t)) - }) - - oldTableCreate := `CREATE TABLE IF NOT EXISTS schema_migrations (version bigint primary key, dirty boolean not null)` - _, err = p.db.ExecContext(ctx, oldTableCreate) - assert.NoError(t, err) - - tableExists := false - oldTableCheck := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = 'schema_migrations')" - assert.NoError(t, p.db.QueryRowContext(ctx, oldTableCheck).Scan(&tableExists)) - assert.True(t, tableExists) - - query := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = '" + defaultMigrationsTable + "')" - assert.NoError(t, p.db.QueryRowContext(ctx, query).Scan(&tableExists)) - assert.False(t, tableExists) - - assert.NoError(t, p.EnsureVersionTable(ctx)) - - assert.NoError(t, p.db.QueryRowContext(ctx, oldTableCheck).Scan(&tableExists)) - assert.False(t, tableExists) - assert.NoError(t, p.db.QueryRowContext(ctx, query).Scan(&tableExists)) - assert.True(t, tableExists) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + oldTableCreate := `CREATE TABLE IF NOT EXISTS schema_migrations (version bigint primary key, dirty boolean not null)` + _, err = d.db.ExecContext(ctx, oldTableCreate) + assert.NoError(t, err) + + tableExists := false + oldTableCheck := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = 'schema_migrations')" + assert.NoError(t, d.db.QueryRowContext(ctx, oldTableCheck).Scan(&tableExists)) + assert.True(t, tableExists) + + query := "SELECT exists (SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_name = '" + defaultMigrationsTable + "')" + assert.NoError(t, d.db.QueryRowContext(ctx, query).Scan(&tableExists)) + assert.False(t, tableExists) + + assert.NoError(t, d.EnsureVersionTable(ctx)) + + assert.NoError(t, d.db.QueryRowContext(ctx, oldTableCheck).Scan(&tableExists)) + assert.False(t, tableExists) + assert.NoError(t, d.db.QueryRowContext(ctx, query).Scan(&tableExists)) + assert.True(t, tableExists) } func TestRollback(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - p, err := open(t, ctx, addr) - if err != nil { - require.NoError(t, err) - } - t.Cleanup(func() { - require.NoError(t, p.close(t)) - }) - - assert.NoError(t, p.StartRun(ctx)) - assert.NoError(t, p.EnsureVersionTable(ctx)) - assert.NoError(t, p.Run(ctx, bytes.NewReader([]byte("create table if not exists foo (foo text)")), 2)) - var exists bool - query := "select exists (select 1 from information_schema.tables where table_name = 'foo' and table_schema = (select current_schema()))" - assert.NoError(t, p.conn.QueryRowContext(context.Background(), query).Scan(&exists)) - assert.True(t, exists) - assert.NoError(t, p.Rollback()) - - assert.NoError(t, p.conn.QueryRowContext(context.Background(), query).Scan(&exists)) - assert.False(t, exists) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + assert.NoError(t, d.StartRun(ctx)) + assert.NoError(t, d.EnsureVersionTable(ctx)) + assert.NoError(t, d.Run(ctx, bytes.NewReader([]byte("create table if not exists foo (foo text)")), 2)) + var exists bool + query := "select exists (select 1 from information_schema.tables where table_name = 'foo' and table_schema = (select current_schema()))" + assert.NoError(t, d.conn.QueryRowContext(context.Background(), query).Scan(&exists)) + assert.True(t, exists) + assert.NoError(t, d.Rollback()) + + assert.NoError(t, d.conn.QueryRowContext(context.Background(), query).Scan(&exists)) + assert.False(t, exists) } func TestRun_Error(t *testing.T) { - dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { - ctx := context.Background() - ip, port, err := c.FirstPort() - if err != nil { - t.Fatal(err) - } - - addr := pgConnectionString(ip, port) - p, err := open(t, ctx, addr) - if err != nil { - require.NoError(t, err) - } - t.Cleanup(func() { - require.NoError(t, p.close(t)) - }) - - err = p.Run(ctx, bytes.NewReader([]byte("SELECT *\nFROM foo")), 2) - assert.Error(t, err) + t.Parallel() + ctx := context.Background() + c, u, _, err := dbtest.StartUsingTemplate(dbtest.Postgres, dbtest.WithTemplate(dbtest.Template1)) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) }) + d, err := open(t, ctx, u) + require.NoError(t, err) + + err = d.Run(ctx, bytes.NewReader([]byte("SELECT *\nFROM foo")), 2) + assert.Error(t, err) } func Test_computeLineFromPos(t *testing.T) { diff --git a/internal/db/schema/schema_test.go b/internal/db/schema/schema_test.go index e921c9060..634e881ef 100644 --- a/internal/db/schema/schema_test.go +++ b/internal/db/schema/schema_test.go @@ -5,16 +5,16 @@ import ( "database/sql" "testing" - "github.com/hashicorp/boundary/internal/docker" + "github.com/hashicorp/boundary/testing/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMigrateStore(t *testing.T) { - dialect := "postgres" + dialect := dbtest.Postgres ctx := context.Background() - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, c()) @@ -45,10 +45,10 @@ func TestMigrateStore(t *testing.T) { func Test_MigrateStore_WithMigrationStates(t *testing.T) { assert, require := assert.New(t), require.New(t) - dialect := "postgres" + dialect := dbtest.Postgres ctx := context.Background() - c, u, _, err := docker.StartDbInDocker(dialect) + c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1)) require.NoError(err) t.Cleanup(func() { require.NoError(c()) diff --git a/internal/db/testing.go b/internal/db/testing.go index 440f748c0..915695103 100644 --- a/internal/db/testing.go +++ b/internal/db/testing.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/db/schema" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/oplog/store" + "github.com/hashicorp/boundary/testing/dbtest" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/jinzhu/gorm" @@ -30,7 +31,7 @@ func TestSetup(t *testing.T, dialect string, opt ...TestOption) (*gorm.DB, strin switch opts.withTestDatabaseUrl { case "": - cleanup, url, _, err = StartDbInDocker(dialect) + cleanup, url, _, err = dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(opts.withTemplate)) if err != nil { t.Fatal(err) } @@ -172,6 +173,7 @@ type testOptions struct { withOperation oplog.OpType withTestDatabaseUrl string withResourcePrivateId bool + withTemplate string } func getDefaultTestOptions() testOptions { @@ -211,3 +213,11 @@ func WithResourcePrivateId(enable bool) TestOption { o.withResourcePrivateId = enable } } + +// WithTemplate provides a way to specify the source database template for creating +// a database. +func WithTemplate(template string) TestOption { + return func(o *testOptions) { + o.withTemplate = template + } +} diff --git a/internal/oplog/testing.go b/internal/oplog/testing.go index 054fec0e0..5476b6b91 100644 --- a/internal/oplog/testing.go +++ b/internal/oplog/testing.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/hashicorp/boundary/internal/db/schema" - "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/oplog/oplog_test" + "github.com/hashicorp/boundary/testing/dbtest" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/hashicorp/go-uuid" @@ -57,7 +57,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") + cleanup, retURL, _, err = dbtest.StartUsingTemplate(dbtest.Postgres) if err != nil { t.Fatal(err) } diff --git a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go index d152109cd..041ffcc63 100644 --- a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go +++ b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go @@ -124,7 +124,7 @@ func TestList(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform())) + assert.ElementsMatch(t, got.Items, tc.res.Items) // Test anonymous listing got, gErr = s.ListCredentialStores(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 7e5258865..79bfa0ed1 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -582,6 +582,7 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if opts.DisableOidcAuthMethodCreation { createOpts = append(createOpts, base.WithSkipOidcAuthMethodCreation()) } + createOpts = append(createOpts, base.WithDatabaseTemplate("boundary_template")) if err := tc.b.CreateDevDatabase(ctx, createOpts...); err != nil { t.Fatal(err) } diff --git a/testing/dbtest/docker/Dockerfile.11-alpine b/testing/dbtest/docker/Dockerfile.11-alpine new file mode 100644 index 000000000..f569cac04 --- /dev/null +++ b/testing/dbtest/docker/Dockerfile.11-alpine @@ -0,0 +1,6 @@ +FROM postgres:11-alpine + +ADD init-db.sh /docker-entrypoint-initdb.d/00-init-db.sh +ADD postgresql.conf /etc/postgresql/postgresql.conf + +CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] diff --git a/testing/dbtest/docker/Dockerfile.12-alpine b/testing/dbtest/docker/Dockerfile.12-alpine new file mode 100644 index 000000000..3ffdb3d35 --- /dev/null +++ b/testing/dbtest/docker/Dockerfile.12-alpine @@ -0,0 +1,6 @@ +FROM postgres:12-alpine + +ADD init-db.sh /docker-entrypoint-initdb.d/00-init-db.sh +ADD postgresql.conf /etc/postgresql/postgresql.conf + +CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] diff --git a/testing/dbtest/docker/Dockerfile.13-alpine b/testing/dbtest/docker/Dockerfile.13-alpine new file mode 100644 index 000000000..ddab38a16 --- /dev/null +++ b/testing/dbtest/docker/Dockerfile.13-alpine @@ -0,0 +1,6 @@ +FROM postgres:13-alpine + +ADD init-db.sh /docker-entrypoint-initdb.d/00-init-db.sh +ADD postgresql.conf /etc/postgresql/postgresql.conf + +CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] diff --git a/testing/dbtest/docker/Dockerfile.alpine b/testing/dbtest/docker/Dockerfile.alpine new file mode 100644 index 000000000..1655beab9 --- /dev/null +++ b/testing/dbtest/docker/Dockerfile.alpine @@ -0,0 +1,6 @@ +FROM postgres:alpine + +ADD init-db.sh /docker-entrypoint-initdb.d/00-init-db.sh +ADD postgresql.conf /etc/postgresql/postgresql.conf + +CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] diff --git a/testing/dbtest/docker/Makefile b/testing/dbtest/docker/Makefile new file mode 100644 index 000000000..f1b8ec515 --- /dev/null +++ b/testing/dbtest/docker/Makefile @@ -0,0 +1,50 @@ +all: docker-build + +CWD := $(shell pwd) +DOCKER_ARGS ?= -d + +REGISTRY_NAME?=docker.io/hashicorpboundary +IMAGE_NAME=postgres +IMAGE_TAG ?= $(REGISTRY_NAME)/$(IMAGE_NAME):11-alpine +PG_OPTS ?= +TEST_DB_PORT ?= 5432 + +# Generate targets from dockerfiles +dockerfiles = $(wildcard Dockerfile.*) +docker-builds = $(patsubst Dockerfile.%,%-build, $(dockerfiles)) +docker-publishes = $(patsubst Dockerfile.%,%-publish, $(dockerfiles)) + +${docker-builds}: %-build: + docker build -t $(REGISTRY_NAME)/$(IMAGE_NAME):$* -f Dockerfile.$* . + +docker-build: ${docker-builds} + +docker-publish: ${docker-publishes} + +${docker-publishes}: %-publish: + docker push $(REGISTRY_NAME)/$(IMAGE_NAME):$* + +database-up: + @echo "Using image: $(IMAGE_TAG)" + @echo "Additional postgres configuration: $(PG_OPTS)" + @docker run \ + $(DOCKER_ARGS) \ + --name boundary-sql-tests \ + -p $(TEST_DB_PORT):5432 \ + -e POSTGRES_PASSWORD=boundary \ + -e POSTGRES_USER=boundary \ + -e POSTGRES_DB=boundary \ + -e PGDATA=/pgdata \ + --mount type=tmpfs,destination=/pgdata \ + -v "$(CWD)/../../../internal/db/schema/migrations/postgres":/migrations \ + $(IMAGE_TAG) \ + -c 'config_file=/etc/postgresql/postgresql.conf' \ + $(PG_OPTS) 1> /dev/null + @echo "Test database available at: 127.0.0.1:$(TEST_DB_PORT)" + @echo "For database logs run:" + @echo " docker logs boundary-sql-test" +clean: + docker stop boundary-sql-tests || true + docker rm -v boundary-sql-tests || true + +.PHONY: all docker-build database-up ${docker-builds} ${docker-publishes} clean diff --git a/testing/dbtest/docker/init-db.sh b/testing/dbtest/docker/init-db.sh new file mode 100755 index 000000000..ae18f8edc --- /dev/null +++ b/testing/dbtest/docker/init-db.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e +shopt -s globstar + +psql -v "ON_ERROR_STOP=1" --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -q <= 0; { + if remain == 0 { + cache, remain = rand.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +func startUsingTemplate(dialect string, opt ...Option) (cleanup func() error, retURL, dbname string, err error) { + if _, ok := supportedDialects[dialect]; !ok { + return func() error { return nil }, "", "", fmt.Errorf("unsupported dialect: %s", dialect) + } + + db, err := sql.Open(dialect, fmt.Sprintf("postgres://boundary:boundary@127.0.0.1:%s/boundary?sslmode=disable", testDBPort)) + if err != nil { + return func() error { return nil }, "", "", fmt.Errorf("could not connect to source database: %w", err) + } + defer db.Close() + + dbname = fmt.Sprintf("boundary_test_%s", randStr(16)) + template := BoundaryTemplate + + opts := GetOpts(opt...) + if opts.withTemplate != "" { + template = opts.withTemplate + } + if _, ok := supportedTemplates[template]; !ok { + return func() error { return nil }, "", "", fmt.Errorf("unsupported database template: %s", template) + } + + _, err = db.Exec(fmt.Sprintf("create database %s template %s", dbname, template)) + if err != nil { + return func() error { return nil }, "", "", fmt.Errorf("could not create test database: %w", err) + } + + url := fmt.Sprintf("postgres://boundary:boundary@127.0.0.1:%s/%s?sslmode=disable", testDBPort, dbname) + + cleanup = func() error { + return dropDatabase(dialect, dbname) + } + + tdb, err := sql.Open(dialect, url) + if err != nil { + return func() error { return nil }, "", "", fmt.Errorf("could not create test database: %w", err) + } + defer tdb.Close() + + if err := tdb.Ping(); err != nil { + return func() error { return nil }, "", "", fmt.Errorf("could not ping test database: %w", err) + } + + return cleanup, url, dbname, nil +} + +const killconns = ` +select + pg_terminate_backend(pg_stat_activity.pid) +from + pg_stat_activity +where pg_stat_activity.datname = $1 + and pid <> pg_backend_pid(); +` + +func dropDatabase(dialect, dbname string) error { + if dbname == "" { + return fmt.Errorf("empty dbname") + } + + db, err := sql.Open(dialect, fmt.Sprintf("postgres://boundary:boundary@127.0.0.1:%s/boundary?sslmode=disable", testDBPort)) + if err != nil { + return err + } + defer db.Close() + + _, err = db.Exec(killconns, dbname) + if err != nil { + return err + } + _, err = db.Exec(fmt.Sprintf("drop database %s", dbname)) + if err != nil { + return err + } + return nil +} diff --git a/testing/dbtest/template_test.go b/testing/dbtest/template_test.go new file mode 100644 index 000000000..dcf305199 --- /dev/null +++ b/testing/dbtest/template_test.go @@ -0,0 +1,72 @@ +package dbtest_test + +import ( + "database/sql" + "errors" + "testing" + + "github.com/hashicorp/boundary/testing/dbtest" + "github.com/stretchr/testify/require" +) + +func TestStartUsingTemplate(t *testing.T) { + tests := []struct { + name string + dialect string + options []dbtest.Option + err error + }{ + { + "PostgresNoOptions", + dbtest.Postgres, + []dbtest.Option{}, + nil, + }, + { + "PostgresTemplate1", + dbtest.Postgres, + []dbtest.Option{ + dbtest.WithTemplate(dbtest.Template1), + }, + nil, + }, + { + "PostgresUnsupportedTemplate", + dbtest.Postgres, + []dbtest.Option{ + dbtest.WithTemplate("unsupportedTemplate"), + }, + errors.New("unsupported database template: unsupportedTemplate"), + }, + { + "UnsupportedDialect", + "unsupportedDialect", + []dbtest.Option{}, + errors.New("unsupported dialect: unsupportedDialect"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, u, _, err := dbtest.StartUsingTemplate(tt.dialect, tt.options...) + + if tt.err != nil { + require.EqualError(t, err, tt.err.Error()) + require.NoError(t, c()) + } else { + require.NoError(t, err) + + db, err := sql.Open(tt.dialect, u) + require.NoError(t, err) + require.NoError(t, db.Ping()) + db.Close() + + require.NoError(t, c()) + + // ensure that database is gone after calling c + db, _ = sql.Open(tt.dialect, u) + require.Error(t, db.Ping()) + } + }) + } +}