From 04c578c7e5f025020bea2009b1e687c1c889b3eb Mon Sep 17 00:00:00 2001 From: Joakim Tangnes Date: Fri, 7 Mar 2025 15:10:41 +0100 Subject: [PATCH 1/3] feat(gcloud): add option to run firestore in datastore mode --- docs/modules/gcloud.md | 6 ++ modules/gcloud/firestore/examples_test.go | 67 ++++++++++++++++++++++ modules/gcloud/firestore/firestore.go | 7 ++- modules/gcloud/firestore/firestore_test.go | 46 +++++++++++++++ modules/gcloud/firestore/options.go | 34 +++++++++-- 5 files changed, 153 insertions(+), 7 deletions(-) diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index 364c6f3ce6..beb4480b97 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -176,6 +176,12 @@ In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk {% include "./gcloud-shared.md" %} +### Datastore mode + +Using the `WithDatastoreMode` option will run the Firestore emulator using `Firestore In Datastore` mode allowing you to use Datastore APIs and clients towards the Firestore emulator. + +Requires `cloud-sdk:465.0.0` or higher + ### Examples diff --git a/modules/gcloud/firestore/examples_test.go b/modules/gcloud/firestore/examples_test.go index d06abe3200..697da5db87 100644 --- a/modules/gcloud/firestore/examples_test.go +++ b/modules/gcloud/firestore/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "cloud.google.com/go/datastore" "cloud.google.com/go/firestore" "google.golang.org/api/option" "google.golang.org/grpc" @@ -97,3 +98,69 @@ func ExampleRun() { // Output: // Ada Lovelace } + +func ExampleRun_datastoreMode() { + ctx := context.Background() + + firestoreContainer, err := tcfirestore.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:513.0.0-emulators", + tcfirestore.WithProjectID("firestore-project"), + tcfirestore.WithDatastoreMode(), + ) + defer func() { + if err := testcontainers.TerminateContainer(firestoreContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } + + projectID := firestoreContainer.ProjectID() + + conn, err := grpc.NewClient(firestoreContainer.URI(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(emulatorCreds{})) + if err != nil { + log.Printf("failed to dial: %v", err) + return + } + + options := []option.ClientOption{option.WithGRPCConn(conn)} + client, err := datastore.NewClient(ctx, projectID, options...) + if err != nil { + log.Printf("failed to create client: %v", err) + return + } + defer client.Close() + + userKey := datastore.NameKey("users", "alovelace", nil) + + type Person struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + } + + data := Person{ + Firstname: "Ada", + Lastname: "Lovelace", + } + + _, err = client.Put(ctx, userKey, &data) + if err != nil { + log.Printf("failed to create entity: %v", err) + return + } + + saved := Person{} + err = client.Get(ctx, userKey, &saved) + if err != nil { + log.Printf("failed to get entity: %v", err) + return + } + + fmt.Println(saved.Firstname, saved.Lastname) + + // Output: + // Ada Lovelace +} diff --git a/modules/gcloud/firestore/firestore.go b/modules/gcloud/firestore/firestore.go index cade5dd48a..1f7f417626 100644 --- a/modules/gcloud/firestore/firestore.go +++ b/modules/gcloud/firestore/firestore.go @@ -56,10 +56,15 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } } + gcloudParameters := "--project=" + settings.ProjectID + if settings.datastoreMode { + gcloudParameters += " --database-mode=datastore-mode" + } + req.Cmd = []string{ "/bin/sh", "-c", - "gcloud beta emulators firestore start --host-port 0.0.0.0:8080 --project=" + settings.ProjectID, + "gcloud beta emulators firestore start --host-port 0.0.0.0:8080 " + gcloudParameters, } container, err := testcontainers.GenericContainer(ctx, req) diff --git a/modules/gcloud/firestore/firestore_test.go b/modules/gcloud/firestore/firestore_test.go index a98a52b2dc..c1db5c0e18 100644 --- a/modules/gcloud/firestore/firestore_test.go +++ b/modules/gcloud/firestore/firestore_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "cloud.google.com/go/datastore" "cloud.google.com/go/firestore" "github.com/stretchr/testify/require" "google.golang.org/api/option" @@ -59,3 +60,48 @@ func TestRun(t *testing.T) { require.Equal(t, "Ada", saved.Firstname) require.Equal(t, "Lovelace", saved.Lastname) } + +func TestRunWithDatastore(t *testing.T) { + ctx := context.Background() + + firestoreContainer, err := tcfirestore.Run( + ctx, + "gcr.io/google.com/cloudsdktool/cloud-sdk:513.0.0-emulators", + tcfirestore.WithProjectID("firestore-project"), + tcfirestore.WithDatastoreMode(), + ) + 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 := datastore.NewClient(ctx, projectID, options...) + require.NoError(t, err) + defer client.Close() + + userKey := datastore.NameKey("users", "alovelace", nil) + + type Person struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + } + + data := Person{ + Firstname: "Ada", + Lastname: "Lovelace", + } + + _, err = client.Put(ctx, userKey, &data) + require.NoError(t, err) + + saved := Person{} + err = client.Get(ctx, userKey, &saved) + require.NoError(t, err) + + 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 index 8c6f32a947..1b4cb24331 100644 --- a/modules/gcloud/firestore/options.go +++ b/modules/gcloud/firestore/options.go @@ -1,16 +1,38 @@ package firestore -import "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" +import ( + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/gcloud/internal/shared" +) -// Options aliases the common GCloud options -type options = shared.Options +// options embeds the common GCloud options +type options struct { + shared.Options + datastoreMode bool +} + +type Option func(o *options) error -// Option aliases the common GCloud option type -type Option = shared.Option +// 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 shared.DefaultOptions() + return options{ + Options: shared.DefaultOptions(), + } +} + +// WithDatastoreMode sets the firestore emulator to run in datastore mode. +// Requires a cloud-sdk image with version 465.0.0 or higher +func WithDatastoreMode() Option { + return func(o *options) error { + o.datastoreMode = true + return nil + } } // WithProjectID re-exports the common GCloud WithProjectID option From 35602e3bbdc224658aec1595cc9f1492750d1b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 25 Apr 2025 11:19:19 +0200 Subject: [PATCH 2/3] docs: include release marker for the option --- docs/modules/gcloud.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index beb4480b97..60a030d032 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -178,6 +178,9 @@ In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk ### Datastore mode +- Not available until the next release of testcontainers-go :material-tag: main + + Using the `WithDatastoreMode` option will run the Firestore emulator using `Firestore In Datastore` mode allowing you to use Datastore APIs and clients towards the Firestore emulator. Requires `cloud-sdk:465.0.0` or higher From 066c693b3b5cd1989a3b5db9a789b9c6532acb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 25 Apr 2025 11:19:43 +0200 Subject: [PATCH 3/3] fix: remove whiteline --- docs/modules/gcloud.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index 60a030d032..00025457fb 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -180,7 +180,6 @@ In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk - Not available until the next release of testcontainers-go :material-tag: main - Using the `WithDatastoreMode` option will run the Firestore emulator using `Firestore In Datastore` mode allowing you to use Datastore APIs and clients towards the Firestore emulator. Requires `cloud-sdk:465.0.0` or higher