diff --git a/docs/modules/gcloud-shared.md b/docs/modules/gcloud-shared.md new file mode 100644 index 0000000000..b845b95de3 --- /dev/null +++ b/docs/modules/gcloud-shared.md @@ -0,0 +1,7 @@ +{% include "../features/common_functional_options.md" %} + +#### WithProjectID + +- Not available until the next release of testcontainers-go :material-tag: main + +The `WithProjectID` function sets the project ID for the Google Cloud container. diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index 79b23c2935..364c6f3ce6 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -16,21 +16,47 @@ go get github.com/testcontainers/testcontainers-go/modules/gcloud ## Usage example +The Google Cloud module exposes the following Go packages: + +- [BigQuery](#bigquery): `github.com/testcontainers/testcontainers-go/modules/gcloud/bigquery`. +- [BigTable](#bigtable): `github.com/testcontainers/testcontainers-go/modules/gcloud/bigtable`. +- [Datastore](#datastore): `github.com/testcontainers/testcontainers-go/modules/gcloud/datastore`. +- [Firestore](#firestore): `github.com/testcontainers/testcontainers-go/modules/gcloud/firestore`. +- [Pubsub](#pubsub): `github.com/testcontainers/testcontainers-go/modules/gcloud/pubsub`. +- [Spanner](#spanner): `github.com/testcontainers/testcontainers-go/modules/gcloud/spanner`. !!!info By default, the all the emulators use `gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators` as the default Docker image, except for the BigQuery emulator, which uses `ghcr.io/goccy/bigquery-emulator:0.6.1`, and Spanner, which uses `gcr.io/cloud-spanner-emulator/emulator:1.4.0`. -### BigQuery +## BigQuery - -[Creating a BigQuery container](../../modules/gcloud/bigquery_test.go) inside_block:runBigQueryContainer -[Obtaining a BigQuery client](../../modules/gcloud/bigquery_test.go) inside_block:bigQueryClient - +### Run function -It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the client example above. +- Not available until the next release of testcontainers-go :material-tag: main + +The BigQuery module exposes one entrypoint function to create the BigQuery 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. + +### Container Options + +When starting the BigQuery container, you can pass options in a variadic way to configure it. + +#### Image + +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "ghcr.io/goccy/bigquery-emulator:0.6.1")`. + +{% include "./gcloud-shared.md" %} #### Data YAML (Seed File) -- Since testcontainers-go :material-tag: v0.35.0 +- Not available until the next release of testcontainers-go :material-tag: main If you would like to do additional initialization in the BigQuery container, add a `data.yaml` file represented by an `io.Reader` to the container request with the `WithDataYAML` function. That file is copied after the container is created but before it's started. The startup command then used will look like `--project test --data-from-yaml /testcontainers-data.yaml`. @@ -38,77 +64,173 @@ That file is copied after the container is created but before it's started. The An example of a `data.yaml` file that seeds the BigQuery instance with datasets and tables is shown below: -[Data Yaml content](../../modules/gcloud/testdata/data.yaml) +[Data Yaml content](../../modules/gcloud/bigquery/testdata/data.yaml) -!!!warning - This feature is only available for the `BigQuery` container, and if you pass multiple `WithDataYAML` options, an error is returned. +### Examples + + +[Creating a BigQuery container](../../modules/gcloud/bigquery/examples_test.go) inside_block:runBigQueryContainer +[Obtaining a BigQuery client](../../modules/gcloud/bigquery/examples_test.go) inside_block:bigQueryClient + + +It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the client example above. + +## BigTable + +### Run function + +- Not available until the next release of testcontainers-go :material-tag: main + +The BigTable module exposes one entrypoint function to create the BigTable 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. + +### Container Options + +When starting the BigTable container, you can pass options in a variadic way to configure it. + +#### Image -### BigTable +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. + +{% include "./gcloud-shared.md" %} + +### Examples -[Creating a BigTable container](../../modules/gcloud/bigtable_test.go) inside_block:runBigTableContainer -[Obtaining a BigTable Admin client](../../modules/gcloud/bigtable_test.go) inside_block:bigTableAdminClient -[Obtaining a BigTable client](../../modules/gcloud/bigtable_test.go) inside_block:bigTableClient +[Creating a BigTable container](../../modules/gcloud/bigtable/examples_test.go) inside_block:runBigTableContainer +[Obtaining a BigTable Admin client](../../modules/gcloud/bigtable/examples_test.go) inside_block:bigTableAdminClient +[Obtaining a BigTable client](../../modules/gcloud/bigtable/examples_test.go) inside_block:bigTableClient It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the Admin client example above. -### Datastore +## Datastore + +### Run function + +- Not available until the next release of testcontainers-go :material-tag: main + +The Datastore module exposes one entrypoint function to create the Datastore 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. + +### Container Options + +When starting the Datastore container, you can pass options in a variadic way to configure it. + +#### Image + +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. + +{% include "./gcloud-shared.md" %} + +### Examples -[Creating a Datastore container](../../modules/gcloud/datastore_test.go) inside_block:runDatastoreContainer -[Obtaining a Datastore client](../../modules/gcloud/datastore_test.go) inside_block:datastoreClient +[Creating a Datastore container](../../modules/gcloud/datastore/examples_test.go) inside_block:runDatastoreContainer +[Obtaining a Datastore client](../../modules/gcloud/datastore/examples_test.go) inside_block:datastoreClient It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the client example above. -### Firestore +## Firestore - -[Creating a Firestore container](../../modules/gcloud/firestore_test.go) inside_block:runFirestoreContainer -[Obtaining a Firestore client](../../modules/gcloud/firestore_test.go) inside_block:firestoreClient - +### Run function -It's important to set the target string of the `grpc.NewClient` method using the container's URI, as shown in the client example above. +- Not available until the next release of testcontainers-go :material-tag: main + +The Firestore module exposes one entrypoint function to create the Firestore 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. + +### Container Options + +When starting the Firestore container, you can pass options in a variadic way to configure it. + +#### Image + +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. + +{% include "./gcloud-shared.md" %} -### Pubsub +### Examples -[Creating a Pubsub container](../../modules/gcloud/pubsub_test.go) inside_block:runPubsubContainer -[Obtaining a Pubsub client](../../modules/gcloud/pubsub_test.go) inside_block:pubsubClient +[Creating a Firestore container](../../modules/gcloud/firestore/examples_test.go) inside_block:runFirestoreContainer +[Obtaining a Firestore client](../../modules/gcloud/firestore/examples_test.go) inside_block:firestoreClient It's important to set the target string of the `grpc.NewClient` method using the container's URI, as shown in the client example above. -### Spanner +## Pubsub + +### Run function + +- Not available until the next release of testcontainers-go :material-tag: main + +The Pubsub module exposes one entrypoint function to create the Pubsub 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. + +### Container Options + +When starting the Pubsub container, you can pass options in a variadic way to configure it. + +#### Image + +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. + +{% include "./gcloud-shared.md" %} + +### Examples -[Creating a Spanner container](../../modules/gcloud/spanner_test.go) inside_block:runSpannerContainer -[Obtaining a Spanner Admin client](../../modules/gcloud/spanner_test.go) inside_block:spannerAdminClient -[Obtaining a Spanner Database Admin client](../../modules/gcloud/spanner_test.go) inside_block:spannerDBAdminClient +[Creating a Pubsub container](../../modules/gcloud/pubsub/examples_test.go) inside_block:runPubsubContainer +[Obtaining a Pubsub client](../../modules/gcloud/pubsub/examples_test.go) inside_block:pubsubClient -It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the Admin client example above. +It's important to set the target string of the `grpc.NewClient` method using the container's URI, as shown in the client example above. -## Module Reference +## Spanner ### Run function -- Since testcontainers-go :material-tag: v0.32.0 +- Not available until the next release of testcontainers-go :material-tag: main -!!!info - The `RunXXXContainer(ctx, opts...)` functions are deprecated and will be removed in the next major release of _Testcontainers for Go_. - -The GCloud module exposes one entrypoint function to create the different GCloud emulators, and each function receives three parameters: +The Spanner module exposes one entrypoint function to create the Spanner container, and this function receives three parameters: ```golang -func RunBigQuery(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*BigQueryContainer, error) -func RunBigTable(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*BigTableContainer, error) -func RunDatastore(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DatastoreContainer, error) -func RunFirestore(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*FirestoreContainer, error) -func RunPubsub(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*PubsubContainer, error) -func RunSpanner(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*SpannerContainer, error) +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) ``` - `context.Context`, the Go context. @@ -117,15 +239,21 @@ func RunSpanner(ctx context.Context, img string, opts ...testcontainers.Containe ### Container Options -When starting any of the GCloud containers, you can pass options in a variadic way to configure it. +When starting the Spanner container, you can pass options in a variadic way to configure it. #### Image -Use the second argument in the `RunXXX` function (`RunBigQuery, RunDatastore`, ...) to set a valid Docker image. -In example: `RunXXX(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. +Use the second argument in the `Run` function to set a valid Docker image. +In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`. + +{% include "./gcloud-shared.md" %} -{% include "../features/common_functional_options.md" %} +### Examples -### Container Methods + +[Creating a Spanner container](../../modules/gcloud/spanner/examples_test.go) inside_block:runSpannerContainer +[Obtaining a Spanner Admin client](../../modules/gcloud/spanner/examples_test.go) inside_block:spannerAdminClient +[Obtaining a Spanner Database Admin client](../../modules/gcloud/spanner/examples_test.go) inside_block:spannerDBAdminClient + -The GCloud container exposes the following methods: +It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the Admin client example above. diff --git a/modules/gcloud/bigquery.go b/modules/gcloud/bigquery.go index 6e6d7627dc..1e99aafda9 100644 --- a/modules/gcloud/bigquery.go +++ b/modules/gcloud/bigquery.go @@ -8,14 +8,15 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunBigQuery instead +// Deprecated: use [bigquery.Run] instead. // RunBigQueryContainer creates an instance of the GCloud container type for BigQuery. func RunBigQueryContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunBigQuery(ctx, "ghcr.io/goccy/bigquery-emulator:0.6.1", opts...) } +// Deprecated: use [bigquery.Run] instead. // RunBigQuery creates an instance of the GCloud container type for BigQuery. -// The URI will always use http:// as the protocol. +// The URI uses http:// as the protocol. func RunBigQuery(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ diff --git a/modules/gcloud/bigquery/bigquery.go b/modules/gcloud/bigquery/bigquery.go new file mode 100644 index 0000000000..899d16e86e --- /dev/null +++ b/modules/gcloud/bigquery/bigquery.go @@ -0,0 +1,84 @@ +package bigquery + +import ( + "context" + "fmt" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the BigQuery container. + DefaultProjectID = "test-project" + + // bigQueryDataYamlPath is the path to the data yaml file in the container. + bigQueryDataYamlPath = "/testcontainers-data.yaml" +) + +// Container represents the BigQuery container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the BigQuery container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the BigQuery container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the BigQuery GCloud container type. +// The URI uses http:// as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"9050/tcp", "9060/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("9050/tcp"), + wait.ForHTTP("/discovery/v1/apis/bigquery/v2/rest").WithPort("9050/tcp").WithStatusCodeMatcher(func(status int) bool { + return status == 200 + }).WithStartupTimeout(time.Second*5), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + req.Cmd = append(req.Cmd, "--project", settings.ProjectID) + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "9050/tcp", "http") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/bigquery/bigquery_test.go b/modules/gcloud/bigquery/bigquery_test.go new file mode 100644 index 0000000000..5e395f6df3 --- /dev/null +++ b/modules/gcloud/bigquery/bigquery_test.go @@ -0,0 +1,100 @@ +package bigquery_test + +import ( + "bytes" + "context" + _ "embed" + "errors" + "testing" + + "cloud.google.com/go/bigquery" + "github.com/stretchr/testify/require" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + "google.golang.org/api/option/internaloption" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcbigquery "github.com/testcontainers/testcontainers-go/modules/gcloud/bigquery" +) + +//go:embed testdata/data.yaml +var dataYaml []byte + +func TestBigQueryWithDataYAML(t *testing.T) { + ctx := context.Background() + + t.Run("valid", func(t *testing.T) { + bigQueryContainer, err := tcbigquery.Run( + ctx, + "ghcr.io/goccy/bigquery-emulator:0.6.1", + tcbigquery.WithProjectID("test"), + tcbigquery.WithDataYAML(bytes.NewReader(dataYaml)), + ) + testcontainers.CleanupContainer(t, bigQueryContainer) + require.NoError(t, err) + + projectID := bigQueryContainer.ProjectID() + + opts := []option.ClientOption{ + option.WithEndpoint(bigQueryContainer.URI()), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication(), + internaloption.SkipDialSettingsValidation(), + } + + client, err := bigquery.NewClient(ctx, projectID, opts...) + require.NoError(t, err) + defer client.Close() + + selectQuery := client.Query("SELECT * FROM dataset1.table_a where name = @name") + selectQuery.QueryConfig.Parameters = []bigquery.QueryParameter{ + {Name: "name", Value: "bob"}, + } + it, err := selectQuery.Read(ctx) + require.NoError(t, err) + + var val []bigquery.Value + for { + err := it.Next(&val) + if errors.Is(err, iterator.Done) { + break + } + require.NoError(t, err) + } + + require.Equal(t, int64(30), val[0]) + }) + + t.Run("multi-value-set", func(t *testing.T) { + bigQueryContainer, err := tcbigquery.Run( + ctx, + "ghcr.io/goccy/bigquery-emulator:0.6.1", + tcbigquery.WithProjectID("test"), + tcbigquery.WithDataYAML(bytes.NewReader(dataYaml)), + tcbigquery.WithDataYAML(bytes.NewReader(dataYaml)), + ) + testcontainers.CleanupContainer(t, bigQueryContainer) + require.EqualError(t, err, `data yaml already exists`) + }) + + t.Run("multi-value-not-set", func(t *testing.T) { + noValueOption := func() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Cmd = append(req.Cmd, "--data-from-yaml") + return nil + } + } + + bigQueryContainer, err := tcbigquery.Run( + ctx, + "ghcr.io/goccy/bigquery-emulator:0.6.1", + noValueOption(), // because --project is always added last, this option will receive `--project` as value, which results in an error + tcbigquery.WithProjectID("test"), + tcbigquery.WithDataYAML(bytes.NewReader(dataYaml)), + ) + testcontainers.CleanupContainer(t, bigQueryContainer) + require.Error(t, err) + }) +} diff --git a/modules/gcloud/bigquery/examples_test.go b/modules/gcloud/bigquery/examples_test.go new file mode 100644 index 0000000000..4176db7af7 --- /dev/null +++ b/modules/gcloud/bigquery/examples_test.go @@ -0,0 +1,87 @@ +package bigquery_test + +import ( + "context" + "errors" + "fmt" + "log" + + "cloud.google.com/go/bigquery" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + "google.golang.org/api/option/internaloption" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcbigquery "github.com/testcontainers/testcontainers-go/modules/gcloud/bigquery" +) + +func ExampleRun() { + // runBigQueryContainer { + ctx := context.Background() + + bigQueryContainer, err := tcbigquery.Run( + ctx, + "ghcr.io/goccy/bigquery-emulator:0.6.1", + tcbigquery.WithProjectID("bigquery-project"), + ) + defer func() { + if err := testcontainers.TerminateContainer(bigQueryContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } + // } + + // bigQueryClient { + projectID := bigQueryContainer.ProjectID() + + opts := []option.ClientOption{ + option.WithEndpoint(bigQueryContainer.URI()), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication(), + internaloption.SkipDialSettingsValidation(), + } + + client, err := bigquery.NewClient(ctx, projectID, opts...) + if err != nil { + log.Printf("failed to create bigquery client: %v", err) + return + } + defer client.Close() + // } + + createFnQuery := client.Query("CREATE FUNCTION testr(arr ARRAY>) AS ((SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem))") + _, err = createFnQuery.Read(ctx) + if err != nil { + log.Printf("failed to create function: %v", err) + return + } + + selectQuery := client.Query("SELECT testr([STRUCT(\"foo\", 10), STRUCT(\"bar\", 40), STRUCT(\"foo\", 20)])") + it, err := selectQuery.Read(ctx) + if err != nil { + log.Printf("failed to read query: %v", err) + return + } + + var val []bigquery.Value + for { + err := it.Next(&val) + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + log.Printf("failed to iterate: %v", err) + return + } + } + + fmt.Println(val) + // Output: + // [30] +} diff --git a/modules/gcloud/bigquery/options.go b/modules/gcloud/bigquery/options.go new file mode 100644 index 0000000000..16e080bcee --- /dev/null +++ b/modules/gcloud/bigquery/options.go @@ -0,0 +1,47 @@ +package bigquery + +import ( + "errors" + "io" + "slices" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" +) + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID + +// WithDataYAML seeds the BigQuery project for the GCloud container with an [io.Reader] representing +// the data yaml file, which is used to copy the file to the container, and then processed to seed +// the BigQuery project. +// +// Other GCloud containers will ignore this option. +func WithDataYAML(r io.Reader) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + if slices.Contains(req.Cmd, "--data-from-yaml") { + return errors.New("data yaml already exists") + } + + req.Cmd = append(req.Cmd, "--data-from-yaml", bigQueryDataYamlPath) + + req.Files = append(req.Files, testcontainers.ContainerFile{ + Reader: r, + ContainerFilePath: bigQueryDataYamlPath, + FileMode: 0o644, + }) + + return nil + } +} diff --git a/modules/gcloud/bigquery/options_test.go b/modules/gcloud/bigquery/options_test.go new file mode 100644 index 0000000000..b24da996b2 --- /dev/null +++ b/modules/gcloud/bigquery/options_test.go @@ -0,0 +1,38 @@ +package bigquery + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" +) + +func TestWithDataYAML(t *testing.T) { + t.Run("success", func(t *testing.T) { + req := &testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{}, + } + + err := WithDataYAML(bytes.NewReader([]byte("")))(req) + require.NoError(t, err) + require.Contains(t, req.Cmd, "--data-from-yaml") + require.Len(t, req.Files, 1) + }) + + t.Run("double-calls-errors", func(t *testing.T) { + req := &testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{}, + } + + err := WithDataYAML(bytes.NewReader([]byte("")))(req) + require.NoError(t, err) + require.Contains(t, req.Cmd, "--data-from-yaml") + require.Len(t, req.Files, 1) + + err = WithDataYAML(bytes.NewReader([]byte("")))(req) + require.Error(t, err) + require.Len(t, req.Files, 1) + }) +} diff --git a/modules/gcloud/testdata/data.yaml b/modules/gcloud/bigquery/testdata/data.yaml similarity index 100% rename from modules/gcloud/testdata/data.yaml rename to modules/gcloud/bigquery/testdata/data.yaml diff --git a/modules/gcloud/bigquery_test.go b/modules/gcloud/bigquery_test.go deleted file mode 100644 index 221a750c2d..0000000000 --- a/modules/gcloud/bigquery_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package gcloud_test - -import ( - "bytes" - "context" - _ "embed" - "errors" - "fmt" - "log" - "testing" - - "cloud.google.com/go/bigquery" - "github.com/stretchr/testify/require" - "google.golang.org/api/iterator" - "google.golang.org/api/option" - "google.golang.org/api/option/internaloption" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" -) - -//go:embed testdata/data.yaml -var dataYaml []byte - -func ExampleRunBigQueryContainer() { - // runBigQueryContainer { - ctx := context.Background() - - bigQueryContainer, err := gcloud.RunBigQuery( - ctx, - "ghcr.io/goccy/bigquery-emulator:0.6.1", - gcloud.WithProjectID("bigquery-project"), - ) - defer func() { - if err := testcontainers.TerminateContainer(bigQueryContainer); err != nil { - log.Printf("failed to terminate container: %s", err) - } - }() - if err != nil { - log.Printf("failed to run container: %v", err) - return - } - // } - - // bigQueryClient { - projectID := bigQueryContainer.Settings.ProjectID - - opts := []option.ClientOption{ - option.WithEndpoint(bigQueryContainer.URI), - option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), - option.WithoutAuthentication(), - internaloption.SkipDialSettingsValidation(), - } - - client, err := bigquery.NewClient(ctx, projectID, opts...) - if err != nil { - log.Printf("failed to create bigquery client: %v", err) - return - } - defer client.Close() - // } - - createFnQuery := client.Query("CREATE FUNCTION testr(arr ARRAY>) AS ((SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem))") - _, err = createFnQuery.Read(ctx) - if err != nil { - log.Printf("failed to create function: %v", err) - return - } - - selectQuery := client.Query("SELECT testr([STRUCT(\"foo\", 10), STRUCT(\"bar\", 40), STRUCT(\"foo\", 20)])") - it, err := selectQuery.Read(ctx) - if err != nil { - log.Printf("failed to read query: %v", err) - return - } - - var val []bigquery.Value - for { - err := it.Next(&val) - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - log.Printf("failed to iterate: %v", err) - return - } - } - - fmt.Println(val) - // Output: - // [30] -} - -func TestBigQueryWithDataYAML(t *testing.T) { - ctx := context.Background() - - t.Run("valid", func(t *testing.T) { - bigQueryContainer, err := gcloud.RunBigQuery( - ctx, - "ghcr.io/goccy/bigquery-emulator:0.6.1", - gcloud.WithProjectID("test"), - gcloud.WithDataYAML(bytes.NewReader(dataYaml)), - ) - testcontainers.CleanupContainer(t, bigQueryContainer) - require.NoError(t, err) - - projectID := bigQueryContainer.Settings.ProjectID - - opts := []option.ClientOption{ - option.WithEndpoint(bigQueryContainer.URI), - option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), - option.WithoutAuthentication(), - internaloption.SkipDialSettingsValidation(), - } - - client, err := bigquery.NewClient(ctx, projectID, opts...) - require.NoError(t, err) - defer client.Close() - - selectQuery := client.Query("SELECT * FROM dataset1.table_a where name = @name") - selectQuery.QueryConfig.Parameters = []bigquery.QueryParameter{ - {Name: "name", Value: "bob"}, - } - it, err := selectQuery.Read(ctx) - require.NoError(t, err) - - var val []bigquery.Value - for { - err := it.Next(&val) - if errors.Is(err, iterator.Done) { - break - } - require.NoError(t, err) - } - - require.Equal(t, int64(30), val[0]) - }) - - t.Run("multi-value-set", func(t *testing.T) { - bigQueryContainer, err := gcloud.RunBigQuery( - ctx, - "ghcr.io/goccy/bigquery-emulator:0.6.1", - gcloud.WithProjectID("test"), - gcloud.WithDataYAML(bytes.NewReader(dataYaml)), - gcloud.WithDataYAML(bytes.NewReader(dataYaml)), - ) - testcontainers.CleanupContainer(t, bigQueryContainer) - require.EqualError(t, err, `data yaml already exists`) - }) - - t.Run("multi-value-not-set", func(t *testing.T) { - noValueOption := func() testcontainers.CustomizeRequestOption { - return func(req *testcontainers.GenericContainerRequest) error { - req.Cmd = append(req.Cmd, "--data-from-yaml") - return nil - } - } - - bigQueryContainer, err := gcloud.RunBigQuery( - ctx, - "ghcr.io/goccy/bigquery-emulator:0.6.1", - noValueOption(), // because --project is always added last, this option will receive `--project` as value, which results in an error - gcloud.WithProjectID("test"), - gcloud.WithDataYAML(bytes.NewReader(dataYaml)), - ) - testcontainers.CleanupContainer(t, bigQueryContainer) - require.Error(t, err) - }) -} diff --git a/modules/gcloud/bigtable.go b/modules/gcloud/bigtable.go index 134f14d1d6..e133b55e46 100644 --- a/modules/gcloud/bigtable.go +++ b/modules/gcloud/bigtable.go @@ -7,12 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunBigTable instead +// Deprecated: use [bigtable.Run] instead // RunBigTableContainer creates an instance of the GCloud container type for BigTable. func RunBigTableContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunBigQuery(ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", opts...) } +// Deprecated: use [bigtable.Run] instead // RunBigTable creates an instance of the GCloud container type for BigTable. func RunBigTable(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ diff --git a/modules/gcloud/bigtable/bigtable.go b/modules/gcloud/bigtable/bigtable.go new file mode 100644 index 0000000000..5d6a7c98b5 --- /dev/null +++ b/modules/gcloud/bigtable/bigtable.go @@ -0,0 +1,82 @@ +package bigtable + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the BigTable container. + DefaultProjectID = "test-project" +) + +// Container represents the BigTable container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the BigTable container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the BigTable container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the BigTable GCloud container type. +// The URI uses the empty string as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"9000/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("9000/tcp"), + wait.ForLog("running"), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + req.Cmd = []string{ + "/bin/sh", + "-c", + "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000 --project=" + settings.ProjectID, + } + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "9000/tcp", "") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/bigtable/bigtable_test.go b/modules/gcloud/bigtable/bigtable_test.go new file mode 100644 index 0000000000..129cefef7b --- /dev/null +++ b/modules/gcloud/bigtable/bigtable_test.go @@ -0,0 +1,66 @@ +package bigtable_test + +import ( + "context" + "testing" + + "cloud.google.com/go/bigtable" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcbigtable "github.com/testcontainers/testcontainers-go/modules/gcloud/bigtable" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + bigTableContainer, err := tcbigtable.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", + tcbigtable.WithProjectID("bigtable-project"), + ) + testcontainers.CleanupContainer(t, bigTableContainer) + require.NoError(t, err) + + projectId := bigTableContainer.ProjectID() + + const ( + instanceId = "test-instance" + tableName = "test-table" + ) + + options := []option.ClientOption{ + option.WithEndpoint(bigTableContainer.URI()), + option.WithoutAuthentication(), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + } + adminClient, err := bigtable.NewAdminClient(ctx, projectId, instanceId, options...) + require.NoError(t, err) + defer adminClient.Close() + + err = adminClient.CreateTable(ctx, tableName) + require.NoError(t, err) + + err = adminClient.CreateColumnFamily(ctx, tableName, "name") + require.NoError(t, err) + + client, err := bigtable.NewClient(ctx, projectId, instanceId, options...) + require.NoError(t, err) + defer client.Close() + + tbl := client.Open(tableName) + + mut := bigtable.NewMutation() + mut.Set("name", "firstName", bigtable.Now(), []byte("Gopher")) + + err = tbl.Apply(ctx, "1", mut) + require.NoError(t, err) + + row, err := tbl.ReadRow(ctx, "1", bigtable.RowFilter(bigtable.FamilyFilter("name"))) + require.NoError(t, err) + + require.Equal(t, "Gopher", string(row["name"][0].Value)) +} diff --git a/modules/gcloud/bigtable_test.go b/modules/gcloud/bigtable/examples_test.go similarity index 86% rename from modules/gcloud/bigtable_test.go rename to modules/gcloud/bigtable/examples_test.go index 553581bcc4..a94419c502 100644 --- a/modules/gcloud/bigtable_test.go +++ b/modules/gcloud/bigtable/examples_test.go @@ -1,4 +1,4 @@ -package gcloud_test +package bigtable_test import ( "context" @@ -11,17 +11,17 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" + tcbigtable "github.com/testcontainers/testcontainers-go/modules/gcloud/bigtable" ) -func ExampleRunBigTableContainer() { +func ExampleRun() { // runBigTableContainer { ctx := context.Background() - bigTableContainer, err := gcloud.RunBigTable( + bigTableContainer, err := tcbigtable.Run( ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", - gcloud.WithProjectID("bigtable-project"), + tcbigtable.WithProjectID("bigtable-project"), ) defer func() { if err := testcontainers.TerminateContainer(bigTableContainer); err != nil { @@ -35,7 +35,7 @@ func ExampleRunBigTableContainer() { // } // bigTableAdminClient { - projectId := bigTableContainer.Settings.ProjectID + projectId := bigTableContainer.ProjectID() const ( instanceId = "test-instance" @@ -43,7 +43,7 @@ func ExampleRunBigTableContainer() { ) options := []option.ClientOption{ - option.WithEndpoint(bigTableContainer.URI), + option.WithEndpoint(bigTableContainer.URI()), option.WithoutAuthentication(), option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), } diff --git a/modules/gcloud/bigtable/options.go b/modules/gcloud/bigtable/options.go new file mode 100644 index 0000000000..b8ae75b8f4 --- /dev/null +++ b/modules/gcloud/bigtable/options.go @@ -0,0 +1,17 @@ +package bigtable + +import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID diff --git a/modules/gcloud/datastore.go b/modules/gcloud/datastore.go index caf53e9879..02fbd73f6a 100644 --- a/modules/gcloud/datastore.go +++ b/modules/gcloud/datastore.go @@ -7,12 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunDatastore instead +// Deprecated: use [datastore.Run] instead // RunDatastoreContainer creates an instance of the GCloud container type for Datastore. func RunDatastoreContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunDatastore(ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", opts...) } +// Deprecated: use [datastore.Run] instead // RunDatastore creates an instance of the GCloud container type for Datastore. func RunDatastore(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ diff --git a/modules/gcloud/datastore/datastore.go b/modules/gcloud/datastore/datastore.go new file mode 100644 index 0000000000..b421925079 --- /dev/null +++ b/modules/gcloud/datastore/datastore.go @@ -0,0 +1,82 @@ +package datastore + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the Datastore container. + DefaultProjectID = "test-project" +) + +// Container represents the Datastore container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the Datastore container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the Datastore container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the Datastore GCloud container type. +// The URI uses the empty string as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"8081/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("8081/tcp"), + wait.ForHTTP("/").WithPort("8081/tcp"), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + req.Cmd = []string{ + "/bin/sh", + "-c", + "gcloud beta emulators datastore start --host-port 0.0.0.0:8081 --project=" + settings.ProjectID, + } + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "8081/tcp", "") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/datastore/datastore_test.go b/modules/gcloud/datastore/datastore_test.go new file mode 100644 index 0000000000..e844ec0e99 --- /dev/null +++ b/modules/gcloud/datastore/datastore_test.go @@ -0,0 +1,60 @@ +package datastore_test + +import ( + "context" + "log" + "testing" + + "cloud.google.com/go/datastore" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcdatastore "github.com/testcontainers/testcontainers-go/modules/gcloud/datastore" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + datastoreContainer, err := tcdatastore.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", + tcdatastore.WithProjectID("datastore-project"), + ) + testcontainers.CleanupContainer(t, datastoreContainer) + require.NoError(t, err) + + projectID := datastoreContainer.ProjectID() + + options := []option.ClientOption{ + option.WithEndpoint(datastoreContainer.URI()), + option.WithoutAuthentication(), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + } + + dsClient, err := datastore.NewClient(ctx, projectID, options...) + if err != nil { + log.Printf("failed to create client: %v", err) + return + } + defer dsClient.Close() + + type Task struct { + Description string + } + + k := datastore.NameKey("Task", "sample", nil) + data := Task{ + Description: "my description", + } + _, err = dsClient.Put(ctx, k, &data) + require.NoError(t, err) + + saved := Task{} + err = dsClient.Get(ctx, k, &saved) + require.NoError(t, err) + + require.Equal(t, "my description", saved.Description) +} diff --git a/modules/gcloud/datastore_test.go b/modules/gcloud/datastore/examples_test.go similarity index 81% rename from modules/gcloud/datastore_test.go rename to modules/gcloud/datastore/examples_test.go index fa056bbf63..7093f0e46b 100644 --- a/modules/gcloud/datastore_test.go +++ b/modules/gcloud/datastore/examples_test.go @@ -1,4 +1,4 @@ -package gcloud_test +package datastore_test import ( "context" @@ -11,17 +11,17 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" + tcdatastore "github.com/testcontainers/testcontainers-go/modules/gcloud/datastore" ) -func ExampleRunDatastoreContainer() { +func ExampleRun() { // runDatastoreContainer { ctx := context.Background() - datastoreContainer, err := gcloud.RunDatastore( + datastoreContainer, err := tcdatastore.Run( ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", - gcloud.WithProjectID("datastore-project"), + tcdatastore.WithProjectID("datastore-project"), ) defer func() { if err := testcontainers.TerminateContainer(datastoreContainer); err != nil { @@ -35,10 +35,10 @@ func ExampleRunDatastoreContainer() { // } // datastoreClient { - projectID := datastoreContainer.Settings.ProjectID + projectID := datastoreContainer.ProjectID() options := []option.ClientOption{ - option.WithEndpoint(datastoreContainer.URI), + option.WithEndpoint(datastoreContainer.URI()), option.WithoutAuthentication(), option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), } diff --git a/modules/gcloud/datastore/options.go b/modules/gcloud/datastore/options.go new file mode 100644 index 0000000000..2adbd09340 --- /dev/null +++ b/modules/gcloud/datastore/options.go @@ -0,0 +1,17 @@ +package datastore + +import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID diff --git a/modules/gcloud/firestore.go b/modules/gcloud/firestore.go index 297b47f80c..b333a64f44 100644 --- a/modules/gcloud/firestore.go +++ b/modules/gcloud/firestore.go @@ -7,12 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunFirestore instead +// Deprecated: use [firestore.Run] instead // RunFirestoreContainer creates an instance of the GCloud container type for Firestore. func RunFirestoreContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunFirestore(ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", opts...) } +// Deprecated: use [firestore.Run] instead // RunFirestore creates an instance of the GCloud container type for Firestore. func RunFirestore(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ diff --git a/modules/gcloud/firestore_test.go b/modules/gcloud/firestore/examples_test.go similarity index 81% rename from modules/gcloud/firestore_test.go rename to modules/gcloud/firestore/examples_test.go index a457f2764e..d06abe3200 100644 --- a/modules/gcloud/firestore_test.go +++ b/modules/gcloud/firestore/examples_test.go @@ -1,4 +1,4 @@ -package gcloud_test +package firestore_test import ( "context" @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" + tcfirestore "github.com/testcontainers/testcontainers-go/modules/gcloud/firestore" ) type emulatorCreds struct{} @@ -24,14 +24,14 @@ func (ec emulatorCreds) RequireTransportSecurity() bool { return false } -func ExampleRunFirestoreContainer() { +func ExampleRun() { // runFirestoreContainer { ctx := context.Background() - firestoreContainer, err := gcloud.RunFirestore( + firestoreContainer, err := tcfirestore.Run( ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", - gcloud.WithProjectID("firestore-project"), + tcfirestore.WithProjectID("firestore-project"), ) defer func() { if err := testcontainers.TerminateContainer(firestoreContainer); err != nil { @@ -45,9 +45,9 @@ func ExampleRunFirestoreContainer() { // } // firestoreClient { - projectID := firestoreContainer.Settings.ProjectID + projectID := firestoreContainer.ProjectID() - conn, err := grpc.NewClient(firestoreContainer.URI, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(emulatorCreds{})) + conn, err := grpc.NewClient(firestoreContainer.URI(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(emulatorCreds{})) if err != nil { log.Printf("failed to dial: %v", err) return diff --git a/modules/gcloud/firestore/firestore.go b/modules/gcloud/firestore/firestore.go new file mode 100644 index 0000000000..cade5dd48a --- /dev/null +++ b/modules/gcloud/firestore/firestore.go @@ -0,0 +1,82 @@ +package firestore + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the Firestore container. + DefaultProjectID = "test-project" +) + +// Container represents the Firestore container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the Firestore container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the Firestore container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the Firestore GCloud container type. +// The URI uses the empty string as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("8080/tcp"), + wait.ForLog("running"), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + req.Cmd = []string{ + "/bin/sh", + "-c", + "gcloud beta emulators firestore start --host-port 0.0.0.0:8080 --project=" + settings.ProjectID, + } + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "8080/tcp", "") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/firestore/firestore_test.go b/modules/gcloud/firestore/firestore_test.go new file mode 100644 index 0000000000..a98a52b2dc --- /dev/null +++ b/modules/gcloud/firestore/firestore_test.go @@ -0,0 +1,61 @@ +package firestore_test + +import ( + "context" + "testing" + + "cloud.google.com/go/firestore" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcfirestore "github.com/testcontainers/testcontainers-go/modules/gcloud/firestore" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + firestoreContainer, err := tcfirestore.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", + tcfirestore.WithProjectID("firestore-project"), + ) + testcontainers.CleanupContainer(t, firestoreContainer) + require.NoError(t, err) + + projectID := firestoreContainer.ProjectID() + + conn, err := grpc.NewClient(firestoreContainer.URI(), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + options := []option.ClientOption{option.WithGRPCConn(conn)} + client, err := firestore.NewClient(ctx, projectID, options...) + require.NoError(t, err) + defer client.Close() + + users := client.Collection("users") + docRef := users.Doc("alovelace") + + type Person struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + } + + data := Person{ + Firstname: "Ada", + Lastname: "Lovelace", + } + _, err = docRef.Create(ctx, data) + require.NoError(t, err) + + docsnap, err := docRef.Get(ctx) + require.NoError(t, err) + + var saved Person + require.NoError(t, docsnap.DataTo(&saved)) + + require.Equal(t, "Ada", saved.Firstname) + require.Equal(t, "Lovelace", saved.Lastname) +} diff --git a/modules/gcloud/firestore/options.go b/modules/gcloud/firestore/options.go new file mode 100644 index 0000000000..8c6f32a947 --- /dev/null +++ b/modules/gcloud/firestore/options.go @@ -0,0 +1,17 @@ +package firestore + +import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID diff --git a/modules/gcloud/gcloud.go b/modules/gcloud/gcloud.go index 2b6a28ed5d..157bbf934f 100644 --- a/modules/gcloud/gcloud.go +++ b/modules/gcloud/gcloud.go @@ -13,6 +13,13 @@ import ( const defaultProjectID = "test-project" +// Deprecated: use the specialized containers instead: +// - [bigquery.Container] +// - [bigtable.Container] +// - [datastore.Container] +// - [firestore.Container] +// - [pubsub.Container] +// - [spanner.Container] type GCloudContainer struct { testcontainers.Container Settings options @@ -76,6 +83,7 @@ func WithProjectID(projectID string) Option { } } +// Deprecated: Use [bigquery.WithDataYAML] instead. // WithDataYAML seeds the BigQuery project for the GCloud container with an [io.Reader] representing // the data yaml file, which is used to copy the file to the container, and then processed to seed // the BigQuery project. diff --git a/modules/gcloud/internal/shared/shared.go b/modules/gcloud/internal/shared/shared.go new file mode 100644 index 0000000000..0caf6a56df --- /dev/null +++ b/modules/gcloud/internal/shared/shared.go @@ -0,0 +1,44 @@ +package shared + +import ( + "github.com/testcontainers/testcontainers-go" +) + +const ( + // DefaultProjectID is the default project ID for the Pubsub container. + DefaultProjectID = "test-project" +) + +// Options represents the options for the different GCloud containers. +// This type must contain all the options that are common to all the GCloud containers. +type Options struct { + ProjectID string + URI string +} + +// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface. +var _ testcontainers.ContainerCustomizer = (*Option)(nil) + +// Option is an option for the GCloud container. +type Option func(*Options) error + +// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { + // NOOP to satisfy interface. + return nil +} + +// DefaultOptions returns a new Options instance with the default project ID. +func DefaultOptions() Options { + return Options{ + ProjectID: DefaultProjectID, + } +} + +// WithProjectID sets the project ID for the GCloud container. +func WithProjectID(projectID string) Option { + return func(o *Options) error { + o.ProjectID = projectID + return nil + } +} diff --git a/modules/gcloud/internal/shared/shared_test.go b/modules/gcloud/internal/shared/shared_test.go new file mode 100644 index 0000000000..666203f9f7 --- /dev/null +++ b/modules/gcloud/internal/shared/shared_test.go @@ -0,0 +1,28 @@ +package shared_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" +) + +func TestDefaultOptions(t *testing.T) { + opts := shared.DefaultOptions() + require.Equal(t, shared.DefaultProjectID, opts.ProjectID) +} + +func TestWithProjectID(t *testing.T) { + opts := shared.DefaultOptions() + + err := shared.WithProjectID("test-project")(&opts) + require.NoError(t, err) + require.Equal(t, "test-project", opts.ProjectID) +} + +func TestCustomize(t *testing.T) { + err := shared.WithProjectID("test-project-2").Customize(&testcontainers.GenericContainerRequest{}) + require.NoError(t, err) +} diff --git a/modules/gcloud/pubsub.go b/modules/gcloud/pubsub.go index d57ea35c16..2d637563dc 100644 --- a/modules/gcloud/pubsub.go +++ b/modules/gcloud/pubsub.go @@ -7,12 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunPubsub instead +// Deprecated: use [pubsub.Run] instead // RunPubsubContainer creates an instance of the GCloud container type for Pubsub. func RunPubsubContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunPubsub(ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", opts...) } +// Deprecated: use [pubsub.Run] instead // RunPubsub creates an instance of the GCloud container type for Pubsub. func RunPubsub(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ diff --git a/modules/gcloud/pubsub_test.go b/modules/gcloud/pubsub/examples_test.go similarity index 82% rename from modules/gcloud/pubsub_test.go rename to modules/gcloud/pubsub/examples_test.go index 38cd539129..4707662c74 100644 --- a/modules/gcloud/pubsub_test.go +++ b/modules/gcloud/pubsub/examples_test.go @@ -1,4 +1,4 @@ -package gcloud_test +package pubsub_test import ( "context" @@ -11,17 +11,17 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" + tcpubsub "github.com/testcontainers/testcontainers-go/modules/gcloud/pubsub" ) -func ExampleRunPubsubContainer() { +func ExampleRun() { // runPubsubContainer { ctx := context.Background() - pubsubContainer, err := gcloud.RunPubsub( + pubsubContainer, err := tcpubsub.Run( ctx, "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", - gcloud.WithProjectID("pubsub-project"), + tcpubsub.WithProjectID("pubsub-project"), ) defer func() { if err := testcontainers.TerminateContainer(pubsubContainer); err != nil { @@ -35,9 +35,9 @@ func ExampleRunPubsubContainer() { // } // pubsubClient { - projectID := pubsubContainer.Settings.ProjectID + projectID := pubsubContainer.ProjectID() - conn, err := grpc.NewClient(pubsubContainer.URI, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(pubsubContainer.URI(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Printf("failed to dial: %v", err) return diff --git a/modules/gcloud/pubsub/options.go b/modules/gcloud/pubsub/options.go new file mode 100644 index 0000000000..7b6049af7d --- /dev/null +++ b/modules/gcloud/pubsub/options.go @@ -0,0 +1,17 @@ +package pubsub + +import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID diff --git a/modules/gcloud/pubsub/pubsub.go b/modules/gcloud/pubsub/pubsub.go new file mode 100644 index 0000000000..d41e55d21a --- /dev/null +++ b/modules/gcloud/pubsub/pubsub.go @@ -0,0 +1,82 @@ +package pubsub + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the Pubsub container. + DefaultProjectID = "test-project" +) + +// Container represents the Pubsub container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the Pubsub container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the Pubsub container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the Pubsub GCloud container type. +// The URI uses the empty string as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"8085/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("8085/tcp"), + wait.ForLog("started"), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + req.Cmd = []string{ + "/bin/sh", + "-c", + "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085 --project=" + settings.ProjectID, + } + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "8085/tcp", "") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/pubsub/pubsub_test.go b/modules/gcloud/pubsub/pubsub_test.go new file mode 100644 index 0000000000..f8979b05ff --- /dev/null +++ b/modules/gcloud/pubsub/pubsub_test.go @@ -0,0 +1,63 @@ +package pubsub_test + +import ( + "context" + "log" + "testing" + + "cloud.google.com/go/pubsub" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcpubsub "github.com/testcontainers/testcontainers-go/modules/gcloud/pubsub" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + pubsubContainer, err := tcpubsub.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", + tcpubsub.WithProjectID("pubsub-project"), + ) + testcontainers.CleanupContainer(t, pubsubContainer) + require.NoError(t, err) + + projectID := pubsubContainer.ProjectID() + + conn, err := grpc.NewClient(pubsubContainer.URI(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Printf("failed to dial: %v", err) + return + } + + options := []option.ClientOption{option.WithGRPCConn(conn)} + client, err := pubsub.NewClient(ctx, projectID, options...) + require.NoError(t, err) + defer client.Close() + + topic, err := client.CreateTopic(ctx, "greetings") + require.NoError(t, err) + + subscription, err := client.CreateSubscription(ctx, "subscription", + pubsub.SubscriptionConfig{Topic: topic}) + require.NoError(t, err) + + result := topic.Publish(ctx, &pubsub.Message{Data: []byte("Hello World")}) + _, err = result.Get(ctx) + require.NoError(t, err) + + var data []byte + cctx, cancel := context.WithCancel(ctx) + err = subscription.Receive(cctx, func(_ context.Context, m *pubsub.Message) { + data = m.Data + m.Ack() + defer cancel() + }) + require.NoError(t, err) + + require.Equal(t, "Hello World", string(data)) +} diff --git a/modules/gcloud/spanner.go b/modules/gcloud/spanner.go index 8b306db4ce..b5e9bb57f3 100644 --- a/modules/gcloud/spanner.go +++ b/modules/gcloud/spanner.go @@ -7,12 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -// Deprecated: use RunSpanner instead +// Deprecated: use [spanner.Run] instead // RunSpannerContainer creates an instance of the GCloud container type for Spanner. func RunSpannerContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { return RunSpanner(ctx, "gcr.io/cloud-spanner-emulator/emulator:1.4.0", opts...) } +// Deprecated: use [spanner.Run] instead // RunSpanner creates an instance of the GCloud container type for Spanner. func RunSpanner(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) { req := testcontainers.GenericContainerRequest{ diff --git a/modules/gcloud/spanner_test.go b/modules/gcloud/spanner/examples_test.go similarity index 91% rename from modules/gcloud/spanner_test.go rename to modules/gcloud/spanner/examples_test.go index 02e3b48b28..4237ba8c78 100644 --- a/modules/gcloud/spanner_test.go +++ b/modules/gcloud/spanner/examples_test.go @@ -1,4 +1,4 @@ -package gcloud_test +package spanner_test import ( "context" @@ -16,17 +16,17 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/gcloud" + tcspanner "github.com/testcontainers/testcontainers-go/modules/gcloud/spanner" ) -func ExampleRunSpannerContainer() { +func ExampleRun() { // runSpannerContainer { ctx := context.Background() - spannerContainer, err := gcloud.RunSpanner( + spannerContainer, err := tcspanner.Run( ctx, "gcr.io/cloud-spanner-emulator/emulator:1.4.0", - gcloud.WithProjectID("spanner-project"), + tcspanner.WithProjectID("spanner-project"), ) defer func() { if err := testcontainers.TerminateContainer(spannerContainer); err != nil { @@ -40,7 +40,7 @@ func ExampleRunSpannerContainer() { // } // spannerAdminClient { - projectId := spannerContainer.Settings.ProjectID + projectId := spannerContainer.ProjectID() const ( instanceId = "test-instance" @@ -48,7 +48,7 @@ func ExampleRunSpannerContainer() { ) options := []option.ClientOption{ - option.WithEndpoint(spannerContainer.URI), + option.WithEndpoint(spannerContainer.URI()), option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), option.WithoutAuthentication(), internaloption.SkipDialSettingsValidation(), diff --git a/modules/gcloud/spanner/options.go b/modules/gcloud/spanner/options.go new file mode 100644 index 0000000000..2dabbcd722 --- /dev/null +++ b/modules/gcloud/spanner/options.go @@ -0,0 +1,17 @@ +package spanner + +import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" + +// Options aliases the common GCloud options +type options = shared.Options + +// Option aliases the common GCloud option type +type Option = shared.Option + +// defaultOptions returns a new Options instance with the default project ID. +func defaultOptions() options { + return shared.DefaultOptions() +} + +// WithProjectID re-exports the common GCloud WithProjectID option +var WithProjectID = shared.WithProjectID diff --git a/modules/gcloud/spanner/spanner.go b/modules/gcloud/spanner/spanner.go new file mode 100644 index 0000000000..388c6e5074 --- /dev/null +++ b/modules/gcloud/spanner/spanner.go @@ -0,0 +1,76 @@ +package spanner + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + // DefaultProjectID is the default project ID for the Pubsub container. + DefaultProjectID = "test-project" +) + +// Container represents the Spanner container type used in the module +type Container struct { + testcontainers.Container + settings options +} + +// ProjectID returns the project ID of the Spanner container. +func (c *Container) ProjectID() string { + return c.settings.ProjectID +} + +// URI returns the URI of the Spanner container. +func (c *Container) URI() string { + return c.settings.URI +} + +// Run creates an instance of the Spanner GCloud container type. +// The URI uses the empty string as the protocol. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"9010/tcp"}, + WaitingFor: wait.ForAll( + wait.ForListeningPort("9010/tcp"), + wait.ForLog("Cloud Spanner emulator running"), + ), + }, + Started: true, + } + + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + if err := apply(&settings); err != nil { + return nil, err + } + } + if err := opt.Customize(&req); err != nil { + return nil, err + } + } + + container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container, settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + portEndpoint, err := c.PortEndpoint(ctx, "9010/tcp", "") + if err != nil { + return c, fmt.Errorf("port endpoint: %w", err) + } + + c.settings.URI = portEndpoint + + return c, nil +} diff --git a/modules/gcloud/spanner/spanner_test.go b/modules/gcloud/spanner/spanner_test.go new file mode 100644 index 0000000000..eafa75d729 --- /dev/null +++ b/modules/gcloud/spanner/spanner_test.go @@ -0,0 +1,105 @@ +package spanner_test + +import ( + "context" + "fmt" + "log" + "testing" + + "cloud.google.com/go/spanner" + database "cloud.google.com/go/spanner/admin/database/apiv1" + databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" + instance "cloud.google.com/go/spanner/admin/instance/apiv1" + instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + "google.golang.org/api/option/internaloption" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/testcontainers/testcontainers-go" + tcspanner "github.com/testcontainers/testcontainers-go/modules/gcloud/spanner" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + spannerContainer, err := tcspanner.Run( + ctx, + "gcr.io/cloud-spanner-emulator/emulator:1.4.0", + tcspanner.WithProjectID("spanner-project"), + ) + testcontainers.CleanupContainer(t, spannerContainer) + require.NoError(t, err) + + projectId := spannerContainer.ProjectID() + + const ( + instanceId = "test-instance" + databaseName = "test-db" + ) + + options := []option.ClientOption{ + option.WithEndpoint(spannerContainer.URI()), + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication(), + internaloption.SkipDialSettingsValidation(), + } + + instanceAdmin, err := instance.NewInstanceAdminClient(ctx, options...) + if err != nil { + log.Printf("failed to create instance admin client: %v", err) + return + } + defer instanceAdmin.Close() + + instanceOp, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ + Parent: "projects/" + projectId, + InstanceId: instanceId, + Instance: &instancepb.Instance{ + DisplayName: instanceId, + }, + }) + require.NoError(t, err) + + _, err = instanceOp.Wait(ctx) + require.NoError(t, err) + + c, err := database.NewDatabaseAdminClient(ctx, options...) + require.NoError(t, err) + defer c.Close() + + databaseOp, err := c.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ + Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), + CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseName), + ExtraStatements: []string{ + "CREATE TABLE Languages (Language STRING(MAX), Mascot STRING(MAX)) PRIMARY KEY (Language)", + }, + }) + require.NoError(t, err) + + _, err = databaseOp.Wait(ctx) + require.NoError(t, err) + + db := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseName) + client, err := spanner.NewClient(ctx, db, options...) + require.NoError(t, err) + defer client.Close() + + _, err = client.Apply(ctx, []*spanner.Mutation{ + spanner.Insert("Languages", + []string{"language", "mascot"}, + []any{"Go", "Gopher"}), + }) + require.NoError(t, err) + + row, err := client.Single().ReadRow(ctx, "Languages", + spanner.Key{"Go"}, []string{"mascot"}) + require.NoError(t, err) + + var mascot string + err = row.ColumnByName("Mascot", &mascot) + require.NoError(t, err) + + require.Equal(t, "Gopher", mascot) +}