From 49d3ae3b658693edc7177810be0e75d23724dcce Mon Sep 17 00:00:00 2001 From: MattiasMTS Date: Mon, 17 Feb 2025 22:33:19 +0100 Subject: [PATCH 1/3] feat(mssql): add WithInitSQL function --- modules/mssql/mssql.go | 54 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/modules/mssql/mssql.go b/modules/mssql/mssql.go index 17337bf85b..7044abb784 100644 --- a/modules/mssql/mssql.go +++ b/modules/mssql/mssql.go @@ -3,9 +3,11 @@ package mssql import ( "context" "fmt" + "io" "strings" "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/wait" ) @@ -41,6 +43,58 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { } } +// WithInitSQL adds SQL scripts to be executed after the container is ready. +// The scripts are executed in the order they are provided using sqlcmd tool. +func WithInitSQL(files ...io.Reader) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + hooks := make([]testcontainers.ContainerHook, 0, len(files)) + + for i, script := range files { + content, err := io.ReadAll(script) + if err != nil { + return fmt.Errorf("failed to read script: %w", err) + } + + hook := func(ctx context.Context, c testcontainers.Container) error { + password := defaultPassword + if req.Env["MSSQL_SA_PASSWORD"] != "" { + password = req.Env["MSSQL_SA_PASSWORD"] + } + + // targetPath is a dummy path to store the script in the container + targetPath := "/tmp/" + fmt.Sprintf("script_%d.sql", i) + if err := c.CopyToContainer(ctx, content, targetPath, 0o644); err != nil { + return fmt.Errorf("failed to copy script to container: %w", err) + } + + // NOTE: we add both legacy and new mssql-tools paths to ensure compatibility + envOpts := tcexec.WithEnv([]string{ + "PATH=/opt/mssql-tools18/bin:/opt/mssql-tools/bin:$PATH", + }) + cmd := []string{ + "sqlcmd", + "-S", "localhost", + "-U", defaultUsername, + "-P", password, + "-No", + "-i", targetPath, + } + if _, _, err := c.Exec(ctx, cmd, envOpts); err != nil { + return fmt.Errorf("failed to execute SQL script %q using sqlcmd: %w", targetPath, err) + } + return nil + } + hooks = append(hooks, hook) + } + + req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ + PostReadies: hooks, + }) + + return nil + } +} + // Deprecated: use Run instead // RunContainer creates an instance of the MSSQLServer container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MSSQLServerContainer, error) { From b4395383d12c8640506b74fd05a889784f502974 Mon Sep 17 00:00:00 2001 From: MattiasMTS Date: Mon, 17 Feb 2025 22:33:42 +0100 Subject: [PATCH 2/3] test(mssql): add test for WithInitSQL --- modules/mssql/mssql_test.go | 83 +++++++++++++++++++++++++++++++++ modules/mssql/testdata/seed.sql | 14 ++++++ 2 files changed, 97 insertions(+) create mode 100644 modules/mssql/testdata/seed.sql diff --git a/modules/mssql/mssql_test.go b/modules/mssql/mssql_test.go index 737c97414e..fffd977355 100644 --- a/modules/mssql/mssql_test.go +++ b/modules/mssql/mssql_test.go @@ -1,8 +1,10 @@ package mssql_test import ( + "bytes" "context" "database/sql" + _ "embed" "testing" _ "github.com/microsoft/go-mssqldb" @@ -128,3 +130,84 @@ func TestMSSQLServerWithInvalidPassword(t *testing.T) { testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) } + +//go:embed testdata/seed.sql +var seedSQLContent []byte + +// tests that a container can be created with a DDL script +func TestMSSQLServerWithScriptsDDL(t *testing.T) { + const password = "MyCustom@Passw0rd" + + // assertContainer contains the logic for asserting the test + assertContainer := func(t *testing.T, ctx context.Context, image string, options ...testcontainers.ContainerCustomizer) { + t.Helper() + + ctr, err := mssql.Run(ctx, + image, + append([]testcontainers.ContainerCustomizer{mssql.WithAcceptEULA()}, options...)..., + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) + + db, err := sql.Open("sqlserver", connectionString) + require.NoError(t, err) + defer db.Close() + + err = db.PingContext(ctx) + require.NoError(t, err) + + rows, err := db.QueryContext(ctx, "SELECT * FROM pizza_palace.pizzas") + require.NoError(t, err) + defer rows.Close() + + type Pizza struct { + ID int + ToppingName string + Deliciousness string + } + + want := []Pizza{ + {1, "Pineapple", "Controversial but tasty"}, + {2, "Pepperoni", "Classic never fails"}, + } + got := make([]Pizza, 0, len(want)) + + for rows.Next() { + var p Pizza + err := rows.Scan(&p.ID, &p.ToppingName, &p.Deliciousness) + require.NoError(t, err) + got = append(got, p) + } + + require.EqualValues(t, want, got) + } + + ctx := context.Background() + + t.Run("WithPassword/beforeWithScripts", func(t *testing.T) { + assertContainer(t, ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", + mssql.WithPassword(password), + mssql.WithInitSQL(bytes.NewReader(seedSQLContent)), + ) + }) + + t.Run("WithPassword/afterWithScripts", func(t *testing.T) { + assertContainer(t, ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", + mssql.WithInitSQL(bytes.NewReader(seedSQLContent)), + mssql.WithPassword(password), + ) + }) + + t.Run("2019-CU30-ubuntu-20.04/oldSQLCmd", func(t *testing.T) { + assertContainer(t, ctx, + "mcr.microsoft.com/mssql/server:2019-CU30-ubuntu-20.04", + mssql.WithPassword(password), + mssql.WithInitSQL(bytes.NewReader(seedSQLContent)), + ) + }) +} diff --git a/modules/mssql/testdata/seed.sql b/modules/mssql/testdata/seed.sql new file mode 100644 index 0000000000..b4a0128af2 --- /dev/null +++ b/modules/mssql/testdata/seed.sql @@ -0,0 +1,14 @@ +CREATE SCHEMA pizza_palace; +GO + +CREATE TABLE pizza_palace.pizzas ( + ID INT PRIMARY KEY IDENTITY, + ToppingName NVARCHAR(100), + Deliciousness NVARCHAR(100) UNIQUE +); +GO + +INSERT INTO pizza_palace.pizzas (ToppingName, Deliciousness) VALUES + ('Pineapple', 'Controversial but tasty'), + ('Pepperoni', 'Classic never fails') +GO From 8494e1ce6c0ad240e35ae607b328968d9c3c4957 Mon Sep 17 00:00:00 2001 From: MattiasMTS Date: Tue, 18 Feb 2025 15:23:53 +0100 Subject: [PATCH 3/3] docs(mssql): add docs for WithInitSQL --- docs/modules/mssql.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/modules/mssql.md b/docs/modules/mssql.md index d7c09f02d9..906bb65fb9 100644 --- a/docs/modules/mssql.md +++ b/docs/modules/mssql.md @@ -48,6 +48,23 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom When starting the MS SQL Server container, you can pass options in a variadic way to configure it. +#### Init Scripts + +- Not available until the next release of testcontainers-go :material-tag: main + +If you need to execute SQL files when the container starts, you can use `mssql.WithInitSQL(files +...io.Reader)` with one or more `*.sql` files. The files will be executed in order after the +container is ready. + + +[Example of SQL script](../../modules/mssql/testdata/seed.sql) + + +This will: + +1. Copy each file into the container. +2. Execute them using `sqlcmd` after the container is ready. + #### Image If you need to set a different MS SQL Server Docker image, you can set a valid Docker image as the second argument in the `Run` function.