diff --git a/docs/modules/azure.md b/docs/modules/azure.md index 695e0efeb8..49e8bfe821 100644 --- a/docs/modules/azure.md +++ b/docs/modules/azure.md @@ -23,7 +23,7 @@ The Azure module exposes the following Go packages: - [ServiceBus](#servicebus): `github.com/testcontainers/testcontainers-go/modules/azure/servicebus`. !!! warning "EULA Acceptance" Due to licensing restrictions you are required to explicitly accept an End User License Agreement (EULA) for the EventHubs container image. This is facilitated through the `WithAcceptEULA` function. - +- [CosmosDB](#cosmosdb): `github.com/testcontainers/testcontainers-go/modules/azure/cosmosdb`. [Creating a Azurite container](../../modules/azure/azurite/examples_test.go) inside_block:runAzuriteContainer @@ -307,4 +307,49 @@ In the following example, inspired by the [Azure Event Hubs Go SDK](https://lear [Create Client](../../modules/azure/servicebus/examples_test.go) inside_block:createClient [Send messages to a Queue](../../modules/azure/servicebus/examples_test.go) inside_block:sendMessages [Receive messages from a Queue](../../modules/azure/servicebus/examples_test.go) inside_block:receiveMessages - \ No newline at end of file + + +## CosmosDB + +### Run function + +- Not available until the next release :material-tag: main + +The CosmosDB module exposes one entrypoint function to create the CosmosDB container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +#### Image + +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")`. + +### Container Options + +When starting the CosmosDB container, you can pass options in a variadic way to configure it. + +{% include "../features/common_functional_options_list.md" %} + +### Container Methods + +The CosmosDB container exposes the following methods: + +#### ConnectionString + +- Not available until the next release :material-tag: main + +Returns the connection string to connect to the CosmosDB container and an error, passing the Go context as parameter. + +### Examples + +#### Connect and Create database + + +[Connect_CreateDatabase](../../modules/azure/cosmosdb/examples_test.go) inside_block:ExampleRun_connect + diff --git a/modules/azure/cosmosdb/cosmosdb.go b/modules/azure/cosmosdb/cosmosdb.go new file mode 100644 index 0000000000..5c5457cd8c --- /dev/null +++ b/modules/azure/cosmosdb/cosmosdb.go @@ -0,0 +1,69 @@ +package cosmosdb + +import ( + "context" + "fmt" + + "github.com/docker/go-connections/nat" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + defaultPort = "8081/tcp" + defaultProtocol = "http" + + // Well-known, publicly documented account key for the Azure CosmosDB Emulator. + // See: https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-develop-emulator + testAccKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" +) + +// Container represents the CosmosDB container type used in the module +type Container struct { + testcontainers.Container +} + +// Run creates an instance of the CosmosDB container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + // Initialize with module defaults + moduleOpts := []testcontainers.ContainerCustomizer{ + testcontainers.WithExposedPorts(defaultPort), + testcontainers.WithCmdArgs("--enable-explorer", "false"), + testcontainers.WithWaitStrategy( + wait.ForAll( + wait.ForLog("Started"), + wait.ForListeningPort(nat.Port(defaultPort)), + ), + ), + } + + // Add user-provided options + moduleOpts = append(moduleOpts, opts...) + + ctr, err := testcontainers.Run(ctx, img, moduleOpts...) + var c *Container + if ctr != nil { + c = &Container{Container: ctr} + } + + if err != nil { + return c, fmt.Errorf("run cosmosdb: %w", err) + } + + return c, nil +} + +// ConnectionString returns a connection string that can be used to connect to the CosmosDB emulator. +// The connection string includes the account endpoint (host:port) and the default test account key. +// It returns an error if the port endpoint cannot be determined. +// +// Format: "AccountEndpoint=:;AccountKey=" +func (c *Container) ConnectionString(ctx context.Context) (string, error) { + endpoint, err := c.PortEndpoint(ctx, defaultPort, defaultProtocol) + if err != nil { + return "", fmt.Errorf("port endpoint: %w", err) + } + + return fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s;", endpoint, testAccKey), nil +} diff --git a/modules/azure/cosmosdb/cosmosdb_test.go b/modules/azure/cosmosdb/cosmosdb_test.go new file mode 100644 index 0000000000..9d2bcdc4a7 --- /dev/null +++ b/modules/azure/cosmosdb/cosmosdb_test.go @@ -0,0 +1,77 @@ +package cosmosdb_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/azure/cosmosdb" +) + +func TestCosmosDB(t *testing.T) { + ctx := context.Background() + + ctr, err := cosmosdb.Run(ctx, "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // Create Azure Cosmos client + connStr, err := ctr.ConnectionString(ctx) + require.NoError(t, err) + require.NotNil(t, connStr) + + p, err := cosmosdb.NewContainerPolicy(ctx, ctr) + require.NoError(t, err) + + client, err := azcosmos.NewClientFromConnectionString(connStr, p.ClientOptions()) + require.NoError(t, err) + require.NotNil(t, client) + + // Create database + createDatabaseResp, err := client.CreateDatabase(ctx, azcosmos.DatabaseProperties{ID: "myDatabase"}, nil) + require.NoError(t, err) + require.NotNil(t, createDatabaseResp) + + dbClient, err := client.NewDatabase("myDatabase") + require.NoError(t, err) + require.NotNil(t, dbClient) + + // Create container + containerProps := azcosmos.ContainerProperties{ + ID: "myContainer", + PartitionKeyDefinition: azcosmos.PartitionKeyDefinition{Paths: []string{"/category"}}, + } + createContainerResp, err := dbClient.CreateContainer(ctx, containerProps, nil) + require.NoError(t, err) + require.NotNil(t, createContainerResp) + containerClient, err := dbClient.NewContainer("myContainer") + require.NoError(t, err) + require.NotNil(t, containerClient) + + // Create item + type Product struct { + ID string `json:"id"` + Category string `json:"category"` + Name string `json:"name"` + } + + testItem := Product{ID: "item123", Category: "gear-surf-surfboards", Name: "Yamba Surfboard"} + + pk := azcosmos.NewPartitionKeyString(testItem.Category) + + jsonItem, err := json.Marshal(testItem) + require.NoError(t, err) + + createItemResp, err := containerClient.CreateItem(ctx, pk, jsonItem, nil) + require.NoError(t, err) + require.NotNil(t, createItemResp) + + // Read item + readItemResp, err := containerClient.ReadItem(ctx, pk, testItem.ID, nil) + require.NoError(t, err) + require.NotNil(t, readItemResp) +} diff --git a/modules/azure/cosmosdb/examples_test.go b/modules/azure/cosmosdb/examples_test.go new file mode 100644 index 0000000000..3b4d9fccf1 --- /dev/null +++ b/modules/azure/cosmosdb/examples_test.go @@ -0,0 +1,85 @@ +package cosmosdb_test + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/azure/cosmosdb" +) + +func ExampleRun() { + ctx := context.Background() + + cosmosdbContainer, err := cosmosdb.Run(ctx, "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") + defer func() { + if err := testcontainers.TerminateContainer(cosmosdbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } + + state, err := cosmosdbContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: + // true +} + +func ExampleRun_connect() { + ctx := context.Background() + + cosmosdbContainer, err := cosmosdb.Run(ctx, "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") + defer func() { + if err := testcontainers.TerminateContainer(cosmosdbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + connString, err := cosmosdbContainer.ConnectionString(ctx) + if err != nil { + log.Printf("failed to get connection string: %s", err) + return + } + + p, err := cosmosdb.NewContainerPolicy(ctx, cosmosdbContainer) + if err != nil { + log.Printf("failed to create policy: %s", err) + return + } + + client, err := azcosmos.NewClientFromConnectionString(connString, p.ClientOptions()) + if err != nil { + log.Printf("failed to create cosmosdb client: %s", err) + return + } + + createDatabaseResp, err := client.CreateDatabase(ctx, azcosmos.DatabaseProperties{ID: "myDatabase"}, nil) + if err != nil { + log.Printf("failed to create database: %s", err) + return + } + // } + + fmt.Println(createDatabaseResp.RawResponse.StatusCode == http.StatusCreated) + + // Output: + // true +} diff --git a/modules/azure/cosmosdb/policy.go b/modules/azure/cosmosdb/policy.go new file mode 100644 index 0000000000..8dd5aa10b9 --- /dev/null +++ b/modules/azure/cosmosdb/policy.go @@ -0,0 +1,45 @@ +package cosmosdb + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" +) + +// ContainerPolicy ensures that requests always target the CosmosDB emulator container endpoint. +// It overrides the CosmosDB client's globalEndpointManager, which would otherwise dynamically +// update [http.Request.Host] based on global endpoint discovery, pinning all requests to the container. +type ContainerPolicy struct { + endpoint string +} + +func NewContainerPolicy(ctx context.Context, c *Container) (*ContainerPolicy, error) { + endpoint, err := c.PortEndpoint(ctx, defaultPort, "") + if err != nil { + return nil, fmt.Errorf("port endpoint: %w", err) + } + + return &ContainerPolicy{ + endpoint: endpoint, + }, nil +} + +func (p *ContainerPolicy) Do(req *policy.Request) (*http.Response, error) { + req.Raw().Host = p.endpoint + req.Raw().URL.Host = p.endpoint + + return req.Next() +} + +// ClientOptions returns Azure CosmosDB client options that contain ContainerPolicy. +func (p *ContainerPolicy) ClientOptions() *azcosmos.ClientOptions { + return &azcosmos.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + PerRetryPolicies: []policy.Policy{p}, + }, + } +} diff --git a/modules/azure/go.mod b/modules/azure/go.mod index f1b06b96d2..3e7aabe989 100644 --- a/modules/azure/go.mod +++ b/modules/azure/go.mod @@ -5,7 +5,8 @@ go 1.24.0 toolchain go1.24.7 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 + github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1 github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 @@ -19,8 +20,8 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/go-amqp v1.3.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect diff --git a/modules/azure/go.sum b/modules/azure/go.sum index 28a71e607e..1ad974a1a6 100644 --- a/modules/azure/go.sum +++ b/modules/azure/go.sum @@ -2,14 +2,18 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1 h1:ToPLhnXvatKVN4ZkcxLOwcXOJhdu4iQl8w0efeuDz9Y= +github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0 h1:NnE8y/opvxowwNcSNHubQUiSSEhfk3dmooLGAOmPuKs= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0/go.mod h1:GhHzPHiiHxZloo6WvKu9X7krmSAKTyGoIwoKMbrKTTA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.3.0 h1:skbmKp8umb8jMxl4A4CwvYyfCblujU00XUB/ytUjEac= github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.3.0/go.mod h1:nynTZqX7jGM6FQy6Y+7uFT7Y+LhaAeO3q3d48VZzH5E= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70= @@ -26,8 +30,8 @@ github.com/Azure/go-amqp v1.3.0 h1://1rikYhoIQNXJFXyoO/Rlb4+4EkHYfJceNtLlys2/4= github.com/Azure/go-amqp v1.3.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -71,8 +75,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre 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/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=