diff --git a/modules/postgres/postgres.go b/modules/postgres/postgres.go index 1f54add722..fd00877429 100644 --- a/modules/postgres/postgres.go +++ b/modules/postgres/postgres.go @@ -94,21 +94,40 @@ func WithDatabase(dbName string) testcontainers.CustomizeRequestOption { } } -// WithInitScripts sets the init scripts to be run when the container starts +// WithInitScripts sets the init scripts to be run when the container starts. +// These init scripts will be executed in sorted name order as defined by the container's current locale, which defaults to en_US.utf8. +// If you need to run your scripts in a specific order, consider using `WithOrderedInitScripts` instead. func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { return func(req *testcontainers.GenericContainerRequest) error { - initScripts := []testcontainers.ContainerFile{} + containerFiles := []testcontainers.ContainerFile{} for _, script := range scripts { - cf := testcontainers.ContainerFile{ - HostFilePath: script, - ContainerFilePath: "/docker-entrypoint-initdb.d/" + filepath.Base(script), - FileMode: 0o755, - } - initScripts = append(initScripts, cf) + initScript := buildInitScriptContainerFile(script, filepath.Base(script)) + containerFiles = append(containerFiles, initScript) } - req.Files = append(req.Files, initScripts...) + return testcontainers.WithFiles(containerFiles...)(req) + } +} - return nil +// WithOrderedInitScripts sets the init scripts to be run when the container starts. +// The scripts will be run in the order that they are provided in this function. +func WithOrderedInitScripts(scripts ...string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + containerFiles := []testcontainers.ContainerFile{} + for idx, script := range scripts { + containerFilePath := fmt.Sprintf("%03d-%s", idx, filepath.Base(script)) + initScript := buildInitScriptContainerFile(script, containerFilePath) + containerFiles = append(containerFiles, initScript) + } + return testcontainers.WithFiles(containerFiles...)(req) + } +} + +// Adds the file located at hostFilePath as an init script that postgres will launch +func buildInitScriptContainerFile(hostFilePath string, containerFilePath string) testcontainers.ContainerFile { + return testcontainers.ContainerFile{ + HostFilePath: hostFilePath, + ContainerFilePath: "/docker-entrypoint-initdb.d/" + containerFilePath, + FileMode: 0o755, } } diff --git a/modules/postgres/postgres_test.go b/modules/postgres/postgres_test.go index e83b8e1454..88ecda8d30 100644 --- a/modules/postgres/postgres_test.go +++ b/modules/postgres/postgres_test.go @@ -5,8 +5,10 @@ import ( "database/sql" "errors" "fmt" + "io" "os" "path/filepath" + "strings" "testing" "time" @@ -19,6 +21,7 @@ import ( "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) @@ -288,6 +291,60 @@ func TestWithInitScript(t *testing.T) { require.NotNil(t, result) } +func TestWithOrderedInitScript(t *testing.T) { + ctx := context.Background() + + ctr, err := postgres.Run(ctx, + "postgres:15.2-alpine", + // Executes first the init-user-db shell-script, then the do-insert-user SQL script + // Using WithInitScripts, this would not work. + // This is because aaaa-insert-user would get executed first, but requires init-user-db to be executed before. + postgres.WithOrderedInitScripts( + filepath.Join("testdata", "init-user-db.sh"), + filepath.Join("testdata", "aaaa-insert-user.sql"), + ), + postgres.WithDatabase(dbname), + postgres.WithUsername(user), + postgres.WithPassword(password), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // Test that init scripts have been correctly renamed + c, reader, err := ctr.Exec(ctx, []string{"ls", "-l", "/docker-entrypoint-initdb.d"}, tcexec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, c, "Expected to read init scripts from the container") + + buf := new(strings.Builder) + _, err = io.Copy(buf, reader) + require.NoError(t, err) + + initScripts := buf.String() + strings.Contains(initScripts, "000-init-user-db.sh") + strings.Contains(initScripts, "001-aaaa-insert-user.sql") + + // explicitly set sslmode=disable because the container is not configured to use TLS + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + require.NotNil(t, db) + defer db.Close() + + // database created in init script. See testdata/init-user-db.sh + rows, err := db.Query("SELECT COUNT(*) FROM testdb;") + require.NoError(t, err) + require.NotNil(t, rows) + for rows.Next() { + var count int + err := rows.Scan(&count) + require.NoError(t, err) + require.Equal(t, 2, count) + } +} + func TestSnapshot(t *testing.T) { tests := []struct { name string diff --git a/modules/postgres/testdata/aaaa-insert-user.sql b/modules/postgres/testdata/aaaa-insert-user.sql new file mode 100644 index 0000000000..687937f374 --- /dev/null +++ b/modules/postgres/testdata/aaaa-insert-user.sql @@ -0,0 +1,5 @@ +-- Do not rename this file, it is named like this +-- to test correct ordering behavior. +-- +-- See TestWithOrderedInitScripts. +INSERT INTO testdb (id, name) VALUES (2, 'second user')