From 9bf1c74b10e4462bd65bffcd73e592a3f61ccc48 Mon Sep 17 00:00:00 2001 From: Maria Ines Parnisari Date: Thu, 18 Dec 2025 12:01:16 -0800 Subject: [PATCH 1/4] chore: use testcontainers --- .../integration/migrate_integration_test.go | 63 ++++---- .../restgateway_integration_test.go | 7 +- .../schemawatch_integration_test.go | 92 +++++------ .../integration/serve_integration_test.go | 125 +++++++-------- .../servetesting_integration_test.go | 123 +++++++++------ .../integration/servetesting_race_test.go | 17 +- .../memory_protection_integration_test.go | 35 ++-- go.mod | 38 +++-- go.sum | 80 ++++++++-- internal/datastore/crdb/crdb_test.go | 60 ++++--- internal/testserver/datastore/crdb.go | 143 ++++++++--------- internal/testserver/datastore/mysql.go | 73 +++++---- internal/testserver/datastore/postgres.go | 149 +++++++++++------- internal/testserver/datastore/spanner.go | 84 +++++----- 14 files changed, 606 insertions(+), 483 deletions(-) diff --git a/cmd/spicedb/integration/migrate_integration_test.go b/cmd/spicedb/integration/migrate_integration_test.go index 3cd9509b7..b208165eb 100644 --- a/cmd/spicedb/integration/migrate_integration_test.go +++ b/cmd/spicedb/integration/migrate_integration_test.go @@ -4,14 +4,17 @@ package integration_test import ( "bytes" + "context" "fmt" + "io" "slices" + "strings" "testing" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" testdatastore "github.com/authzed/spicedb/internal/testserver/datastore" "github.com/authzed/spicedb/pkg/datastore" @@ -20,18 +23,14 @@ import ( var toSkip = []string{"memory"} func TestMigrate(t *testing.T) { + ctx := context.Background() bridgeNetworkName := fmt.Sprintf("bridge-%s", uuid.New().String()) - pool, err := dockertest.NewPool("") - require.NoError(t, err) - // Create a bridge network for testing. - network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ - Name: bridgeNetworkName, - }) + net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{"name": bridgeNetworkName})) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Client.RemoveNetwork(network.ID) + _ = net.Remove(ctx) }) for _, engineKey := range datastore.Engines { @@ -45,43 +44,43 @@ func TestMigrate(t *testing.T) { r := testdatastore.RunDatastoreEngineWithBridge(t, engineKey, bridgeNetworkName) db := r.NewDatabase(t) - envVars := []string{} + envVars := map[string]string{} if wev, ok := r.(testdatastore.RunningEngineForTestWithEnvVars); ok { - envVars = wev.ExternalEnvVars() + for _, env := range wev.ExternalEnvVars() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVars[parts[0]] = parts[1] + } + } } // Run the migrate command and wait for it to complete. - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"migrate", "head", "--datastore-engine", engineKey, "--datastore-conn-uri", db}, - NetworkID: bridgeNetworkName, - Env: envVars, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"migrate", "head", "--datastore-engine", engineKey, "--datastore-conn-uri", db}, + Networks: []string{bridgeNetworkName}, + Env: envVars, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(resource) + _ = container.Terminate(ctx) }) // Ensure the command completed successfully. - status, err := pool.Client.WaitContainerWithContext(resource.Container.ID, t.Context()) + state, err := container.State(ctx) require.NoError(t, err) - if status != 0 { + if state.ExitCode != 0 { stream := new(bytes.Buffer) - lerr := pool.Client.Logs(docker.LogsOptions{ - Context: t.Context(), - OutputStream: stream, - ErrorStream: stream, - Stdout: true, - Stderr: true, - Container: resource.Container.ID, - }) + logReader, lerr := container.Logs(ctx) + require.NoError(t, lerr) + defer logReader.Close() + + _, lerr = io.Copy(stream, logReader) require.NoError(t, lerr) require.Fail(t, "Got non-zero exit code", stream.String()) diff --git a/cmd/spicedb/integration/restgateway_integration_test.go b/cmd/spicedb/integration/restgateway_integration_test.go index 64bd0cb81..68f6cab34 100644 --- a/cmd/spicedb/integration/restgateway_integration_test.go +++ b/cmd/spicedb/integration/restgateway_integration_test.go @@ -8,17 +8,16 @@ import ( "net/http" "testing" - "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" ) func TestRESTGateway(t *testing.T) { require := require.New(t) tester, err := newTester(t, - &dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", + testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", Cmd: []string{"serve", "--log-level", "debug", "--grpc-preshared-key", "somerandomkeyhere", "--http-enabled"}, ExposedPorts: []string{"50051/tcp", "8443/tcp"}, }, diff --git a/cmd/spicedb/integration/schemawatch_integration_test.go b/cmd/spicedb/integration/schemawatch_integration_test.go index 2ba01b1e1..77d2907e7 100644 --- a/cmd/spicedb/integration/schemawatch_integration_test.go +++ b/cmd/spicedb/integration/schemawatch_integration_test.go @@ -5,14 +5,16 @@ package integration_test import ( "context" "fmt" + "io" + "strings" "testing" "time" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" testdatastore "github.com/authzed/spicedb/internal/testserver/datastore" "github.com/authzed/spicedb/pkg/datastore" @@ -33,83 +35,75 @@ func TestSchemaWatch(t *testing.T) { } t.Run(driverName, func(t *testing.T) { + ctx := context.Background() bridgeNetworkName := fmt.Sprintf("bridge-%s", uuid.New().String()) - pool, err := dockertest.NewPool("") - require.NoError(t, err) - // Create a bridge network for testing. - network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ - Name: bridgeNetworkName, - }) + net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{"name": bridgeNetworkName})) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Client.RemoveNetwork(network.ID) + _ = net.Remove(ctx) }) engine := testdatastore.RunDatastoreEngineWithBridge(t, driverName, bridgeNetworkName) - envVars := []string{} + envVars := map[string]string{} if wev, ok := engine.(testdatastore.RunningEngineForTestWithEnvVars); ok { - envVars = wev.ExternalEnvVars() + for _, env := range wev.ExternalEnvVars() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVars[parts[0]] = parts[1] + } + } } // Run the migrate command and wait for it to complete. db := engine.NewDatabase(t) - migrateResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, - NetworkID: bridgeNetworkName, - Env: envVars, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + migrateContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, + Networks: []string{bridgeNetworkName}, + Env: envVars, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(migrateResource) + _ = migrateContainer.Terminate(ctx) }) // Ensure the command completed successfully. - status, err := pool.Client.WaitContainerWithContext(migrateResource.Container.ID, t.Context()) + exitCode, err := migrateContainer.State(ctx) require.NoError(t, err) - require.Equal(t, 0, status) + require.Equal(t, 0, exitCode.ExitCode) // Run a serve and immediately close, ensuring it shuts down gracefully. - serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", "", "--log-level", "trace", "--enable-experimental-watchable-schema-cache"}, - NetworkID: bridgeNetworkName, - Env: envVars, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + serveContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", "", "--log-level", "trace", "--enable-experimental-watchable-schema-cache"}, + Networks: []string{bridgeNetworkName}, + Env: envVars, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(serveResource) + _ = serveContainer.Terminate(ctx) }) ww := &watchingWriter{make(chan bool, 1), "starting watching cache"} - // Grab logs and ensure GC has run before starting a graceful shutdown. - opts := docker.LogsOptions{ - Context: context.Background(), - Stderr: true, - Stdout: true, - Follow: true, - Timestamps: true, - RawTerminal: true, - Container: serveResource.Container.ID, - OutputStream: ww, - } - + // Grab logs and ensure schema watch has started before graceful shutdown. go (func() { - err = pool.Client.Logs(opts) + logReader, err := serveContainer.Logs(ctx) + if err != nil { + assert.NoError(t, err) + return + } + defer logReader.Close() + _, err = io.Copy(ww, logReader) assert.NoError(t, err) })() @@ -121,7 +115,7 @@ func TestSchemaWatch(t *testing.T) { require.Fail(t, "timed out waiting for schema watch to run") } - require.True(t, gracefulShutdown(pool, serveResource)) + require.True(t, gracefulShutdown(ctx, serveContainer)) }) } } diff --git a/cmd/spicedb/integration/serve_integration_test.go b/cmd/spicedb/integration/serve_integration_test.go index fe1e98f27..46af22108 100644 --- a/cmd/spicedb/integration/serve_integration_test.go +++ b/cmd/spicedb/integration/serve_integration_test.go @@ -5,15 +5,16 @@ package integration_test import ( "context" "fmt" + "io" "strings" "testing" "time" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -31,9 +32,8 @@ func TestServe(t *testing.T) { requireParent := require.New(t) tester, err := newTester(t, - &dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", + testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", Cmd: []string{"serve", "--log-level", "debug", "--grpc-preshared-key", "firstkey", "--grpc-preshared-key", "secondkey"}, ExposedPorts: []string{"50051/tcp"}, }, @@ -93,15 +93,11 @@ func TestServe(t *testing.T) { } } -func gracefulShutdown(pool *dockertest.Pool, serveResource *dockertest.Resource) bool { +func gracefulShutdown(ctx context.Context, container testcontainers.Container) bool { closed := make(chan bool, 1) go func() { // Send SIGSTOP to have the container gracefully shutdown. - _ = pool.Client.KillContainer(docker.KillContainerOptions{ - ID: serveResource.Container.ID, - Signal: docker.SIGSTOP, - Context: context.Background(), - }) + _ = container.Stop(ctx, nil) closed <- true }() @@ -110,31 +106,28 @@ func gracefulShutdown(pool *dockertest.Pool, serveResource *dockertest.Resource) return true case <-time.After(10 * time.Second): - _ = pool.Purge(serveResource) + _ = container.Terminate(ctx) return false } } func TestGracefulShutdownInMemory(t *testing.T) { - pool, err := dockertest.NewPool("") - require.NoError(t, err) + ctx := context.Background() // Run a serve and immediately close, ensuring it shuts down gracefully. - serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"serve", "--grpc-preshared-key", "firstkey"}, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"serve", "--grpc-preshared-key", "firstkey"}, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(serveResource) + _ = container.Terminate(ctx) }) - require.True(t, gracefulShutdown(pool, serveResource)) + require.True(t, gracefulShutdown(ctx, container)) } type watchingWriter struct { @@ -161,84 +154,76 @@ func TestGracefulShutdown(t *testing.T) { for driverName, awaitGC := range engines { t.Run(driverName, func(t *testing.T) { + ctx := context.Background() bridgeNetworkName := fmt.Sprintf("bridge-%s", uuid.New().String()) - pool, err := dockertest.NewPool("") - require.NoError(t, err) - // Create a bridge network for testing. - network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ - Name: bridgeNetworkName, - }) + net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{"name": bridgeNetworkName})) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Client.RemoveNetwork(network.ID) + _ = net.Remove(ctx) }) engine := testdatastore.RunDatastoreEngineWithBridge(t, driverName, bridgeNetworkName) - envVars := []string{} + envVars := map[string]string{} if wev, ok := engine.(testdatastore.RunningEngineForTestWithEnvVars); ok { - envVars = wev.ExternalEnvVars() + for _, env := range wev.ExternalEnvVars() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envVars[parts[0]] = parts[1] + } + } } // Run the migrate command and wait for it to complete. db := engine.NewDatabase(t) - migrateResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, - NetworkID: bridgeNetworkName, - Env: envVars, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + migrateContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, + Networks: []string{bridgeNetworkName}, + Env: envVars, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(migrateResource) + _ = migrateContainer.Terminate(ctx) }) // Ensure the command completed successfully. - status, err := pool.Client.WaitContainerWithContext(migrateResource.Container.ID, t.Context()) + exitCode, err := migrateContainer.State(ctx) require.NoError(t, err) - require.Equal(t, 0, status) + require.Equal(t, 0, exitCode.ExitCode) // Run a serve and immediately close, ensuring it shuts down gracefully. - serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", ""}, - NetworkID: bridgeNetworkName, - Env: envVars, - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + serveContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", ""}, + Networks: []string{bridgeNetworkName}, + Env: envVars, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - _ = pool.Purge(serveResource) + _ = serveContainer.Terminate(ctx) }) if awaitGC { ww := &watchingWriter{make(chan bool, 1), "running garbage collection worker"} // Grab logs and ensure GC has run before starting a graceful shutdown. - opts := docker.LogsOptions{ - Context: context.Background(), - Stderr: true, - Stdout: true, - Follow: true, - Timestamps: true, - RawTerminal: true, - Container: serveResource.Container.ID, - OutputStream: ww, - } - go (func() { - err = pool.Client.Logs(opts) + logReader, err := serveContainer.Logs(ctx) + if err != nil { + assert.NoError(t, err) + return + } + defer logReader.Close() + _, err = io.Copy(ww, logReader) assert.NoError(t, err) })() @@ -251,7 +236,7 @@ func TestGracefulShutdown(t *testing.T) { } } - require.True(t, gracefulShutdown(pool, serveResource)) + require.True(t, gracefulShutdown(ctx, serveContainer)) }) } } diff --git a/cmd/spicedb/integration/servetesting_integration_test.go b/cmd/spicedb/integration/servetesting_integration_test.go index 1b4a5716b..cb9f6e59f 100644 --- a/cmd/spicedb/integration/servetesting_integration_test.go +++ b/cmd/spicedb/integration/servetesting_integration_test.go @@ -12,9 +12,8 @@ import ( "time" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -31,9 +30,8 @@ func TestTestServer(t *testing.T) { require := require.New(t) key := uuid.NewString() tester, err := newTester(t, - &dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", + testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", Cmd: []string{ "serve-testing", "--log-level", "debug", @@ -189,38 +187,59 @@ const retryCount = 8 // newTester spins up a SpiceDB server running against a specific datastore with a specific access token. // It also writes or reads a schema. // On test termination it cleans up all resources. -func newTester(t *testing.T, containerOpts *dockertest.RunOptions, token string, withExistingSchema bool) (*spicedbHandle, error) { +func newTester(t *testing.T, containerReq testcontainers.ContainerRequest, token string, withExistingSchema bool) (*spicedbHandle, error) { + ctx := context.Background() + for i := 0; i < retryCount; i++ { - pool, err := dockertest.NewPool("") + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: containerReq, + Started: true, + }) if err != nil { - return nil, fmt.Errorf("could not connect to docker: %w", err) + return nil, fmt.Errorf("could not start resource: %w", err) } - pool.MaxWait = 60 * time.Second + t.Cleanup(func() { + _ = container.Terminate(ctx) + }) - resource, err := pool.RunWithOptions(containerOpts) + mappedPort, err := container.MappedPort(ctx, "50051") if err != nil { - return nil, fmt.Errorf("could not start resource: %w", err) + return nil, fmt.Errorf("could not get port: %w", err) + } + port := mappedPort.Port() + + mappedReadonlyPort, err := container.MappedPort(ctx, "50052") + if err != nil { + return nil, fmt.Errorf("could not get readonly port: %w", err) } + readonlyPort := mappedReadonlyPort.Port() - port := resource.GetPort("50051/tcp") - readonlyPort := resource.GetPort("50052/tcp") - httpPort := resource.GetPort("8443/tcp") - readonlyHTTPPort := resource.GetPort("8444/tcp") + mappedHTTPPort, err := container.MappedPort(ctx, "8443") + if err != nil { + return nil, fmt.Errorf("could not get HTTP port: %w", err) + } + httpPort := mappedHTTPPort.Port() - t.Cleanup(func() { - _ = pool.Purge(resource) - }) + mappedReadonlyHTTPPort, err := container.MappedPort(ctx, "8444") + if err != nil { + return nil, fmt.Errorf("could not get readonly HTTP port: %w", err) + } + readonlyHTTPPort := mappedReadonlyHTTPPort.Port() // Give the service time to boot. - err = pool.Retry(func() error { + maxRetries := 30 + var lastErr error + for j := 0; j < maxRetries; j++ { conn, err := grpc.NewClient( fmt.Sprintf("localhost:%s", port), grpc.WithTransportCredentials(insecure.NewCredentials()), grpcutil.WithInsecureBearerToken(token), ) if err != nil { - return fmt.Errorf("could not create connection: %w", err) + lastErr = fmt.Errorf("could not create connection: %w", err) + time.Sleep(500 * time.Millisecond) + continue } t.Cleanup(func() { @@ -231,14 +250,17 @@ func newTester(t *testing.T, containerOpts *dockertest.RunOptions, token string, if withExistingSchema { _, err = client.ReadSchema(context.Background(), &v1.ReadSchemaRequest{}) - return err - } - - // Write a basic schema. - _, err = client.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ - Schema: ` + if err != nil { + lastErr = err + time.Sleep(500 * time.Millisecond) + continue + } + } else { + // Write a basic schema. + _, err = client.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ + Schema: ` definition user {} - + definition resource { relation reader: user relation writer: user @@ -246,33 +268,32 @@ func newTester(t *testing.T, containerOpts *dockertest.RunOptions, token string, permission view = reader + writer } `, - }) - - return err - }) - if err != nil { - stream := new(bytes.Buffer) - - lerr := pool.Client.Logs(docker.LogsOptions{ - Context: t.Context(), - OutputStream: stream, - ErrorStream: stream, - Stdout: true, - Stderr: true, - Container: resource.Container.ID, - }) - require.NoError(t, lerr) + }) + if err != nil { + lastErr = err + time.Sleep(500 * time.Millisecond) + continue + } + } - fmt.Printf("got error on startup: %v\ncontainer logs: %s\n", err, stream.String()) - continue + return &spicedbHandle{ + port: port, + readonlyPort: readonlyPort, + HTTPPort: httpPort, + readonlyHTTPPort: readonlyHTTPPort, + }, nil } - return &spicedbHandle{ - port: port, - readonlyPort: readonlyPort, - HTTPPort: httpPort, - readonlyHTTPPort: readonlyHTTPPort, - }, nil + // If we got here, retries failed + logs, err := container.Logs(ctx) + if err == nil { + stream := new(bytes.Buffer) + _, _ = io.Copy(stream, logs) + fmt.Printf("got error on startup: %v\ncontainer logs: %s\n", lastErr, stream.String()) + } else { + fmt.Printf("got error on startup: %v (could not retrieve logs: %v)\n", lastErr, err) + } + continue } return nil, fmt.Errorf("hit maximum retries when trying to boot SpiceDB server") diff --git a/cmd/spicedb/integration/servetesting_race_test.go b/cmd/spicedb/integration/servetesting_race_test.go index 32c77266c..64e4be90b 100644 --- a/cmd/spicedb/integration/servetesting_race_test.go +++ b/cmd/spicedb/integration/servetesting_race_test.go @@ -12,8 +12,8 @@ import ( "time" "github.com/google/uuid" - "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" healthpb "google.golang.org/grpc/health/grpc_health_v1" @@ -26,11 +26,16 @@ func TestCheckPermissionOnTesterNoFlakes(t *testing.T) { _, b, _, _ := runtime.Caller(0) basepath := filepath.Dir(b) tester, err := newTester(t, - &dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"serve-testing", "--load-configs", "/mnt/spicedb_bootstrap.yaml"}, - Mounts: []string{path.Join(basepath, "testdata/bootstrap.yaml") + ":/mnt/spicedb_bootstrap.yaml"}, + testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"serve-testing", "--load-configs", "/mnt/spicedb_bootstrap.yaml"}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: path.Join(basepath, "testdata/bootstrap.yaml"), + ContainerFilePath: "/mnt/spicedb_bootstrap.yaml", + FileMode: 0644, + }, + }, ExposedPorts: []string{"50051/tcp", "50052/tcp", "8443/tcp", "8444/tcp"}, }, uuid.NewString(), diff --git a/cmd/spicedb/memoryprotection/memory_protection_integration_test.go b/cmd/spicedb/memoryprotection/memory_protection_integration_test.go index 296a8f893..6dc6127bf 100644 --- a/cmd/spicedb/memoryprotection/memory_protection_integration_test.go +++ b/cmd/spicedb/memoryprotection/memory_protection_integration_test.go @@ -8,9 +8,8 @@ import ( "testing" "time" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -31,27 +30,29 @@ func init() { func TestServeWithMemoryProtectionMiddleware(t *testing.T) { t.Parallel() - pool, err := dockertest.NewPool("") - require.NoError(t, err) - + ctx := context.Background() serverToken := "mykey" - serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "authzed/spicedb", - Tag: "ci", - Cmd: []string{"serve", "--log-level=debug", "--grpc-preshared-key", serverToken, "--telemetry-endpoint=\"\""}, - ExposedPorts: []string{"50051/tcp"}, - Env: []string{"GOMEMLIMIT=1B"}, // NOTE: Absurdly low on purpose - }, func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "authzed/spicedb:ci", + Cmd: []string{"serve", "--log-level=debug", "--grpc-preshared-key", serverToken, "--telemetry-endpoint=\"\""}, + ExposedPorts: []string{"50051/tcp"}, + Env: map[string]string{ + "GOMEMLIMIT": "1B", // NOTE: Absurdly low on purpose + }, + }, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - require.NoError(t, pool.Purge(serveResource)) + require.NoError(t, container.Terminate(ctx)) }) - serverPort := serveResource.GetPort("50051/tcp") + mappedPort, err := container.MappedPort(ctx, "50051") + require.NoError(t, err) + serverPort := mappedPort.Port() + conn, err := grpc.NewClient(fmt.Sprintf("localhost:%s", serverPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpcutil.WithInsecureBearerToken(serverToken), diff --git a/go.mod b/go.mod index 4fa958c91..f909068df 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,6 @@ replace github.com/fsnotify/fsnotify => github.com/fsnotify/fsnotify v1.9.0 // https://github.com/hdrodz/tdigest/commits/fix-oob-access replace github.com/influxdata/tdigest => github.com/hdrodz/tdigest v0.0.0-20230422191729-3d4528d8cfec -// See https://github.com/ory/dockertest/issues/614 and https://pkg.go.dev/vuln/GO-2025-3829 -replace github.com/docker/docker => github.com/docker/docker v28.0.0+incompatible - require ( buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251118093737-4105057cc7d4.1 cloud.google.com/go/spanner v1.86.1 @@ -101,6 +98,8 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0 github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 go.opencensus.io v0.24.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 @@ -159,7 +158,7 @@ require ( cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.2 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect dev.gaijin.team/go/golib v0.6.0 // indirect filippo.io/edwards25519 v1.1.1 // indirect @@ -226,17 +225,24 @@ require ( github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/dave/jennifer v1.7.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/docker/cli v27.4.1+incompatible // indirect - github.com/docker/docker v27.1.1+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.9.0 // indirect github.com/ecordell/optgen v0.2.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect @@ -253,6 +259,7 @@ require ( github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -330,9 +337,10 @@ require ( github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/magefile/mage v1.15.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect @@ -346,11 +354,16 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/moricho/tparallel v0.3.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -360,12 +373,13 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.21.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.2.8 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runc v1.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.8.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect @@ -388,6 +402,7 @@ require ( github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect github.com/securego/gosec/v2 v2.22.10 // indirect + github.com/shirou/gopsutil/v4 v4.25.10 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.4.0 // indirect @@ -406,6 +421,8 @@ require ( github.com/tetratelabs/wazero v1.9.0 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect @@ -422,6 +439,7 @@ require ( github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect diff --git a/go.sum b/go.sum index 8bfd804e6..5bbbca7ea 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6M codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= @@ -639,6 +639,8 @@ github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8 github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= @@ -865,7 +867,17 @@ github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -890,22 +902,26 @@ github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42 github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= -github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= +github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ecordell/optgen v0.2.3 h1:DXuT9cYRInIJEh/dIOuLPgi7gYXrlfjzV/KsD80CXLE= github.com/ecordell/optgen v0.2.3/go.mod h1:pqjipFkG6vAwvKgjPGWaZyqmtWAqdb2w6EcTnP+kgqQ= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= @@ -1000,6 +1016,8 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= @@ -1370,6 +1388,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -1377,8 +1397,8 @@ github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddB github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= @@ -1421,8 +1441,18 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1435,6 +1465,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= @@ -1471,10 +1503,10 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= -github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= @@ -1512,6 +1544,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v1.8.0 h1:DL4RestQqRLr8U4LygLw8g2DX6RN1eBJOpa2mzsrl1Q= github.com/polyfloyd/go-errorlint v1.8.0/go.mod h1:G2W0Q5roxbLCt0ZQbdoxQxXktTjwNyDbEaj3n7jvl4s= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1619,6 +1653,8 @@ github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aep github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= @@ -1689,6 +1725,10 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0 h1:UNYfrnFV9mkO93Sw6hqRA5KbE9DsAvDeYKD4GDConiE= +github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0/go.mod h1:O8By1J/1y726YYk7obTIXxfv2OzonVe+ORq9Z+K+fDg= github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= @@ -1697,6 +1737,10 @@ github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tomarrell/wrapcheck/v2 v2.11.0 h1:BJSt36snX9+4WTIXeJ7nvHBQBcm1h2SjQMSlmQ6aFSU= github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= @@ -1741,6 +1785,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -2032,6 +2078,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2057,6 +2104,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2554,8 +2602,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/datastore/crdb/crdb_test.go b/internal/datastore/crdb/crdb_test.go index 3204a7494..4b812e133 100644 --- a/internal/datastore/crdb/crdb_test.go +++ b/internal/datastore/crdb/crdb_test.go @@ -25,11 +25,12 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - "github.com/ory/dockertest/v3" "github.com/prometheus/client_golang/prometheus" promclient "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/cockroachdb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -262,8 +263,6 @@ func TestCRDBDatastoreWithIntegrity(t *testing.T) { //nolint:tparallel func TestWatchFeatureDetection(t *testing.T) { t.Parallel() - pool, err := dockertest.NewPool("") - require.NoError(t, err) cases := []struct { name string postInit func(ctx context.Context, adminConn *pgx.Conn) @@ -309,8 +308,7 @@ func TestWatchFeatureDetection(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - adminConn, connStrings := newCRDBWithUser(t, pool) - require.NoError(t, err) + adminConn, connStrings := newCRDBWithUser(t) migrationDriver, err := crdbmigrations.NewCRDBDriver(connStrings[testuser]) require.NoError(t, err) @@ -349,7 +347,7 @@ const ( unprivileged provisionedUser = "unprivileged" ) -func newCRDBWithUser(t *testing.T, pool *dockertest.Pool) (adminConn *pgx.Conn, connStrings map[provisionedUser]string) { +func newCRDBWithUser(t *testing.T) (adminConn *pgx.Conn, connStrings map[provisionedUser]string) { // in order to create users, cockroach must be running with // real certs, and the root user must be authenticated with // client certs. @@ -457,39 +455,53 @@ func newCRDBWithUser(t *testing.T, pool *dockertest.Pool) (adminConn *pgx.Conn, })) require.NoError(t, rootCertFile.Close()) - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "mirror.gcr.io/cockroachdb/cockroach", - Tag: "v" + crdbTestVersion(), - Cmd: []string{"start-single-node", "--certs-dir", "/certs", "--accept-sql-without-tls"}, - Mounts: []string{certDir + ":/certs"}, - }) + container, err := cockroachdb.Run(t.Context(), + "cockroachdb/cockroach:v"+crdbTestVersion(), + cockroachdb.WithInsecure(), + testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Cmd: []string{"--certs-dir", "/certs", "--accept-sql-without-tls"}, + Mounts: testcontainers.ContainerMounts{{ + Source: testcontainers.GenericBindMountSource{HostPath: certDir}, + Target: "/certs", + }}, + }, + }), + ) require.NoError(t, err) t.Cleanup(func() { - require.NoError(t, pool.Purge(resource)) + require.NoError(t, container.Terminate(context.Background())) }) - port := resource.GetPort(fmt.Sprintf("%d/tcp", 26257)) - require.NoError(t, pool.Retry(func() error { - var err error + mappedPort, err := container.MappedPort(t.Context(), "26257") + require.NoError(t, err) + port := mappedPort.Port() + + // Retry connection + maxRetries := 10 + for i := 0; i < maxRetries; i++ { _, err = pgxpool.New(context.Background(), fmt.Sprintf("postgres://root@localhost:%[1]s/defaultdb?sslmode=verify-full&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir)) - if err != nil { - t.Log(err) - return err + if err == nil { + break } - return nil - })) + if i == maxRetries-1 { + require.NoError(t, err) + } + t.Log(err) + time.Sleep(1 * time.Second) + } - ctx, cancel := context.WithCancel(context.Background()) + cancel := func() {} t.Cleanup(cancel) adminConnString := fmt.Sprintf("postgresql://root:unused@localhost:%[1]s?sslmode=require&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir) require.Eventually(t, func() bool { - adminConn, err = pgx.Connect(ctx, adminConnString) + adminConn, err = pgx.Connect(t.Context(), adminConnString) return err == nil }, 30*time.Second, 1*time.Second) // create a non-admin user - _, err = adminConn.Exec(ctx, ` + _, err = adminConn.Exec(t.Context(), ` CREATE DATABASE testspicedb; CREATE USER testuser WITH PASSWORD 'testpass'; CREATE USER unprivileged WITH PASSWORD 'testpass2'; diff --git a/internal/testserver/datastore/crdb.go b/internal/testserver/datastore/crdb.go index f8e855a77..20c2e5392 100644 --- a/internal/testserver/datastore/crdb.go +++ b/internal/testserver/datastore/crdb.go @@ -3,14 +3,12 @@ package datastore import ( "context" "fmt" - "sync" "testing" "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/cockroachdb" crdbmigrations "github.com/authzed/spicedb/internal/datastore/crdb/migrations" "github.com/authzed/spicedb/pkg/datastore" @@ -18,107 +16,94 @@ import ( "github.com/authzed/spicedb/pkg/secrets" ) -const ( - enableRangefeeds = `SET CLUSTER SETTING kv.rangefeed.enabled = true;` -) - // crdbTester is safe for concurrent use by tests. type crdbTester struct { - conn *pgx.Conn // GUARDED_BY(connMutex) - connMutex sync.Mutex - hostname string - creds string - port string + databaseConnectStr string } var _ RunningEngineForTest = (*crdbTester)(nil) // RunCRDBForTesting returns a RunningEngineForTest for CRDB func RunCRDBForTesting(t testing.TB, bridgeNetworkName string, crdbVersion string) *crdbTester { - pool, err := dockertest.NewPool("") + ctx := context.Background() + name := "crds-" + uuid.New().String() + + containerReq := testcontainers.ContainerRequest{ + Name: name, + Cmd: []string{"--insecure", "--max-offset=50ms"}, + } + + if bridgeNetworkName != "" { + containerReq.Networks = []string{bridgeNetworkName} + } + + opts := []testcontainers.ContainerCustomizer{ + cockroachdb.WithInsecure(), + testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ + ContainerRequest: containerReq, + }), + } + + container, err := cockroachdb.Run(ctx, "mirror.gcr.io/cockroachdb/cockroach:v"+crdbVersion, opts...) require.NoError(t, err) - name := "crds-" + uuid.New().String() - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Name: name, - Repository: "mirror.gcr.io/cockroachdb/cockroach", - Tag: "v" + crdbVersion, - Cmd: []string{"start-single-node", "--insecure", "--max-offset=50ms"}, - NetworkID: bridgeNetworkName, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} + t.Cleanup(func() { + require.NoError(t, container.Terminate(ctx)) }) + + // enable changefeeds + code, _, _ := container.Exec(ctx, []string{ + "cockroach", "sql", + "--insecure", + "-e", "SET CLUSTER SETTING kv.rangefeed.enabled = true;", + }) + require.Equal(t, 0, code) + + // create DB + dbNameUniquePortion, err := secrets.TokenHex(4) require.NoError(t, err) - builder := &crdbTester{ - hostname: "localhost", - creds: "root:fake", - } - t.Cleanup(func() { - builder.connMutex.Lock() - defer builder.connMutex.Unlock() - if builder.conn != nil { - require.NoError(t, builder.conn.Close(context.Background())) - } - require.NoError(t, pool.Purge(resource)) + newDBName := "db" + dbNameUniquePortion + code, _, _ = container.Exec(ctx, []string{ + "cockroach", "sql", + "--insecure", + "-e", "CREATE DATABASE " + newDBName + ";", }) + require.Equal(t, 0, code) + + hostname := "localhost" + creds := "root:fake" + port := "26257" - port := resource.GetPort(fmt.Sprintf("%d/tcp", 26257)) if bridgeNetworkName != "" { - builder.hostname = name - builder.port = "26257" + hostname = name } else { - builder.port = port + mappedPort, err := container.MappedPort(ctx, "26257") + require.NoError(t, err) + port = mappedPort.Port() } - uri := fmt.Sprintf("postgres://%s@localhost:%s/defaultdb?sslmode=disable", builder.creds, port) - require.NoError(t, pool.Retry(func() error { - var err error - ctx, cancelConnect := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelConnect() - conn, err := pgx.Connect(ctx, uri) - if err != nil { - return err - } - builder.connMutex.Lock() - builder.conn = conn - ctx, cancelRangeFeeds := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelRangeFeeds() - _, err = builder.conn.Exec(ctx, enableRangefeeds) - builder.connMutex.Unlock() - return err - })) - - return builder + return &crdbTester{ + databaseConnectStr: fmt.Sprintf( + "postgres://%s@%s:%s/%s?sslmode=disable", + creds, + hostname, + port, + newDBName, + ), + } } -// NewDatabase creates a database. func (r *crdbTester) NewDatabase(t testing.TB) string { - uniquePortion, err := secrets.TokenHex(4) - require.NoError(t, err) - - newDBName := "db" + uniquePortion - - r.connMutex.Lock() - defer r.connMutex.Unlock() - _, err = r.conn.Exec(context.Background(), "CREATE DATABASE "+newDBName) - require.NoError(t, err) - - connectStr := fmt.Sprintf( - "postgres://%s@%s:%s/%s?sslmode=disable", - r.creds, - r.hostname, - r.port, - newDBName, - ) - return connectStr + // nothing to do; database was already created + return r.databaseConnectStr } // NewDatastore creates a database and runs migrations on it. func (r *crdbTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore { - connectStr := r.NewDatabase(t) + t.Log("MAINE") + t.Log(r.databaseConnectStr) + connectStr := r.databaseConnectStr migrationDriver, err := crdbmigrations.NewCRDBDriver(connectStr) require.NoError(t, err) diff --git a/internal/testserver/datastore/mysql.go b/internal/testserver/datastore/mysql.go index 7349f2157..325dcebb1 100644 --- a/internal/testserver/datastore/mysql.go +++ b/internal/testserver/datastore/mysql.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" "github.com/authzed/spicedb/internal/datastore/mysql/migrations" "github.com/authzed/spicedb/internal/datastore/mysql/version" @@ -50,24 +50,31 @@ func RunMySQLForTesting(t testing.TB, bridgeNetworkName string) RunningEngineFor // RunMySQLForTestingWithOptions returns a RunningEngineForTest for the mysql driver // backed by a MySQL instance, while allowing options to be forwarded func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bridgeNetworkName string) RunningEngineForTest { - pool, err := dockertest.NewPool("") - require.NoError(t, err) - + ctx := context.Background() containerImageTag := version.MinimumSupportedMySQLVersion name := "mysql-" + uuid.New().String() - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Name: name, - Repository: "mirror.gcr.io/library/mysql", - Tag: containerImageTag, - Env: []string{"MYSQL_ROOT_PASSWORD=secret"}, + + req := testcontainers.ContainerRequest{ + Name: name, + Image: "mirror.gcr.io/library/mysql:" + containerImageTag, + ExposedPorts: []string{fmt.Sprintf("%d/tcp", mysqlPort)}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "secret", + }, // increase max connections (default 151) to accommodate tests using the same docker container - Cmd: []string{"--max-connections=500"}, - NetworkID: bridgeNetworkName, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} + Cmd: []string{"--max-connections=500"}, + WaitingFor: wait.ForLog("port: 3306 MySQL Community Server"). + WithStartupTimeout(dockerBootTimeout), + } + + if bridgeNetworkName != "" { + req.Networks = []string{bridgeNetworkName} + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, }) require.NoError(t, err) @@ -75,34 +82,38 @@ func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bri creds: defaultCreds, options: options, } - t.Cleanup(func() { - require.NoError(t, builder.db.Close()) - require.NoError(t, pool.Purge(resource)) - }) - port := resource.GetPort(fmt.Sprintf("%d/tcp", mysqlPort)) + mappedPort, err := container.MappedPort(ctx, "3306") + require.NoError(t, err) + if bridgeNetworkName != "" { builder.hostname = name builder.port = strconv.Itoa(mysqlPort) } else { - builder.port = port + builder.port = mappedPort.Port() } - dsn := fmt.Sprintf("%s@(localhost:%s)/mysql?parseTime=true", builder.creds, port) + dsn := fmt.Sprintf("%s@(localhost:%s)/mysql?parseTime=true", builder.creds, port) builder.db, err = sql.Open("mysql", dsn) require.NoError(t, err) - require.NoError(t, pool.Retry(func() error { - var err error - ctx, cancelPing := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelPing() - err = builder.db.PingContext(ctx) - if err != nil { - return err + // TODO: use require.Eventually here + require.NoError(t, pool.Retry(func() error { + var err error + ctx, cancelPing := context.WithTimeout(context.Background(), dockerBootTimeout) + defer cancelPing() + err = builder.db.PingContext(ctx) + if err != nil { + return err } - return nil + time.Sleep(500 * time.Millisecond) })) + t.Cleanup(func() { + require.NoError(t, builder.db.Close()) + require.NoError(t, container.Terminate(ctx)) + }) + return builder } diff --git a/internal/testserver/datastore/postgres.go b/internal/testserver/datastore/postgres.go index 3f3fdfd49..6335dcb0a 100644 --- a/internal/testserver/datastore/postgres.go +++ b/internal/testserver/datastore/postgres.go @@ -8,9 +8,10 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" pgmigrations "github.com/authzed/spicedb/internal/datastore/postgres/migrations" "github.com/authzed/spicedb/pkg/datastore" @@ -38,7 +39,7 @@ type postgresTester struct { creds string targetMigration string pgbouncerProxy *container - pool *dockertest.Pool + pgContainer testcontainers.Container useContainerHostname bool } @@ -48,13 +49,13 @@ func RunPostgresForTesting(t testing.TB, bridgeNetworkName string, targetMigrati } func RunPostgresForTestingWithCommitTimestamps(t testing.TB, bridgeNetworkName string, targetMigration string, withCommitTimestamps bool, pgVersion string, enablePgbouncer bool) RunningEngineForTest { - pool, err := dockertest.NewPool("") - require.NoError(t, err) + ctx := context.Background() bridgeSupplied := bridgeNetworkName != "" + var bridgeNetwork *testcontainers.DockerNetwork if enablePgbouncer && !bridgeSupplied { // We will need a network bridge if we're running pgbouncer - bridgeNetworkName = createNetworkBridge(t, pool) + bridgeNetworkName, bridgeNetwork = createNetworkBridge(t) } postgresContainerHostname := "postgres-" + uuid.New().String() @@ -64,47 +65,59 @@ func RunPostgresForTestingWithCommitTimestamps(t testing.TB, bridgeNetworkName s cmd = append(cmd, "-c", "track_commit_timestamp=1") } - postgres, err := pool.RunWithOptions(&dockertest.RunOptions{ - Name: postgresContainerHostname, - Repository: "mirror.gcr.io/library/postgres", - Tag: pgVersion, - Env: []string{ - "POSTGRES_USER=" + PostgresTestUser, - "POSTGRES_PASSWORD=" + PostgresTestPassword, + req := testcontainers.ContainerRequest{ + Name: postgresContainerHostname, + Image: "mirror.gcr.io/library/postgres:" + pgVersion, + ExposedPorts: []string{PostgresTestPort + "/tcp"}, + Env: map[string]string{ + "POSTGRES_USER": PostgresTestUser, + "POSTGRES_PASSWORD": PostgresTestPassword, // use md5 auth to align postgres and pgbouncer auth methods - "POSTGRES_HOST_AUTH_METHOD=md5", - "POSTGRES_INITDB_ARGS=--auth=md5", + "POSTGRES_HOST_AUTH_METHOD": "md5", + "POSTGRES_INITDB_ARGS": "--auth=md5", }, - ExposedPorts: []string{PostgresTestPort + "/tcp"}, - NetworkID: bridgeNetworkName, - Cmd: cmd, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} + Cmd: cmd, + WaitingFor: wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(dockerBootTimeout), + } + + if bridgeNetworkName != "" { + req.Networks = []string{bridgeNetworkName} + } + + postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, }) require.NoError(t, err) + mappedPort, err := postgres.MappedPort(ctx, PostgresTestPort) + require.NoError(t, err) + builder := &postgresTester{ container: container{ hostHostname: "localhost", - hostPort: postgres.GetPort(PostgresTestPort + "/tcp"), + hostPort: mappedPort.Port(), containerHostname: postgresContainerHostname, containerPort: PostgresTestPort, }, creds: PostgresTestUser + ":" + PostgresTestPassword, targetMigration: targetMigration, useContainerHostname: bridgeSupplied, - pool: pool, + pgContainer: postgres, } t.Cleanup(func() { - require.NoError(t, pool.Purge(postgres)) + require.NoError(t, postgres.Terminate(ctx)) + if bridgeNetwork != nil { + _ = bridgeNetwork.Remove(ctx) + } }) if enablePgbouncer { // if we are running with pgbouncer enabled then set it up - builder.runPgbouncerForTesting(t, pool, bridgeNetworkName) + builder.runPgbouncerForTesting(t, bridgeNetworkName) } return builder @@ -171,47 +184,60 @@ func (b *postgresTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore return nil } -func createNetworkBridge(t testing.TB, pool *dockertest.Pool) string { +func createNetworkBridge(t testing.TB) (string, *testcontainers.DockerNetwork) { + ctx := context.Background() bridgeNetworkName := "bridge-" + uuid.New().String() - network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{Name: bridgeNetworkName}) + net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{ + "name": bridgeNetworkName, + })) require.NoError(t, err) - t.Cleanup(func() { - _ = pool.Client.RemoveNetwork(network.ID) - }) - return bridgeNetworkName + return net.Name, net } -func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pool *dockertest.Pool, bridgeNetworkName string) { +func (b *postgresTester) runPgbouncerForTesting(t testing.TB, bridgeNetworkName string) { + ctx := context.Background() uniqueID := uuid.New().String() pgbouncerContainerHostname := "pgbouncer-" + uniqueID - pgbouncer, err := pool.RunWithOptions(&dockertest.RunOptions{ - Name: pgbouncerContainerHostname, - Repository: "mirror.gcr.io/edoburu/pgbouncer", - Tag: "latest", - Env: []string{ - "DB_USER=" + PostgresTestUser, - "DB_PASSWORD=" + PostgresTestPassword, - "DB_HOST=" + b.containerHostname, - "DB_PORT=" + b.containerPort, - "LISTEN_PORT=" + PgbouncerTestPort, - "DB_NAME=*", // Needed to make pgbouncer okay with the randomly named databases generated by the test suite - "AUTH_TYPE=md5", // use the same auth type as postgres - "MAX_CLIENT_CONN=" + PostgresTestMaxConnections, - }, + req := testcontainers.ContainerRequest{ + Name: pgbouncerContainerHostname, + Image: "mirror.gcr.io/edoburu/pgbouncer:latest", ExposedPorts: []string{PgbouncerTestPort + "/tcp"}, - NetworkID: bridgeNetworkName, + Env: map[string]string{ + "DB_USER": PostgresTestUser, + "DB_PASSWORD": PostgresTestPassword, + "DB_HOST": b.containerHostname, + "DB_PORT": b.containerPort, + "LISTEN_PORT": PgbouncerTestPort, + "DB_NAME": "*", // Needed to make pgbouncer okay with the randomly named databases generated by the test suite + "AUTH_TYPE": "md5", // use the same auth type as postgres + "MAX_CLIENT_CONN": PostgresTestMaxConnections, + }, + WaitingFor: wait.ForListeningPort(PgbouncerTestPort + "/tcp"). + WithStartupTimeout(dockerBootTimeout), + } + + if bridgeNetworkName != "" { + req.Networks = []string{bridgeNetworkName} + } + + pgbouncer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - require.NoError(t, pool.Purge(pgbouncer)) + require.NoError(t, pgbouncer.Terminate(ctx)) }) + mappedPort, err := pgbouncer.MappedPort(ctx, PgbouncerTestPort) + require.NoError(t, err) + b.pgbouncerProxy = &container{ hostHostname: "localhost", - hostPort: pgbouncer.GetPort(PgbouncerTestPort + "/tcp"), + hostPort: mappedPort.Port(), containerHostname: pgbouncerContainerHostname, containerPort: PgbouncerTestPort, } @@ -220,17 +246,24 @@ func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pool *dockertest.P func (b *postgresTester) initializeHostConnection(t testing.TB) (conn *pgx.Conn) { hostname, port := b.getHostHostnameAndPort() uri := fmt.Sprintf("postgresql://%s@%s:%s/?sslmode=disable", b.creds, hostname, port) - err := b.pool.Retry(func() error { - var err error + + // Retry connection + maxRetries := 10 + var err error + for i := 0; i < maxRetries; i++ { ctx, cancelConnect := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelConnect() conn, err = pgx.Connect(ctx, uri) - if err != nil { - return err + cancelConnect() + if err == nil { + break } - return nil - }) - require.NoError(t, err) + + if i == maxRetries-1 { + require.NoError(t, err) + } + time.Sleep(500 * time.Millisecond) + } + return conn } diff --git a/internal/testserver/datastore/spanner.go b/internal/testserver/datastore/spanner.go index 350c6a8ec..1b9910d09 100644 --- a/internal/testserver/datastore/spanner.go +++ b/internal/testserver/datastore/spanner.go @@ -12,9 +12,9 @@ import ( instances "cloud.google.com/go/spanner/admin/instance/apiv1" "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" "github.com/authzed/spicedb/internal/datastore/spanner/migrations" "github.com/authzed/spicedb/pkg/datastore" @@ -29,54 +29,66 @@ type spannerTest struct { // RunSpannerForTesting returns a RunningEngineForTest for spanner func RunSpannerForTesting(t testing.TB, bridgeNetworkName string, targetMigration string) RunningEngineForTest { - pool, err := dockertest.NewPool("") - require.NoError(t, err) - + ctx := context.Background() name := "spanner-" + uuid.New().String() - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + + req := testcontainers.ContainerRequest{ Name: name, - Repository: "gcr.io/cloud-spanner-emulator/emulator", - Tag: "1.5.41", + Image: "gcr.io/cloud-spanner-emulator/emulator:1.5.41", ExposedPorts: []string{"9010/tcp"}, - NetworkID: bridgeNetworkName, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} + WaitingFor: wait.ForListeningPort("9010/tcp").WithStartupTimeout(dockerBootTimeout), + } + + if bridgeNetworkName != "" { + req.Networks = []string{bridgeNetworkName} + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, }) require.NoError(t, err) t.Cleanup(func() { - require.NoError(t, pool.Purge(resource)) + require.NoError(t, container.Terminate(ctx)) }) - port := resource.GetPort("9010/tcp") - spannerEmulatorAddr := "localhost:" + port + mappedPort, err := container.MappedPort(ctx, "9010") + require.NoError(t, err) + + spannerEmulatorAddr := "localhost:" + mappedPort.Port() t.Setenv("SPANNER_EMULATOR_HOST", spannerEmulatorAddr) - require.NoError(t, pool.Retry(func() error { + // Retry initialization + maxRetries := 10 + for i := 0; i < maxRetries; i++ { ctx, cancel := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancel() - instancesClient, err := instances.NewInstanceAdminClient(ctx) - if err != nil { - return err + if err == nil { + ctx, cancel = context.WithTimeout(context.Background(), dockerBootTimeout) + _, err = instancesClient.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ + Parent: "projects/fake-project-id", + InstanceId: "init", + Instance: &instancepb.Instance{ + Config: "emulator-config", + DisplayName: "Test Instance", + NodeCount: 1, + }, + }) + instancesClient.Close() + cancel() + if err == nil { + break + } + } else { + cancel() } - defer func() { require.NoError(t, instancesClient.Close()) }() - - ctx, cancel = context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancel() - _, err = instancesClient.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ - Parent: "projects/fake-project-id", - InstanceId: "init", - Instance: &instancepb.Instance{ - Config: "emulator-config", - DisplayName: "Test Instance", - NodeCount: 1, - }, - }) - return err - })) + + if i == maxRetries-1 { + require.NoError(t, err) + } + time.Sleep(500 * time.Millisecond) + } builder := &spannerTest{ targetMigration: targetMigration, From fbe233e121276128a230715d441f067a6e1a242a Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Fri, 27 Feb 2026 08:58:37 -0700 Subject: [PATCH 2/4] chore: do fixups --- go.mod | 10 +- go.sum | 23 +---- internal/fdw/pgserver_e2e_test.go | 161 +++++++++++++----------------- 3 files changed, 73 insertions(+), 121 deletions(-) diff --git a/go.mod b/go.mod index f909068df..10ebedd7f 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,6 @@ require ( github.com/muesli/roff v0.1.0 github.com/ngrok/sqlmw v0.0.0-20220520173518-97c9c04efc79 github.com/odigos-io/go-rtml v0.0.1 - github.com/ory/dockertest/v3 v3.12.0 github.com/outcaste-io/ristretto v0.2.3 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pganalyze/pg_query_go/v6 v6.1.0 @@ -100,6 +99,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 go.opencensus.io v0.24.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 @@ -177,7 +177,6 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/MirrexOne/unqueryvet v1.2.1 // indirect - github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect @@ -224,7 +223,6 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect - github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -238,7 +236,6 @@ require ( github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/docker/cli v27.4.1+incompatible // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -291,7 +288,6 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect @@ -374,7 +370,6 @@ require ( github.com/nunnatsa/ginkgolinter v0.21.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -431,9 +426,6 @@ require ( github.com/uudashr/iface v1.4.1 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect diff --git a/go.sum b/go.sum index 5bbbca7ea..0f5f2fab1 100644 --- a/go.sum +++ b/go.sum @@ -678,8 +678,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/MirrexOne/unqueryvet v1.2.1 h1:M+zdXMq84g+E1YOLa7g7ExN3dWfZQrdDSTCM7gC+m/A= github.com/MirrexOne/unqueryvet v1.2.1/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= @@ -865,8 +863,6 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= -github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= -github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -908,8 +904,6 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= @@ -1193,8 +1187,6 @@ github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -1429,6 +1421,8 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI= github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU= github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= @@ -1505,12 +1499,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= -github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= -github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -1729,6 +1719,8 @@ github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+ github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0 h1:UNYfrnFV9mkO93Sw6hqRA5KbE9DsAvDeYKD4GDConiE= github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0/go.mod h1:O8By1J/1y726YYk7obTIXxfv2OzonVe+ORq9Z+K+fDg= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= @@ -1761,13 +1753,6 @@ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8S github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= diff --git a/internal/fdw/pgserver_e2e_test.go b/internal/fdw/pgserver_e2e_test.go index 9f714883b..095efa698 100644 --- a/internal/fdw/pgserver_e2e_test.go +++ b/internal/fdw/pgserver_e2e_test.go @@ -1,4 +1,4 @@ -//go:build ci && !skipintegrationtests +// //go:build ci && !skipintegrationtests package fdw_test @@ -13,12 +13,12 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/jackc/pgx/v5" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/log" + "github.com/testcontainers/testcontainers-go/modules/postgres" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -35,12 +35,7 @@ import ( const ( pgVersion = "17.2" - dockerBootTimeout = 10 * time.Second postgresTestUser = "postgres" - postgresTestPassword = "secret" - postgresTestPort = "5432" - postgresTestMaxConnections = "3000" - pgbouncerTestPort = "6432" fdwPassword = "proxypassword" ) @@ -613,67 +608,69 @@ func runEndToEndTest(t *testing.T, tc e2eTestCase) { createCommands := strings.ReplaceAll(createCommandsTemplate, "port '5433'", fmt.Sprintf("port '%d'", pgServerPort)) // Invoke initial Postgres commands. - _, err = pgconn.Exec(context.Background(), createCommands) + _, err = pgconn.Exec(t.Context(), createCommands) require.NoError(t, err) t.Log("Initial Postgres commands executed") // Run queries. var lastZedToken string for _, q := range tc.queries { - t.Run(q.name, func(t *testing.T) { - // If this is an at_least_as_fresh test, inject the zedtoken into the query - queryToRun := q.query - argsToUse := q.args - if lastZedToken != "" && tc.name == "consistency at_least_as_fresh" && len(q.args) > 0 { - // Add consistency parameter to the WHERE clause - queryToRun = q.query + ` AND consistency = $` + fmt.Sprintf("%d", len(q.args)+1) - argsToUse = append(argsToUse, lastZedToken) - } else if lastZedToken != "" && tc.name == "consistency at_exact_snapshot" && len(q.args) > 0 { - // Add consistency parameter with @ prefix for at_exact_snapshot - queryToRun = q.query + ` AND consistency = $` + fmt.Sprintf("%d", len(q.args)+1) - argsToUse = append(argsToUse, "@"+lastZedToken) - } + t.Logf("executing test step: %s", q.name) + // If this is an at_least_as_fresh test, inject the zedtoken into the query + queryToRun := q.query + argsToUse := q.args + if lastZedToken != "" && tc.name == "consistency at_least_as_fresh" && len(q.args) > 0 { + // Add consistency parameter to the WHERE clause + queryToRun = q.query + ` AND consistency = $` + fmt.Sprintf("%d", len(q.args)+1) + argsToUse = append(argsToUse, lastZedToken) + } else if lastZedToken != "" && tc.name == "consistency at_exact_snapshot" && len(q.args) > 0 { + // Add consistency parameter with @ prefix for at_exact_snapshot + queryToRun = q.query + ` AND consistency = $` + fmt.Sprintf("%d", len(q.args)+1) + argsToUse = append(argsToUse, "@"+lastZedToken) + } - rows, err := pgconn.Query(context.Background(), queryToRun, argsToUse...) - require.NoError(t, err) - defer rows.Close() - - if q.expectedError != "" { - require.False(t, rows.Next()) - require.Error(t, rows.Err()) - require.ErrorContains(t, rows.Err(), q.expectedError) - } else { - // Read all rows. - foundRows := make([][]any, 0) - for rows.Next() { - require.NoError(t, rows.Err()) - - values, err := rows.Values() - require.NoError(t, err) - foundRows = append(foundRows, values) - - // If this is a RETURNING consistency query, capture the zedtoken - if len(values) == 1 && rows.FieldDescriptions()[0].Name == "consistency" { - if token, ok := values[0].(string); ok && token != "" { - lastZedToken = token - t.Logf("Captured ZedToken: %s", lastZedToken) - } - } - } + err := pgconn.Ping(t.Context()) + require.NoError(t, err, "could not ping server") + rows, err := pgconn.Query(t.Context(), queryToRun, argsToUse...) + require.NoError(t, err) + + if q.expectedError != "" { + require.False(t, rows.Next()) + require.Error(t, rows.Err()) + require.ErrorContains(t, rows.Err(), q.expectedError) + } else { + // Read all rows. + foundRows := make([][]any, 0) + for rows.Next() { require.NoError(t, rows.Err()) - // For queries with expected rows, verify them - if len(q.expectedRows) > 0 { - require.Equal(t, q.expectedRows, foundRows) - } else if q.query != queryToRun { - // For consistency tests that inject parameters, we just verify we got results - require.NotEmpty(t, foundRows) + values, err := rows.Values() + require.NoError(t, err) + foundRows = append(foundRows, values) + + // If this is a RETURNING consistency query, capture the zedtoken + if len(values) == 1 && rows.FieldDescriptions()[0].Name == "consistency" { + if token, ok := values[0].(string); ok && token != "" { + lastZedToken = token + t.Logf("Captured ZedToken: %s", lastZedToken) + } } - rows.Close() - require.Equal(t, q.expectedResponseTag, rows.CommandTag().String()) } - }) + + require.NoError(t, rows.Err()) + + // For queries with expected rows, verify them + if len(q.expectedRows) > 0 { + require.Equal(t, q.expectedRows, foundRows) + } else if q.query != queryToRun { + // For consistency tests that inject parameters, we just verify we got results + require.NotEmpty(t, foundRows) + } + require.Equal(t, q.expectedResponseTag, rows.CommandTag().String()) + } + // Close the query for this query in the loop + rows.Close() } } @@ -779,45 +776,23 @@ func runSpiceDB(t *testing.T) *authzed.Client { } func runPostgres(t *testing.T) (conn *pgx.Conn) { - pool, err := dockertest.NewPool("") + t.Helper() + // TODO: use alpine? + logger := log.TestLogger(t) + image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) + container, err := postgres.Run(t.Context(), image, + testcontainers.WithLogger(logger), + postgres.WithUsername(postgresTestUser), + postgres.WithPassword(fdwPassword), + postgres.BasicWaitStrategies(), +) require.NoError(t, err) - postgresContainerHostname := fmt.Sprintf("postgres-%s", uuid.New().String()) - postgres, err := pool.RunWithOptions(&dockertest.RunOptions{ - Name: postgresContainerHostname, - Repository: "mirror.gcr.io/library/postgres", - Tag: pgVersion, - Env: []string{ - "POSTGRES_USER=" + postgresTestUser, - "POSTGRES_PASSWORD=" + postgresTestPassword, - }, - ExposedPorts: []string{postgresTestPort + "/tcp"}, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) + connUri, err := container.ConnectionString(t.Context(), "sslmode=disable") require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, pool.Purge(postgres)) - }) - - hostname := "localhost" - port := postgres.GetPort(postgresTestPort + "/tcp") - - creds := postgresTestUser + ":" + postgresTestPassword - uri := fmt.Sprintf("postgresql://%s@%s:%s/?sslmode=disable", creds, hostname, port) - err = pool.Retry(func() error { - var err error - ctx, cancelConnect := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelConnect() - conn, err = pgx.Connect(ctx, uri) - if err != nil { - return err - } - return nil - }) + conn, err = pgx.Connect(t.Context(), connUri) require.NoError(t, err) + return conn } From 86b95549a3ac37ca8bf680dec8e17a30083d378f Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Fri, 27 Feb 2026 10:38:15 -0700 Subject: [PATCH 3/4] chore: use testcontainers for crdb --- .../config/postgres-with-timestamps.conf | 2 + .../testserver/datastore/config/postgres.conf | 1 + internal/testserver/datastore/crdb.go | 67 +---- internal/testserver/datastore/datastore.go | 16 +- internal/testserver/datastore/mysql.go | 40 +-- internal/testserver/datastore/postgres.go | 283 +++++++----------- internal/testserver/datastore/spanner.go | 85 +++--- 7 files changed, 167 insertions(+), 327 deletions(-) create mode 100644 internal/testserver/datastore/config/postgres-with-timestamps.conf create mode 100644 internal/testserver/datastore/config/postgres.conf diff --git a/internal/testserver/datastore/config/postgres-with-timestamps.conf b/internal/testserver/datastore/config/postgres-with-timestamps.conf new file mode 100644 index 000000000..46cb6fd66 --- /dev/null +++ b/internal/testserver/datastore/config/postgres-with-timestamps.conf @@ -0,0 +1,2 @@ +max_connections = 3000 +track_commit_timestamp = 1 diff --git a/internal/testserver/datastore/config/postgres.conf b/internal/testserver/datastore/config/postgres.conf new file mode 100644 index 000000000..f86ec1a23 --- /dev/null +++ b/internal/testserver/datastore/config/postgres.conf @@ -0,0 +1 @@ +max_connections = 3000 diff --git a/internal/testserver/datastore/crdb.go b/internal/testserver/datastore/crdb.go index 20c2e5392..10b1259fa 100644 --- a/internal/testserver/datastore/crdb.go +++ b/internal/testserver/datastore/crdb.go @@ -2,18 +2,14 @@ package datastore import ( "context" - "fmt" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/cockroachdb" crdbmigrations "github.com/authzed/spicedb/internal/datastore/crdb/migrations" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/migrate" - "github.com/authzed/spicedb/pkg/secrets" ) // crdbTester is safe for concurrent use by tests. @@ -24,73 +20,28 @@ type crdbTester struct { var _ RunningEngineForTest = (*crdbTester)(nil) // RunCRDBForTesting returns a RunningEngineForTest for CRDB -func RunCRDBForTesting(t testing.TB, bridgeNetworkName string, crdbVersion string) *crdbTester { - ctx := context.Background() - name := "crds-" + uuid.New().String() +func RunCRDBForTesting(t testing.TB, crdbVersion string) *crdbTester { + ctx := t.Context() - containerReq := testcontainers.ContainerRequest{ - Name: name, - Cmd: []string{"--insecure", "--max-offset=50ms"}, - } - - if bridgeNetworkName != "" { - containerReq.Networks = []string{bridgeNetworkName} - } - - opts := []testcontainers.ContainerCustomizer{ + container, err := cockroachdb.Run(ctx, "mirror.gcr.io/cockroachdb/cockroach:v"+crdbVersion, cockroachdb.WithInsecure(), - testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - }), - } - - container, err := cockroachdb.Run(ctx, "mirror.gcr.io/cockroachdb/cockroach:v"+crdbVersion, opts...) +) require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) - // enable changefeeds - code, _, _ := container.Exec(ctx, []string{ + code, _, err := container.Exec(ctx, []string{ "cockroach", "sql", "--insecure", "-e", "SET CLUSTER SETTING kv.rangefeed.enabled = true;", }) - require.Equal(t, 0, code) - - // create DB - dbNameUniquePortion, err := secrets.TokenHex(4) require.NoError(t, err) - - newDBName := "db" + dbNameUniquePortion - code, _, _ = container.Exec(ctx, []string{ - "cockroach", "sql", - "--insecure", - "-e", "CREATE DATABASE " + newDBName + ";", - }) require.Equal(t, 0, code) - hostname := "localhost" - creds := "root:fake" - port := "26257" - - if bridgeNetworkName != "" { - hostname = name - } else { - mappedPort, err := container.MappedPort(ctx, "26257") - require.NoError(t, err) - port = mappedPort.Port() - } + connUri, err := container.ConnectionString(ctx) + require.NoError(t, err) return &crdbTester{ - databaseConnectStr: fmt.Sprintf( - "postgres://%s@%s:%s/%s?sslmode=disable", - creds, - hostname, - port, - newDBName, - ), + databaseConnectStr: connUri, } } @@ -101,8 +52,6 @@ func (r *crdbTester) NewDatabase(t testing.TB) string { // NewDatastore creates a database and runs migrations on it. func (r *crdbTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore { - t.Log("MAINE") - t.Log(r.databaseConnectStr) connectStr := r.databaseConnectStr migrationDriver, err := crdbmigrations.NewCRDBDriver(connectStr) diff --git a/internal/testserver/datastore/datastore.go b/internal/testserver/datastore/datastore.go index 682f5134a..d844e124f 100644 --- a/internal/testserver/datastore/datastore.go +++ b/internal/testserver/datastore/datastore.go @@ -48,33 +48,25 @@ type RunningEngineForTestWithEnvVars interface { // respectively. Note also that the backing database or service will be shutdown automatically via // the Cleanup of the testing object. func RunDatastoreEngine(t testing.TB, engine string) RunningEngineForTest { - return RunDatastoreEngineWithBridge(t, engine, "") -} - -// RunDatastoreEngineWithBridge runs a datastore engine on a specific bridge. If a bridge is -// specified, then the hostnames returned by the engines are those to be called from another -// container on the bridge. -func RunDatastoreEngineWithBridge(t testing.TB, engine string, bridgeNetworkName string) RunningEngineForTest { switch engine { case "memory": - require.Empty(t, bridgeNetworkName, "memory datastore does not support bridge networking") return RunMemoryForTesting(t) case "cockroachdb": ver := os.Getenv("CRDB_TEST_VERSION") if ver == "" { ver = crdbversion.LatestTestedCockroachDBVersion } - return RunCRDBForTesting(t, bridgeNetworkName, ver) + return RunCRDBForTesting(t, ver) case "postgres": ver := os.Getenv("POSTGRES_TEST_VERSION") if ver == "" { ver = pgversion.LatestTestedPostgresVersion } - return RunPostgresForTesting(t, bridgeNetworkName, migrate.Head, ver, false) + return RunPostgresForTesting(t, migrate.Head, ver, false) case "mysql": - return RunMySQLForTesting(t, bridgeNetworkName) + return RunMySQLForTesting(t) case "spanner": - return RunSpannerForTesting(t, bridgeNetworkName, migrate.Head) + return RunSpannerForTesting(t, migrate.Head) default: t.Fatalf("found missing engine for RunDatastoreEngine: %s", engine) return nil diff --git a/internal/testserver/datastore/mysql.go b/internal/testserver/datastore/mysql.go index 325dcebb1..28a3ab0a7 100644 --- a/internal/testserver/datastore/mysql.go +++ b/internal/testserver/datastore/mysql.go @@ -43,20 +43,20 @@ type MySQLTesterOptions struct { // RunMySQLForTesting returns a RunningEngineForTest for the mysql driver // backed by a MySQL instance with RunningEngineForTest options - no prefix is added, and datastore migration is run. -func RunMySQLForTesting(t testing.TB, bridgeNetworkName string) RunningEngineForTest { - return RunMySQLForTestingWithOptions(t, MySQLTesterOptions{Prefix: "", MigrateForNewDatastore: true}, bridgeNetworkName) +func RunMySQLForTesting(t testing.TB) RunningEngineForTest { + return RunMySQLForTestingWithOptions(t, MySQLTesterOptions{Prefix: "", MigrateForNewDatastore: true}) } // RunMySQLForTestingWithOptions returns a RunningEngineForTest for the mysql driver // backed by a MySQL instance, while allowing options to be forwarded -func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bridgeNetworkName string) RunningEngineForTest { - ctx := context.Background() +func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions) RunningEngineForTest { + ctx := t.Context() containerImageTag := version.MinimumSupportedMySQLVersion - name := "mysql-" + uuid.New().String() + // TODO: add wait behavior + // TODO: simplify req := testcontainers.ContainerRequest{ - Name: name, Image: "mirror.gcr.io/library/mysql:" + containerImageTag, ExposedPorts: []string{fmt.Sprintf("%d/tcp", mysqlPort)}, Env: map[string]string{ @@ -68,10 +68,6 @@ func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bri WithStartupTimeout(dockerBootTimeout), } - if bridgeNetworkName != "" { - req.Networks = []string{bridgeNetworkName} - } - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, @@ -86,34 +82,12 @@ func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bri mappedPort, err := container.MappedPort(ctx, "3306") require.NoError(t, err) - if bridgeNetworkName != "" { - builder.hostname = name - builder.port = strconv.Itoa(mysqlPort) - } else { - builder.port = mappedPort.Port() - } + builder.port = mappedPort.Port() dsn := fmt.Sprintf("%s@(localhost:%s)/mysql?parseTime=true", builder.creds, port) builder.db, err = sql.Open("mysql", dsn) require.NoError(t, err) - // TODO: use require.Eventually here - require.NoError(t, pool.Retry(func() error { - var err error - ctx, cancelPing := context.WithTimeout(context.Background(), dockerBootTimeout) - defer cancelPing() - err = builder.db.PingContext(ctx) - if err != nil { - return err - } - time.Sleep(500 * time.Millisecond) - })) - - t.Cleanup(func() { - require.NoError(t, builder.db.Close()) - require.NoError(t, container.Terminate(ctx)) - }) - return builder } diff --git a/internal/testserver/datastore/postgres.go b/internal/testserver/datastore/postgres.go index 6335dcb0a..bfc297ca5 100644 --- a/internal/testserver/datastore/postgres.go +++ b/internal/testserver/datastore/postgres.go @@ -3,13 +3,14 @@ package datastore import ( "context" "fmt" + "strings" "testing" "time" - "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" @@ -20,104 +21,34 @@ import ( ) const ( - PostgresTestUser = "postgres" - PostgresTestPassword = "secret" - PostgresTestPort = "5432" + // NOTE: this is used in this file but also duplicated in postgres.conf. PostgresTestMaxConnections = "3000" - PgbouncerTestPort = "6432" + PgbouncerTestPort = "6432" ) -type container struct { - hostHostname string - hostPort string - containerHostname string - containerPort string -} - type postgresTester struct { - container - creds string targetMigration string - pgbouncerProxy *container - pgContainer testcontainers.Container - useContainerHostname bool + pgbouncerProxy *testcontainers.DockerContainer + pgContainer *postgres.PostgresContainer } // RunPostgresForTesting returns a RunningEngineForTest for postgres -func RunPostgresForTesting(t testing.TB, bridgeNetworkName string, targetMigration string, pgVersion string, enablePgbouncer bool) RunningEngineForTest { - return RunPostgresForTestingWithCommitTimestamps(t, bridgeNetworkName, targetMigration, true, pgVersion, enablePgbouncer) +func RunPostgresForTesting(t testing.TB, targetMigration string, pgVersion string, enablePgbouncer bool) RunningEngineForTest { + return RunPostgresForTestingWithCommitTimestamps(t, targetMigration, true, pgVersion, enablePgbouncer) } -func RunPostgresForTestingWithCommitTimestamps(t testing.TB, bridgeNetworkName string, targetMigration string, withCommitTimestamps bool, pgVersion string, enablePgbouncer bool) RunningEngineForTest { - ctx := context.Background() - - bridgeSupplied := bridgeNetworkName != "" - var bridgeNetwork *testcontainers.DockerNetwork - if enablePgbouncer && !bridgeSupplied { - // We will need a network bridge if we're running pgbouncer - bridgeNetworkName, bridgeNetwork = createNetworkBridge(t) - } - - postgresContainerHostname := "postgres-" + uuid.New().String() - - cmd := []string{"-N", PostgresTestMaxConnections} - if withCommitTimestamps { - cmd = append(cmd, "-c", "track_commit_timestamp=1") - } - - req := testcontainers.ContainerRequest{ - Name: postgresContainerHostname, - Image: "mirror.gcr.io/library/postgres:" + pgVersion, - ExposedPorts: []string{PostgresTestPort + "/tcp"}, - Env: map[string]string{ - "POSTGRES_USER": PostgresTestUser, - "POSTGRES_PASSWORD": PostgresTestPassword, - // use md5 auth to align postgres and pgbouncer auth methods - "POSTGRES_HOST_AUTH_METHOD": "md5", - "POSTGRES_INITDB_ARGS": "--auth=md5", - }, - Cmd: cmd, - WaitingFor: wait.ForLog("database system is ready to accept connections"). - WithOccurrence(2). - WithStartupTimeout(dockerBootTimeout), - } - - if bridgeNetworkName != "" { - req.Networks = []string{bridgeNetworkName} - } - - postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - require.NoError(t, err) - - mappedPort, err := postgres.MappedPort(ctx, PostgresTestPort) - require.NoError(t, err) +func RunPostgresForTestingWithCommitTimestamps(t testing.TB, targetMigration string, withCommitTimestamps bool, pgVersion string, enablePgbouncer bool) RunningEngineForTest { + t.Helper() builder := &postgresTester{ - container: container{ - hostHostname: "localhost", - hostPort: mappedPort.Port(), - containerHostname: postgresContainerHostname, - containerPort: PostgresTestPort, - }, - creds: PostgresTestUser + ":" + PostgresTestPassword, targetMigration: targetMigration, - useContainerHostname: bridgeSupplied, - pgContainer: postgres, } - t.Cleanup(func() { - require.NoError(t, postgres.Terminate(ctx)) - if bridgeNetwork != nil { - _ = bridgeNetwork.Remove(ctx) - } - }) - if enablePgbouncer { // if we are running with pgbouncer enabled then set it up - builder.runPgbouncerForTesting(t, bridgeNetworkName) + builder.runPgbouncerForTesting(t, pgVersion, withCommitTimestamps) + } else { + builder.runPostgresForTesting(t, pgVersion, withCommitTimestamps) } return builder @@ -129,7 +60,7 @@ func (b *postgresTester) NewDatabase(t testing.TB) string { newDBName := "db" + uniquePortion - ctx := context.Background() + ctx := t.Context() conn := b.initializeHostConnection(t) defer conn.Close(ctx) @@ -142,14 +73,10 @@ func (b *postgresTester) NewDatabase(t testing.TB) string { require.NoError(t, err) require.Equal(t, newDBName, dbName) - hostname, port := b.getHostnameAndPort() - return fmt.Sprintf( - "postgres://%s@%s:%s/%s?sslmode=disable", - b.creds, - hostname, - port, - newDBName, - ) + connUri, err := b.pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + return connUri } const ( @@ -158,6 +85,7 @@ const ( ) func (b *postgresTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore { + t.Helper() for i := 1; i <= retryCount; i++ { connectStr := b.NewDatabase(t) @@ -184,110 +112,121 @@ func (b *postgresTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore return nil } -func createNetworkBridge(t testing.TB) (string, *testcontainers.DockerNetwork) { - ctx := context.Background() - bridgeNetworkName := "bridge-" + uuid.New().String() - - net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{ - "name": bridgeNetworkName, - })) +// runPgbouncerForTesting stands up the network, the postgres container, and the pgbouncer container +// for a test run. +func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pgVersion string, withCommitTimestamps bool) { + t.Helper() + ctx := t.Context() + + // set up the network + testNetwork, err := network.New(ctx) require.NoError(t, err) + testcontainers.CleanupNetwork(t, testNetwork) - return net.Name, net -} + // set up the pg container + configFile := "./config/postgres.conf" + if withCommitTimestamps { + configFile = "./config/postgres-with-timestamps.conf" + } -func (b *postgresTester) runPgbouncerForTesting(t testing.TB, bridgeNetworkName string) { - ctx := context.Background() - uniqueID := uuid.New().String() - pgbouncerContainerHostname := "pgbouncer-" + uniqueID - - req := testcontainers.ContainerRequest{ - Name: pgbouncerContainerHostname, - Image: "mirror.gcr.io/edoburu/pgbouncer:latest", - ExposedPorts: []string{PgbouncerTestPort + "/tcp"}, - Env: map[string]string{ - "DB_USER": PostgresTestUser, - "DB_PASSWORD": PostgresTestPassword, - "DB_HOST": b.containerHostname, - "DB_PORT": b.containerPort, + image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) + pgContainer, err := postgres.Run(ctx, image, + testcontainers.WithEnv(map[string]string{ + // use md5 auth to align postgres and pgbouncer auth methods + "POSTGRES_HOST_AUTH_METHOD": "md5", + "POSTGRES_INITDB_ARGS": "--auth=md5", + }), + // contains the config for commit timestamps and max conns + postgres.WithConfigFile(configFile), + network.WithNetwork([]string{"postgres"}, testNetwork), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + b.pgContainer = pgContainer + + pgConnUri, err := pgContainer.ConnectionString(ctx) + require.NoError(t, err) + + // set up the bouncer container + bouncerContainer, err := testcontainers.Run(ctx, "mirror.gcr.io/edoburu/pgbouncer:latest", + testcontainers.WithEnv(map[string]string{ + "DATABASE_URL": pgConnUri, "LISTEN_PORT": PgbouncerTestPort, "DB_NAME": "*", // Needed to make pgbouncer okay with the randomly named databases generated by the test suite "AUTH_TYPE": "md5", // use the same auth type as postgres "MAX_CLIENT_CONN": PostgresTestMaxConnections, - }, - WaitingFor: wait.ForListeningPort(PgbouncerTestPort + "/tcp"). - WithStartupTimeout(dockerBootTimeout), - } - - if bridgeNetworkName != "" { - req.Networks = []string{bridgeNetworkName} - } - - pgbouncer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) + }), + testcontainers.WithExposedPorts(PgbouncerTestPort), + testcontainers.WithWaitStrategy(wait.ForListeningPort(PgbouncerTestPort + "/tcp"). + WithStartupTimeout(dockerBootTimeout)), + network.WithNetwork([]string{"pgbouncer"}, testNetwork), +) require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, pgbouncer.Terminate(ctx)) - }) + testcontainers.CleanupContainer(t, bouncerContainer) - mappedPort, err := pgbouncer.MappedPort(ctx, PgbouncerTestPort) require.NoError(t, err) + b.pgbouncerProxy = bouncerContainer +} - b.pgbouncerProxy = &container{ - hostHostname: "localhost", - hostPort: mappedPort.Port(), - containerHostname: pgbouncerContainerHostname, - containerPort: PgbouncerTestPort, +func (b *postgresTester) runPostgresForTesting(t testing.TB, pgVersion string, withCommitTimestamps bool) { + t.Helper() + ctx := t.Context() + configFile := "./config/postgres.conf" + if withCommitTimestamps { + configFile = "./config/postgres-with-timestamps.conf" } + + image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) + container, err := postgres.Run(ctx, image, + // contains the config for commit timestamps and max conns + postgres.WithConfigFile(configFile), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, container) + b.pgContainer = container + require.NoError(t, err) } func (b *postgresTester) initializeHostConnection(t testing.TB) (conn *pgx.Conn) { - hostname, port := b.getHostHostnameAndPort() - uri := fmt.Sprintf("postgresql://%s@%s:%s/?sslmode=disable", b.creds, hostname, port) - - // Retry connection - maxRetries := 10 - var err error - for i := 0; i < maxRetries; i++ { - ctx, cancelConnect := context.WithTimeout(context.Background(), dockerBootTimeout) - conn, err = pgx.Connect(ctx, uri) - cancelConnect() - if err == nil { - break - } + t.Helper() + ctx := t.Context() - if i == maxRetries-1 { - require.NoError(t, err) - } - time.Sleep(500 * time.Millisecond) + uri, err := b.pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + if b.pgbouncerProxy != nil { + // we have a pgbouncer instance; we want to use its port on the host so we + // find-and-replace the postgres port with the pgbouncer port + bouncerEndpoint, err := b.pgbouncerProxy.PortEndpoint(ctx, PgbouncerTestPort, "") + require.NoError(t, err) + + // do something similar for the pg port + // get the endpoint (hostname:port) for the pg container + pgEndpoint, err := b.pgContainer.PortEndpoint(ctx, "5432/tcp", "") + require.NoError(t, err) + + strings.Replace(uri, pgEndpoint, bouncerEndpoint, 1) } + conn, err = pgx.Connect(ctx, uri) + require.NoError(t, err) + return conn } -func (b *postgresTester) getHostnameAndPort() (string, string) { - // If a bridgeNetworkName is supplied then we will return the container - // hostname and port that is resolvable from within the container network. - // If bridgeNetworkName is not supplied then the hostname and port will be - // resolvable from the host. - if b.useContainerHostname { - return b.getContainerHostnameAndPort() - } - return b.getHostHostnameAndPort() +type pgBouncerWaitStrategy struct { + pollInterval time.Duration + startupTimeout time.Duration } +var _ wait.Strategy = (*pgBouncerWaitStrategy)(nil) -func (b *postgresTester) getHostHostnameAndPort() (string, string) { - if b.pgbouncerProxy != nil { - return b.pgbouncerProxy.hostHostname, b.pgbouncerProxy.hostPort - } - return b.hostHostname, b.hostPort -} -func (b *postgresTester) getContainerHostnameAndPort() (string, string) { - if b.pgbouncerProxy != nil { - return b.pgbouncerProxy.containerHostname, b.pgbouncerProxy.containerPort +func (p *pgBouncerWaitStrategy) WaitUntilReady(ctx context.Context, target wait.StrategyTarget) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("container not ready within the timeout: %w", ctx.Err()) + case <-time.After(500 * time.Millisecond): + } } - return b.containerHostname, b.containerPort } diff --git a/internal/testserver/datastore/spanner.go b/internal/testserver/datastore/spanner.go index 1b9910d09..524e1f5e8 100644 --- a/internal/testserver/datastore/spanner.go +++ b/internal/testserver/datastore/spanner.go @@ -11,7 +11,6 @@ import ( adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instances "cloud.google.com/go/spanner/admin/instance/apiv1" "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -28,44 +27,44 @@ type spannerTest struct { } // RunSpannerForTesting returns a RunningEngineForTest for spanner -func RunSpannerForTesting(t testing.TB, bridgeNetworkName string, targetMigration string) RunningEngineForTest { - ctx := context.Background() - name := "spanner-" + uuid.New().String() - - req := testcontainers.ContainerRequest{ - Name: name, - Image: "gcr.io/cloud-spanner-emulator/emulator:1.5.41", - ExposedPorts: []string{"9010/tcp"}, - WaitingFor: wait.ForListeningPort("9010/tcp").WithStartupTimeout(dockerBootTimeout), - } - - if bridgeNetworkName != "" { - req.Networks = []string{bridgeNetworkName} - } +func RunSpannerForTesting(t testing.TB, targetMigration string) RunningEngineForTest { + ctx := t.Context() + + container, err := testcontainers.Run(ctx, "gcr.io/cloud-spanner-emulator/emulator:1.5.41", + testcontainers.WithWaitStrategy(wait.ForListeningPort("9010/tcp").WithStartupTimeout(dockerBootTimeout), + &spannerWaitStrategy{}, +), + testcontainers.WithExposedPorts("9010/tcp"), +) + require.NoError(t, err) + testcontainers.CleanupContainer(t, container) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) + endpoint, err := container.PortEndpoint(ctx, "9010/tcp", "") require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + t.Setenv("SPANNER_EMULATOR_HOST", endpoint) - mappedPort, err := container.MappedPort(ctx, "9010") - require.NoError(t, err) + builder := &spannerTest{ + targetMigration: targetMigration, + } + return builder +} + +type spannerWaitStrategy struct {} - spannerEmulatorAddr := "localhost:" + mappedPort.Port() - t.Setenv("SPANNER_EMULATOR_HOST", spannerEmulatorAddr) +var _ wait.Strategy = (*spannerWaitStrategy)(nil) - // Retry initialization - maxRetries := 10 - for i := 0; i < maxRetries; i++ { - ctx, cancel := context.WithTimeout(context.Background(), dockerBootTimeout) - instancesClient, err := instances.NewInstanceAdminClient(ctx) - if err == nil { - ctx, cancel = context.WithTimeout(context.Background(), dockerBootTimeout) +func (s *spannerWaitStrategy) WaitUntilReady(ctx context.Context, target wait.StrategyTarget) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("container not ready within the timeout: %w", ctx.Err()) + case <-time.After(500 * time.Millisecond): + instancesClient, err := instances.NewInstanceAdminClient(ctx) + if err != nil { + // If we couldn't create the client we continue + continue + } _, err = instancesClient.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ Parent: "projects/fake-project-id", InstanceId: "init", @@ -76,28 +75,12 @@ func RunSpannerForTesting(t testing.TB, bridgeNetworkName string, targetMigratio }, }) instancesClient.Close() - cancel() if err == nil { + // If we successfully created an instance, we're done and we break break } - } else { - cancel() } - - if i == maxRetries-1 { - require.NoError(t, err) - } - time.Sleep(500 * time.Millisecond) - } - - builder := &spannerTest{ - targetMigration: targetMigration, } - if bridgeNetworkName != "" { - builder.hostname = name - } - - return builder } func (b *spannerTest) ExternalEnvVars() []string { @@ -112,7 +95,7 @@ func (b *spannerTest) NewDatabase(t testing.TB) string { newInstanceName := "fake-instance-" + uniquePortion - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() instancesClient, err := instances.NewInstanceAdminClient(ctx) From 100110fe76df00a5ecebf635166085cc47889edd Mon Sep 17 00:00:00 2001 From: Tanner Stirrat Date: Sat, 28 Feb 2026 11:12:14 -0700 Subject: [PATCH 4/4] chore: try using postgres testcontainer to wrap pgbouncer --- .../integration/migrate_integration_test.go | 10 +- .../schemawatch_integration_test.go | 13 ++- .../integration/serve_integration_test.go | 31 +++--- .../servetesting_integration_test.go | 3 +- .../integration/servetesting_race_test.go | 2 +- go.mod | 1 + go.sum | 2 + internal/datastore/crdb/crdb_test.go | 13 +-- internal/datastore/crdb/pool_test.go | 3 +- internal/datastore/mysql/datastore_test.go | 5 +- .../postgres/postgres_shared_test.go | 33 ++++--- internal/datastore/postgres/postgres_test.go | 2 +- internal/datastore/spanner/spanner_test.go | 2 +- internal/fdw/pgserver_e2e_test.go | 16 ++-- .../testserver/datastore/config/mysql.cnf | 2 + internal/testserver/datastore/crdb.go | 4 +- internal/testserver/datastore/datastore.go | 2 - internal/testserver/datastore/mysql.go | 89 +++++++---------- internal/testserver/datastore/postgres.go | 96 ++++++++----------- internal/testserver/datastore/spanner.go | 14 +-- 20 files changed, 150 insertions(+), 193 deletions(-) create mode 100644 internal/testserver/datastore/config/mysql.cnf diff --git a/cmd/spicedb/integration/migrate_integration_test.go b/cmd/spicedb/integration/migrate_integration_test.go index b208165eb..f8fd9dfec 100644 --- a/cmd/spicedb/integration/migrate_integration_test.go +++ b/cmd/spicedb/integration/migrate_integration_test.go @@ -1,4 +1,4 @@ -//go:build docker && image +// //go:build docker && image package integration_test @@ -41,7 +41,7 @@ func TestMigrate(t *testing.T) { t.Run(engineKey, func(t *testing.T) { engineKey := engineKey - r := testdatastore.RunDatastoreEngineWithBridge(t, engineKey, bridgeNetworkName) + r := testdatastore.RunDatastoreEngine(t, engineKey) db := r.NewDatabase(t) envVars := map[string]string{} @@ -55,6 +55,7 @@ func TestMigrate(t *testing.T) { } // Run the migrate command and wait for it to complete. + // TODO: container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "authzed/spicedb:ci", @@ -65,9 +66,7 @@ func TestMigrate(t *testing.T) { Started: true, }) require.NoError(t, err) - t.Cleanup(func() { - _ = container.Terminate(ctx) - }) + testcontainers.CleanupContainer(t, container) // Ensure the command completed successfully. state, err := container.State(ctx) @@ -76,6 +75,7 @@ func TestMigrate(t *testing.T) { if state.ExitCode != 0 { stream := new(bytes.Buffer) + // TODO: use logs logReader, lerr := container.Logs(ctx) require.NoError(t, lerr) defer logReader.Close() diff --git a/cmd/spicedb/integration/schemawatch_integration_test.go b/cmd/spicedb/integration/schemawatch_integration_test.go index 77d2907e7..e4a91a14f 100644 --- a/cmd/spicedb/integration/schemawatch_integration_test.go +++ b/cmd/spicedb/integration/schemawatch_integration_test.go @@ -1,4 +1,4 @@ -//go:build docker && image +// //go:build docker && image package integration_test @@ -45,7 +45,7 @@ func TestSchemaWatch(t *testing.T) { _ = net.Remove(ctx) }) - engine := testdatastore.RunDatastoreEngineWithBridge(t, driverName, bridgeNetworkName) + engine := testdatastore.RunDatastoreEngine(t, driverName) envVars := map[string]string{} if wev, ok := engine.(testdatastore.RunningEngineForTestWithEnvVars); ok { @@ -79,6 +79,7 @@ func TestSchemaWatch(t *testing.T) { require.Equal(t, 0, exitCode.ExitCode) // Run a serve and immediately close, ensuring it shuts down gracefully. + // TODO; serveContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "authzed/spicedb:ci", @@ -89,14 +90,13 @@ func TestSchemaWatch(t *testing.T) { Started: true, }) require.NoError(t, err) - t.Cleanup(func() { - _ = serveContainer.Terminate(ctx) - }) + testcontainers.CleanupContainer(t, serveContainer) ww := &watchingWriter{make(chan bool, 1), "starting watching cache"} // Grab logs and ensure schema watch has started before graceful shutdown. go (func() { + // TODO: do logging directly logReader, err := serveContainer.Logs(ctx) if err != nil { assert.NoError(t, err) @@ -107,6 +107,7 @@ func TestSchemaWatch(t *testing.T) { assert.NoError(t, err) })() + // TODO: what? select { case <-ww.c: break @@ -114,8 +115,6 @@ func TestSchemaWatch(t *testing.T) { case <-time.After(10 * time.Second): require.Fail(t, "timed out waiting for schema watch to run") } - - require.True(t, gracefulShutdown(ctx, serveContainer)) }) } } diff --git a/cmd/spicedb/integration/serve_integration_test.go b/cmd/spicedb/integration/serve_integration_test.go index 46af22108..21e336c29 100644 --- a/cmd/spicedb/integration/serve_integration_test.go +++ b/cmd/spicedb/integration/serve_integration_test.go @@ -1,4 +1,4 @@ -//go:build docker && image +// //go:build docker && image package integration_test @@ -26,11 +26,13 @@ import ( testdatastore "github.com/authzed/spicedb/internal/testserver/datastore" "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/migrate" ) func TestServe(t *testing.T) { requireParent := require.New(t) + // TODO: tester, err := newTester(t, testcontainers.ContainerRequest{ Image: "authzed/spicedb:ci", @@ -112,7 +114,7 @@ func gracefulShutdown(ctx context.Context, container testcontainers.Container) b } func TestGracefulShutdownInMemory(t *testing.T) { - ctx := context.Background() + ctx := t.Context() // Run a serve and immediately close, ensuring it shuts down gracefully. container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ @@ -154,17 +156,15 @@ func TestGracefulShutdown(t *testing.T) { for driverName, awaitGC := range engines { t.Run(driverName, func(t *testing.T) { - ctx := context.Background() - bridgeNetworkName := fmt.Sprintf("bridge-%s", uuid.New().String()) + ctx := t.Context() - // Create a bridge network for testing. - net, err := network.New(ctx, network.WithDriver("bridge"), network.WithLabels(map[string]string{"name": bridgeNetworkName})) + // Create a network for testing. + net, err := network.New(ctx) require.NoError(t, err) - t.Cleanup(func() { - _ = net.Remove(ctx) - }) + testcontainers.CleanupNetwork(t, net) - engine := testdatastore.RunDatastoreEngineWithBridge(t, driverName, bridgeNetworkName) + // TODO: figure out how to supply the network in this case. + engine := testdatastore.RunDatastoreEngine(t, driverName) envVars := map[string]string{} if wev, ok := engine.(testdatastore.RunningEngineForTestWithEnvVars); ok { @@ -177,20 +177,18 @@ func TestGracefulShutdown(t *testing.T) { } // Run the migrate command and wait for it to complete. + // TODO: probably handled by spicedb testcontainer db := engine.NewDatabase(t) migrateContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "authzed/spicedb:ci", Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, - Networks: []string{bridgeNetworkName}, Env: envVars, }, Started: true, }) require.NoError(t, err) - t.Cleanup(func() { - _ = migrateContainer.Terminate(ctx) - }) + testcontainers.CleanupContainer(t, migrateContainer) // Ensure the command completed successfully. exitCode, err := migrateContainer.State(ctx) @@ -202,15 +200,12 @@ func TestGracefulShutdown(t *testing.T) { ContainerRequest: testcontainers.ContainerRequest{ Image: "authzed/spicedb:ci", Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", ""}, - Networks: []string{bridgeNetworkName}, Env: envVars, }, Started: true, }) require.NoError(t, err) - t.Cleanup(func() { - _ = serveContainer.Terminate(ctx) - }) + testcontainers.CleanupContainer(t, serveContainer) if awaitGC { ww := &watchingWriter{make(chan bool, 1), "running garbage collection worker"} diff --git a/cmd/spicedb/integration/servetesting_integration_test.go b/cmd/spicedb/integration/servetesting_integration_test.go index cb9f6e59f..444e9d6bd 100644 --- a/cmd/spicedb/integration/servetesting_integration_test.go +++ b/cmd/spicedb/integration/servetesting_integration_test.go @@ -1,4 +1,4 @@ -//go:build docker && image +// //go:build docker && image package integration_test @@ -190,6 +190,7 @@ const retryCount = 8 func newTester(t *testing.T, containerReq testcontainers.ContainerRequest, token string, withExistingSchema bool) (*spicedbHandle, error) { ctx := context.Background() + // TODO: for i := 0; i < retryCount; i++ { container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: containerReq, diff --git a/cmd/spicedb/integration/servetesting_race_test.go b/cmd/spicedb/integration/servetesting_race_test.go index 64e4be90b..07f6dae9a 100644 --- a/cmd/spicedb/integration/servetesting_race_test.go +++ b/cmd/spicedb/integration/servetesting_race_test.go @@ -33,7 +33,7 @@ func TestCheckPermissionOnTesterNoFlakes(t *testing.T) { { HostFilePath: path.Join(basepath, "testdata/bootstrap.yaml"), ContainerFilePath: "/mnt/spicedb_bootstrap.yaml", - FileMode: 0644, + FileMode: 0o644, }, }, ExposedPorts: []string{"50051/tcp", "50052/tcp", "8443/tcp", "8444/tcp"}, diff --git a/go.mod b/go.mod index 10ebedd7f..fdfb9c95f 100644 --- a/go.mod +++ b/go.mod @@ -412,6 +412,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect diff --git a/go.sum b/go.sum index 0f5f2fab1..60cfda5a9 100644 --- a/go.sum +++ b/go.sum @@ -1719,6 +1719,8 @@ github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+ github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0 h1:UNYfrnFV9mkO93Sw6hqRA5KbE9DsAvDeYKD4GDConiE= github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.40.0/go.mod h1:O8By1J/1y726YYk7obTIXxfv2OzonVe+ORq9Z+K+fDg= +github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 h1:P9Txfy5Jothx2wFdcus0QoSmX/PKSIXZxrTbZPVJswA= +github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0/go.mod h1:oZPHHqJqXG7FD8OB/yWH7gLnDvZUlFHAVJNrGftL+eg= github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= diff --git a/internal/datastore/crdb/crdb_test.go b/internal/datastore/crdb/crdb_test.go index 4b812e133..2f144e547 100644 --- a/internal/datastore/crdb/crdb_test.go +++ b/internal/datastore/crdb/crdb_test.go @@ -76,8 +76,8 @@ func crdbTestVersion() string { func TestCRDBDatastoreWithoutIntegrity(t *testing.T) { t.Parallel() - b := testdatastore.RunCRDBForTesting(t, "", crdbTestVersion()) - test.All(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { + b := testdatastore.RunCRDBForTesting(t, crdbTestVersion()) + test.All(t, test.DatastoreTesterFunc(func(revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { ctx := context.Background() ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore { ds, err := NewCRDBDatastore( @@ -140,7 +140,7 @@ func TestCRDBDatastoreWithFollowerReads(t *testing.T) { followerReadDelay := time.Duration(4.8 * float64(time.Second)) gcWindow := 100 * time.Second - engine := testdatastore.RunCRDBForTesting(t, "", crdbTestVersion()) + engine := testdatastore.RunCRDBForTesting(t, crdbTestVersion()) quantizationDurations := []time.Duration{ 0 * time.Second, @@ -203,7 +203,7 @@ var defaultKeyForTesting = proxy.KeyConfig{ func TestCRDBDatastoreWithIntegrity(t *testing.T) { //nolint:tparallel t.Parallel() - b := testdatastore.RunCRDBForTesting(t, "", crdbTestVersion()) + b := testdatastore.RunCRDBForTesting(t, crdbTestVersion()) test.All(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { ctx := context.Background() @@ -303,7 +303,6 @@ func TestWatchFeatureDetection(t *testing.T) { }, } for _, tt := range cases { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) @@ -455,6 +454,7 @@ func newCRDBWithUser(t *testing.T) (adminConn *pgx.Conn, connStrings map[provisi })) require.NoError(t, rootCertFile.Close()) + // TODO: fix container, err := cockroachdb.Run(t.Context(), "cockroachdb/cockroach:v"+crdbTestVersion(), cockroachdb.WithInsecure(), @@ -479,8 +479,9 @@ func newCRDBWithUser(t *testing.T) (adminConn *pgx.Conn, connStrings map[provisi // Retry connection maxRetries := 10 + // TODO: require.EventuallyWithT for i := 0; i < maxRetries; i++ { - _, err = pgxpool.New(context.Background(), fmt.Sprintf("postgres://root@localhost:%[1]s/defaultdb?sslmode=verify-full&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir)) + _, err = pgxpool.New(t.Context(), fmt.Sprintf("postgres://root@localhost:%[1]s/defaultdb?sslmode=verify-full&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir)) if err == nil { break } diff --git a/internal/datastore/crdb/pool_test.go b/internal/datastore/crdb/pool_test.go index e1346fc41..e0f428049 100644 --- a/internal/datastore/crdb/pool_test.go +++ b/internal/datastore/crdb/pool_test.go @@ -25,7 +25,7 @@ const ( var testUserNS = namespace.Namespace(testUserNamespace) func TestTxReset(t *testing.T) { - b := testdatastore.RunCRDBForTesting(t, "", crdbTestVersion()) + b := testdatastore.RunCRDBForTesting(t, crdbTestVersion()) cases := []struct { name string @@ -97,7 +97,6 @@ func TestTxReset(t *testing.T) { }, } for _, tt := range cases { - tt := tt t.Run(tt.name, func(t *testing.T) { ctx := t.Context() diff --git a/internal/datastore/mysql/datastore_test.go b/internal/datastore/mysql/datastore_test.go index 3ecaff675..ea9ba28b4 100644 --- a/internal/datastore/mysql/datastore_test.go +++ b/internal/datastore/mysql/datastore_test.go @@ -121,14 +121,13 @@ func TestMySQLDatastoreDSNWithoutParseTime(t *testing.T) { } func TestMySQL8Datastore(t *testing.T) { - b := testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: true}, "") dst := datastoreTester{b: b} test.AllWithExceptions(t, test.DatastoreTesterFunc(dst.createDatastore), test.WithCategories(test.WatchSchemaCategory, test.WatchCheckpointsCategory), true) additionalMySQLTests(t, b) } func TestMySQLRevisionTimestamps(t *testing.T) { - b := testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: true}, "") + b := testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: true}) t.Run("TransactionTimestamps", createDatastoreTest(b, TransactionTimestampsTest, defaultOptions...)) } @@ -845,7 +844,7 @@ func TestMySQLWithAWSIAMCredentialsProvider(t *testing.T) { func datastoreDB(t *testing.T, migrate bool) *sql.DB { var databaseURI string - testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: migrate}, "").NewDatastore(t, func(engine, uri string) datastore.Datastore { + testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: migrate}).NewDatastore(t, func(engine, uri string) datastore.Datastore { databaseURI = uri return nil }) diff --git a/internal/datastore/postgres/postgres_shared_test.go b/internal/datastore/postgres/postgres_shared_test.go index 717e23ac3..28fc7cf8d 100644 --- a/internal/datastore/postgres/postgres_shared_test.go +++ b/internal/datastore/postgres/postgres_shared_test.go @@ -78,7 +78,7 @@ func testPostgresDatastore(t *testing.T, config postgresTestConfig) { } t.Run(fmt.Sprintf("%spostgres-%s-%s-%s-gc", pgbouncerStr, config.pgVersion, config.targetMigration, config.migrationPhase), func(t *testing.T) { - b := testdatastore.RunPostgresForTesting(t, "", config.targetMigration, config.pgVersion, config.pgbouncer) + b := testdatastore.RunPostgresForTesting(t, config.targetMigration, config.pgVersion, config.pgbouncer) ctx := context.Background() // NOTE: gc tests take exclusive locks, so they are run under non-parallel. @@ -116,8 +116,8 @@ func testPostgresDatastore(t *testing.T, config postgresTestConfig) { t.Run(fmt.Sprintf("%spostgres-%s-%s-%s", pgbouncerStr, config.pgVersion, config.targetMigration, config.migrationPhase), func(t *testing.T) { t.Parallel() - b := testdatastore.RunPostgresForTesting(t, "", config.targetMigration, config.pgVersion, config.pgbouncer) - ctx := context.Background() + b := testdatastore.RunPostgresForTesting(t, config.targetMigration, config.pgVersion, config.pgbouncer) + ctx := t.Context() test.AllWithExceptions(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, _, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore { @@ -313,8 +313,8 @@ func testPostgresDatastoreWithoutCommitTimestamps(t *testing.T, config postgresT t.Run(fmt.Sprintf("postgres-%s", pgVersion), func(t *testing.T) { t.Parallel() - ctx := context.Background() - b := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "", "head", false, pgVersion, enablePgbouncer) + ctx := t.Context() + b := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "head", false, pgVersion, enablePgbouncer) // NOTE: watch API requires the commit timestamps, so we skip those tests here. // NOTE: gc tests take exclusive locks, so they are run under non-parallel. @@ -336,9 +336,9 @@ func testPostgresDatastoreWithoutCommitTimestamps(t *testing.T, config postgresT }) t.Run(fmt.Sprintf("postgres-%s-gc", pgVersion), func(t *testing.T) { - ctx := context.Background() - b := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "", "head", false, pgVersion, enablePgbouncer) - test.OnlyGCTests(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { + ctx := t.Context() + b := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "head", false, pgVersion, enablePgbouncer) + test.OnlyGCTests(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore { ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID, RevisionQuantization(revisionQuantization), @@ -1443,8 +1443,8 @@ func OTelTracingTest(t *testing.T, ds datastore.Datastore) { func WatchNotEnabledTest(t *testing.T, _ testdatastore.RunningEngineForTest, pgVersion string) { require := require.New(t) - ds := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "", migrate.Head, false, pgVersion, false).NewDatastore(t, func(engine, uri string) datastore.Datastore { - ctx := context.Background() + ds := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, migrate.Head, false, pgVersion, false).NewDatastore(t, func(engine, uri string) datastore.Datastore { + ctx := t.Context() ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID, RevisionQuantization(0), @@ -1455,7 +1455,9 @@ func WatchNotEnabledTest(t *testing.T, _ testdatastore.RunningEngineForTest, pgV require.NoError(err) return ds }) - defer ds.Close() + t.Cleanup(func() { + ds.Close() + }) ds, revision := testfixtures.StandardDatastoreWithData(ds, require) _, errChan := ds.Watch( @@ -1472,8 +1474,8 @@ func BenchmarkPostgresQuery(b *testing.B) { b.StopTimer() req := require.New(b) - ds := testdatastore.RunPostgresForTesting(b, "", migrate.Head, pgversion.MinimumSupportedPostgresVersion, false).NewDatastore(b, func(engine, uri string) datastore.Datastore { - ctx := context.Background() + ds := testdatastore.RunPostgresForTesting(b, migrate.Head, pgversion.MinimumSupportedPostgresVersion, false).NewDatastore(b, func(engine, uri string) datastore.Datastore { + ctx := b.Context() ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID, RevisionQuantization(0), @@ -1508,10 +1510,11 @@ func BenchmarkPostgresQuery(b *testing.B) { } func datastoreWithInterceptorAndTestData(t *testing.T, interceptor pgcommon.QueryInterceptor, pgVersion string) datastore.Datastore { + t.Helper() require := require.New(t) - ds := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, "", migrate.Head, false, pgVersion, false).NewDatastore(t, func(engine, uri string) datastore.Datastore { - ctx := context.Background() + ds := testdatastore.RunPostgresForTestingWithCommitTimestamps(t, migrate.Head, false, pgVersion, false).NewDatastore(t, func(engine, uri string) datastore.Datastore { + ctx := t.Context() ds, err := newPostgresDatastore(ctx, uri, primaryInstanceID, RevisionQuantization(0), diff --git a/internal/datastore/postgres/postgres_test.go b/internal/datastore/postgres/postgres_test.go index 78c23b161..ad0ccccc0 100644 --- a/internal/datastore/postgres/postgres_test.go +++ b/internal/datastore/postgres/postgres_test.go @@ -49,7 +49,7 @@ func TestPostgresDatastoreGC(t *testing.T) { t.Run(fmt.Sprintf("%spostgres-gc-%s-%s-%s", pgbouncerStr, config.pgVersion, config.targetMigration, config.migrationPhase), func(t *testing.T) { t.Parallel() - b := testdatastore.RunPostgresForTesting(t, "", config.targetMigration, config.pgVersion, config.pgbouncer) + b := testdatastore.RunPostgresForTesting(t, config.targetMigration, config.pgVersion, config.pgbouncer) t.Run("GarbageCollection", createDatastoreTest( b, diff --git a/internal/datastore/spanner/spanner_test.go b/internal/datastore/spanner/spanner_test.go index ba03102e4..2aafc5f1d 100644 --- a/internal/datastore/spanner/spanner_test.go +++ b/internal/datastore/spanner/spanner_test.go @@ -29,7 +29,7 @@ func TestSpannerDatastore(t *testing.T) { // t.Parallel() //nolint:tparallel, the test sets environment variables (the emulator) ctx := context.Background() - b := testdatastore.RunSpannerForTesting(t, "", "head") + b := testdatastore.RunSpannerForTesting(t, "head") // Transaction tests are excluded because, for reasons unknown, one cannot read its own write in one transaction in the Spanner emulator. test.AllWithExceptions(t, test.DatastoreTesterFunc(func(_ testing.TB, revisionQuantization, _, _ time.Duration, watchBufferLength uint16) (datastore.Datastore, error) { diff --git a/internal/fdw/pgserver_e2e_test.go b/internal/fdw/pgserver_e2e_test.go index 095efa698..157758308 100644 --- a/internal/fdw/pgserver_e2e_test.go +++ b/internal/fdw/pgserver_e2e_test.go @@ -34,9 +34,9 @@ import ( ) const ( - pgVersion = "17.2" - postgresTestUser = "postgres" - fdwPassword = "proxypassword" + pgVersion = "17.2" + postgresTestUser = "postgres" + fdwPassword = "proxypassword" ) type qar struct { @@ -781,11 +781,11 @@ func runPostgres(t *testing.T) (conn *pgx.Conn) { logger := log.TestLogger(t) image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) container, err := postgres.Run(t.Context(), image, - testcontainers.WithLogger(logger), - postgres.WithUsername(postgresTestUser), - postgres.WithPassword(fdwPassword), - postgres.BasicWaitStrategies(), -) + testcontainers.WithLogger(logger), + postgres.WithUsername(postgresTestUser), + postgres.WithPassword(fdwPassword), + postgres.BasicWaitStrategies(), + ) require.NoError(t, err) connUri, err := container.ConnectionString(t.Context(), "sslmode=disable") diff --git a/internal/testserver/datastore/config/mysql.cnf b/internal/testserver/datastore/config/mysql.cnf new file mode 100644 index 000000000..4e89eaaa1 --- /dev/null +++ b/internal/testserver/datastore/config/mysql.cnf @@ -0,0 +1,2 @@ +[mysqld] +max_connections=500 diff --git a/internal/testserver/datastore/crdb.go b/internal/testserver/datastore/crdb.go index 10b1259fa..3a98af2e0 100644 --- a/internal/testserver/datastore/crdb.go +++ b/internal/testserver/datastore/crdb.go @@ -23,9 +23,9 @@ var _ RunningEngineForTest = (*crdbTester)(nil) func RunCRDBForTesting(t testing.TB, crdbVersion string) *crdbTester { ctx := t.Context() - container, err := cockroachdb.Run(ctx, "mirror.gcr.io/cockroachdb/cockroach:v"+crdbVersion, + container, err := cockroachdb.Run(ctx, "mirror.gcr.io/cockroachdb/cockroach:v"+crdbVersion, cockroachdb.WithInsecure(), -) + ) require.NoError(t, err) // enable changefeeds diff --git a/internal/testserver/datastore/datastore.go b/internal/testserver/datastore/datastore.go index d844e124f..c01293f26 100644 --- a/internal/testserver/datastore/datastore.go +++ b/internal/testserver/datastore/datastore.go @@ -5,8 +5,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - crdbversion "github.com/authzed/spicedb/internal/datastore/crdb/version" pgversion "github.com/authzed/spicedb/internal/datastore/postgres/version" "github.com/authzed/spicedb/pkg/datastore" diff --git a/internal/testserver/datastore/mysql.go b/internal/testserver/datastore/mysql.go index 28a3ab0a7..7e2682860 100644 --- a/internal/testserver/datastore/mysql.go +++ b/internal/testserver/datastore/mysql.go @@ -4,14 +4,13 @@ import ( "context" "database/sql" "fmt" - "strconv" + "strings" "testing" "time" - "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" + "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/authzed/spicedb/internal/datastore/mysql/migrations" "github.com/authzed/spicedb/internal/datastore/mysql/version" @@ -21,24 +20,23 @@ import ( ) const ( - mysqlPort = 3306 - defaultCreds = "root:secret" testDBPrefix = "spicedb_test_" + // This is the db name used for the dsn that's used to generate initial connections. + initialDB = "testdb" ) type mysqlTester struct { - db *sql.DB - hostname string - creds string - port string - options MySQLTesterOptions + // db is a connection used to create databases + db *sql.DB + // container is a reference to the mysql container instance + container *mysql.MySQLContainer + options MySQLTesterOptions } // MySQLTesterOptions allows tweaking the behaviour of the builder for the MySQL datastore type MySQLTesterOptions struct { Prefix string MigrateForNewDatastore bool - UseV8 bool } // RunMySQLForTesting returns a RunningEngineForTest for the mysql driver @@ -51,40 +49,22 @@ func RunMySQLForTesting(t testing.TB) RunningEngineForTest { // backed by a MySQL instance, while allowing options to be forwarded func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions) RunningEngineForTest { ctx := t.Context() - containerImageTag := version.MinimumSupportedMySQLVersion - - // TODO: add wait behavior - // TODO: simplify - - req := testcontainers.ContainerRequest{ - Image: "mirror.gcr.io/library/mysql:" + containerImageTag, - ExposedPorts: []string{fmt.Sprintf("%d/tcp", mysqlPort)}, - Env: map[string]string{ - "MYSQL_ROOT_PASSWORD": "secret", - }, - // increase max connections (default 151) to accommodate tests using the same docker container - Cmd: []string{"--max-connections=500"}, - WaitingFor: wait.ForLog("port: 3306 MySQL Community Server"). - WithStartupTimeout(dockerBootTimeout), - } - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) + image := "mirror.gcr.io/library/mysql:" + version.MinimumSupportedMySQLVersion + container, err := mysql.Run(ctx, + image, + mysql.WithConfigFile("./config/mysql.cnf"), + mysql.WithDatabase(initialDB), + ) require.NoError(t, err) builder := &mysqlTester{ - creds: defaultCreds, - options: options, + options: options, + container: container, } - mappedPort, err := container.MappedPort(ctx, "3306") + dsn, err := container.ConnectionString(ctx, "parseTime=true") require.NoError(t, err) - - builder.port = mappedPort.Port() - - dsn := fmt.Sprintf("%s@(localhost:%s)/mysql?parseTime=true", builder.creds, port) builder.db, err = sql.Open("mysql", dsn) require.NoError(t, err) @@ -100,7 +80,12 @@ func (mb *mysqlTester) NewDatabase(t testing.TB) string { _, err = mb.db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)) require.NoError(t, err, "failed to create database %s: %s", dbName, err) - return fmt.Sprintf("%s@(%s:%s)/%s?parseTime=true", mb.creds, mb.hostname, mb.port, dbName) + dsn, err := mb.container.ConnectionString(t.Context(), "parseTime=true") + require.NoError(t, err) + + strings.Replace(dsn, initialDB, dbName, 1) + + return dsn } func (mb *mysqlTester) runMigrate(t testing.TB, dsn string) error { @@ -118,23 +103,13 @@ func (mb *mysqlTester) runMigrate(t testing.TB, dsn string) error { } func (mb *mysqlTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore { - for i := 1; i <= retryCount; i++ { - dsn := mb.NewDatabase(t) + var dsn string + require.EventuallyWithT(t, func(collect *assert.CollectT) { + dsn = mb.NewDatabase(t) if mb.options.MigrateForNewDatastore { - if err := mb.runMigrate(t, dsn); err != nil { - if i == retryCount { - require.NoError(t, err, "failed to run migration") - } else { - t.Logf("failed to run migration: %v, retrying... %d", err, i) - } - time.Sleep(time.Duration(i) * timeBetweenRetries) - continue - } + err := mb.runMigrate(t, dsn) + assert.NoError(collect, err) } - - return initFunc("mysql", dsn) - } - - require.Fail(t, "failed to create datastore for testing") - return nil + }, 5*time.Second, 500*time.Millisecond) + return initFunc("mysql", dsn) } diff --git a/internal/testserver/datastore/postgres.go b/internal/testserver/datastore/postgres.go index bfc297ca5..f35a7c85e 100644 --- a/internal/testserver/datastore/postgres.go +++ b/internal/testserver/datastore/postgres.go @@ -3,13 +3,14 @@ package datastore import ( "context" "fmt" - "strings" "testing" "time" "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" @@ -23,13 +24,14 @@ import ( const ( // NOTE: this is used in this file but also duplicated in postgres.conf. PostgresTestMaxConnections = "3000" - PgbouncerTestPort = "6432" + PgTestPass = "testpass" + PgTestUser = "testuser" ) type postgresTester struct { - targetMigration string - pgbouncerProxy *testcontainers.DockerContainer - pgContainer *postgres.PostgresContainer + targetMigration string + pgbouncerProxy *postgres.PostgresContainer + pgContainer *postgres.PostgresContainer } // RunPostgresForTesting returns a RunningEngineForTest for postgres @@ -41,7 +43,7 @@ func RunPostgresForTestingWithCommitTimestamps(t testing.TB, targetMigration str t.Helper() builder := &postgresTester{ - targetMigration: targetMigration, + targetMigration: targetMigration, } if enablePgbouncer { @@ -79,37 +81,24 @@ func (b *postgresTester) NewDatabase(t testing.TB) string { return connUri } -const ( - retryCount = 4 - timeBetweenRetries = 1 * time.Second -) - func (b *postgresTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore { t.Helper() - for i := 1; i <= retryCount; i++ { - connectStr := b.NewDatabase(t) + ctx := context.WithValue(t.Context(), migrate.BackfillBatchSize, uint64(1000)) + + var uri string - migrationDriver, err := pgmigrations.NewAlembicPostgresDriver(context.Background(), connectStr, datastore.NoCredentialsProvider, false) - if err == nil { - ctx := context.WithValue(context.Background(), migrate.BackfillBatchSize, uint64(1000)) - require.NoError(t, pgmigrations.DatabaseMigrations.Run(ctx, migrationDriver, b.targetMigration, migrate.LiveRun)) - return initFunc("postgres", connectStr) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + connectStr := b.NewDatabase(t) + migrationDriver, err := pgmigrations.NewAlembicPostgresDriver(ctx, connectStr, datastore.NoCredentialsProvider, false) + if assert.NoError(collect, err) { + assert.NoError(collect, pgmigrations.DatabaseMigrations.Run(ctx, migrationDriver, b.targetMigration, migrate.LiveRun)) } defer func() { - migrationDriver.Close(context.Background()) + migrationDriver.Close(ctx) }() + }, 5*time.Second, 500*time.Millisecond) - if i == retryCount { - require.NoError(t, err, "got error when trying to create migration driver") - } else { - t.Logf("failed to create migration driver: %v, retrying... %d", err, i) - } - - time.Sleep(time.Duration(i) * timeBetweenRetries) - } - - require.Fail(t, "failed to create datastore for testing") - return nil + return initFunc("postgres", uri) } // runPgbouncerForTesting stands up the network, the postgres container, and the pgbouncer container @@ -117,7 +106,7 @@ func (b *postgresTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pgVersion string, withCommitTimestamps bool) { t.Helper() ctx := t.Context() - + // set up the network testNetwork, err := network.New(ctx) require.NoError(t, err) @@ -130,7 +119,7 @@ func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pgVersion string, } image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) - pgContainer, err := postgres.Run(ctx, image, + pgContainer, err := postgres.Run(ctx, image, testcontainers.WithEnv(map[string]string{ // use md5 auth to align postgres and pgbouncer auth methods "POSTGRES_HOST_AUTH_METHOD": "md5", @@ -148,19 +137,19 @@ func (b *postgresTester) runPgbouncerForTesting(t testing.TB, pgVersion string, require.NoError(t, err) // set up the bouncer container - bouncerContainer, err := testcontainers.Run(ctx, "mirror.gcr.io/edoburu/pgbouncer:latest", - testcontainers.WithEnv(map[string]string{ - "DATABASE_URL": pgConnUri, - "LISTEN_PORT": PgbouncerTestPort, - "DB_NAME": "*", // Needed to make pgbouncer okay with the randomly named databases generated by the test suite - "AUTH_TYPE": "md5", // use the same auth type as postgres - "MAX_CLIENT_CONN": PostgresTestMaxConnections, + bouncerContainer, err := postgres.Run(ctx, "mirror.gcr.io/edoburu/pgbouncer:latest", + testcontainers.WithLogger(log.TestLogger(t)), + testcontainers.WithEnv(map[string]string{ + "DATABASE_URL": pgConnUri, + "DB_NAME": "*", // Needed to make pgbouncer okay with the randomly named databases generated by the test suite + "DB_PASSWORD": PgTestPass, + "DB_USER": PgTestUser, + "AUTH_TYPE": "md5", // use the same auth type as postgres + "MAX_CLIENT_CONN": PostgresTestMaxConnections, }), - testcontainers.WithExposedPorts(PgbouncerTestPort), - testcontainers.WithWaitStrategy(wait.ForListeningPort(PgbouncerTestPort + "/tcp"). - WithStartupTimeout(dockerBootTimeout)), - network.WithNetwork([]string{"pgbouncer"}, testNetwork), -) + // TODO: these may need to change based on logs. see what this container actually logs. + postgres.BasicWaitStrategies(), + ) require.NoError(t, err) testcontainers.CleanupContainer(t, bouncerContainer) @@ -177,9 +166,11 @@ func (b *postgresTester) runPostgresForTesting(t testing.TB, pgVersion string, w } image := fmt.Sprintf("mirror.gcr.io/library/postgres:%s", pgVersion) - container, err := postgres.Run(ctx, image, + container, err := postgres.Run(ctx, image, // contains the config for commit timestamps and max conns postgres.WithConfigFile(configFile), + postgres.WithUsername(PgTestUser), + postgres.WithPassword(PgTestPass), postgres.BasicWaitStrategies(), ) testcontainers.CleanupContainer(t, container) @@ -195,17 +186,8 @@ func (b *postgresTester) initializeHostConnection(t testing.TB) (conn *pgx.Conn) require.NoError(t, err) if b.pgbouncerProxy != nil { - // we have a pgbouncer instance; we want to use its port on the host so we - // find-and-replace the postgres port with the pgbouncer port - bouncerEndpoint, err := b.pgbouncerProxy.PortEndpoint(ctx, PgbouncerTestPort, "") + uri, err = b.pgbouncerProxy.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err) - - // do something similar for the pg port - // get the endpoint (hostname:port) for the pg container - pgEndpoint, err := b.pgContainer.PortEndpoint(ctx, "5432/tcp", "") - require.NoError(t, err) - - strings.Replace(uri, pgEndpoint, bouncerEndpoint, 1) } conn, err = pgx.Connect(ctx, uri) @@ -215,11 +197,11 @@ func (b *postgresTester) initializeHostConnection(t testing.TB) (conn *pgx.Conn) } type pgBouncerWaitStrategy struct { - pollInterval time.Duration + pollInterval time.Duration startupTimeout time.Duration } -var _ wait.Strategy = (*pgBouncerWaitStrategy)(nil) +var _ wait.Strategy = (*pgBouncerWaitStrategy)(nil) func (p *pgBouncerWaitStrategy) WaitUntilReady(ctx context.Context, target wait.StrategyTarget) error { for { diff --git a/internal/testserver/datastore/spanner.go b/internal/testserver/datastore/spanner.go index 524e1f5e8..f36d3462f 100644 --- a/internal/testserver/datastore/spanner.go +++ b/internal/testserver/datastore/spanner.go @@ -31,11 +31,11 @@ func RunSpannerForTesting(t testing.TB, targetMigration string) RunningEngineFor ctx := t.Context() container, err := testcontainers.Run(ctx, "gcr.io/cloud-spanner-emulator/emulator:1.5.41", - testcontainers.WithWaitStrategy(wait.ForListeningPort("9010/tcp").WithStartupTimeout(dockerBootTimeout), - &spannerWaitStrategy{}, -), - testcontainers.WithExposedPorts("9010/tcp"), -) + testcontainers.WithWaitStrategy(wait.ForListeningPort("9010/tcp").WithStartupTimeout(dockerBootTimeout), + &spannerWaitStrategy{}, + ), + testcontainers.WithExposedPorts("9010/tcp"), + ) require.NoError(t, err) testcontainers.CleanupContainer(t, container) @@ -50,7 +50,7 @@ func RunSpannerForTesting(t testing.TB, targetMigration string) RunningEngineFor return builder } -type spannerWaitStrategy struct {} +type spannerWaitStrategy struct{} var _ wait.Strategy = (*spannerWaitStrategy)(nil) @@ -61,7 +61,7 @@ func (s *spannerWaitStrategy) WaitUntilReady(ctx context.Context, target wait.St return fmt.Errorf("container not ready within the timeout: %w", ctx.Err()) case <-time.After(500 * time.Millisecond): instancesClient, err := instances.NewInstanceAdminClient(ctx) - if err != nil { + if err != nil { // If we couldn't create the client we continue continue }