Skip to content

Commit 4193437

Browse files
authored
feat: add AutoConfigEmulator option (#395)
Adds an AutoConfigEmulator option to the database/sql driver. Setting this option instructs the driver to automatically connect to the emulator and create the instance and database on the emulator if they do not already exist. Fixes #384
1 parent 0dc0845 commit 4193437

File tree

7 files changed

+342
-26
lines changed

7 files changed

+342
-26
lines changed

driver.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,16 @@ type ConnectorConfig struct {
205205
Instance string
206206
Database string
207207

208+
// AutoConfigEmulator automatically creates a connection for the emulator
209+
// and also automatically creates the Instance and Database on the emulator.
210+
// Setting this option to true will:
211+
// 1. Set the SPANNER_EMULATOR_HOST environment variable to either Host or
212+
// 'localhost:9010' if no other host has been set.
213+
// 2. Use plain text communication and NoCredentials.
214+
// 3. Automatically create the Instance and the Database on the emulator if
215+
// any of those do not yet exist.
216+
AutoConfigEmulator bool
217+
208218
// Params contains key/value pairs for commonly used configuration parameters
209219
// for connections. The valid values are the same as the parameters that can
210220
// be added to a connection string.
@@ -452,6 +462,11 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
452462
connectorConfig.DecodeToNativeArrays = val
453463
}
454464
}
465+
if strval, ok := connectorConfig.Params[strings.ToLower("AutoConfigEmulator")]; ok {
466+
if val, err := strconv.ParseBool(strval); err == nil {
467+
connectorConfig.AutoConfigEmulator = val
468+
}
469+
}
455470
config.UserAgent = userAgent
456471
var logger *slog.Logger
457472
if connectorConfig.logger == nil {
@@ -468,6 +483,11 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
468483
if connectorConfig.Configurator != nil {
469484
connectorConfig.Configurator(&config, &opts)
470485
}
486+
if connectorConfig.AutoConfigEmulator {
487+
if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database); err != nil {
488+
return nil, err
489+
}
490+
}
471491

472492
c := &connector{
473493
driver: d,

emulator_util.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2025 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package spannerdriver
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
22+
"cloud.google.com/go/spanner"
23+
database "cloud.google.com/go/spanner/admin/database/apiv1"
24+
databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
25+
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
26+
instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
27+
"google.golang.org/grpc/codes"
28+
)
29+
30+
func autoConfigEmulator(ctx context.Context, host, project, instance, database string) error {
31+
if host == "" {
32+
host = "localhost:9010"
33+
}
34+
if err := os.Setenv("SPANNER_EMULATOR_HOST", host); err != nil {
35+
return err
36+
}
37+
if err := createInstance(project, instance); err != nil {
38+
if spanner.ErrCode(err) != codes.AlreadyExists {
39+
return err
40+
}
41+
}
42+
if err := createDatabase(project, instance, database); err != nil {
43+
if spanner.ErrCode(err) != codes.AlreadyExists {
44+
return err
45+
}
46+
}
47+
return nil
48+
}
49+
50+
func createInstance(projectId, instanceId string) error {
51+
ctx := context.Background()
52+
instanceAdmin, err := instance.NewInstanceAdminClient(ctx)
53+
if err != nil {
54+
return err
55+
}
56+
defer instanceAdmin.Close()
57+
op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{
58+
Parent: fmt.Sprintf("projects/%s", projectId),
59+
InstanceId: instanceId,
60+
Instance: &instancepb.Instance{
61+
Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectId, "emulator-config"),
62+
DisplayName: instanceId,
63+
NodeCount: 1,
64+
},
65+
})
66+
if err != nil {
67+
return fmt.Errorf("could not create instance %s: %v", fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), err)
68+
}
69+
// Wait for the instance creation to finish.
70+
if _, err := op.Wait(ctx); err != nil {
71+
return fmt.Errorf("waiting for instance creation to finish failed: %v", err)
72+
}
73+
return nil
74+
}
75+
76+
func createDatabase(projectId, instanceId, databaseId string) error {
77+
ctx := context.Background()
78+
databaseAdminClient, err := database.NewDatabaseAdminClient(ctx)
79+
if err != nil {
80+
return err
81+
}
82+
defer databaseAdminClient.Close()
83+
opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
84+
Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId),
85+
CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId),
86+
})
87+
if err != nil {
88+
return err
89+
}
90+
// Wait for the database creation to finish.
91+
if _, err := opDB.Wait(ctx); err != nil {
92+
return fmt.Errorf("waiting for database creation to finish failed: %v", err)
93+
}
94+
return nil
95+
}

examples/emulator/main.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2025 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
"fmt"
21+
"log"
22+
23+
"github.com/docker/docker/api/types/container"
24+
spannerdriver "github.com/googleapis/go-sql-spanner"
25+
"github.com/testcontainers/testcontainers-go"
26+
"github.com/testcontainers/testcontainers-go/wait"
27+
)
28+
29+
// Sample application that shows how to use the Spanner Go sql driver to connect
30+
// to the Spanner Emulator and automatically create the instance and database
31+
// from the connection string on the Emulator.
32+
//
33+
// Execute the sample with the command `go run main.go` from this directory.
34+
func emulator(projectId, instanceId, databaseId string) error {
35+
ctx := context.Background()
36+
37+
// Start the Spanner emulator in a Docker container.
38+
emulator, host, err := startEmulator()
39+
if err != nil {
40+
return err
41+
}
42+
defer func() { _ = emulator.Terminate(context.Background()) }()
43+
44+
config := spannerdriver.ConnectorConfig{
45+
// AutoConfigEmulator instructs the driver to:
46+
// 1. Connect to the emulator using plain text.
47+
// 2. Create the instance and database if they do not already exist.
48+
AutoConfigEmulator: true,
49+
50+
// You only have to set the host if it is different from the default
51+
// 'localhost:9010' host for the Spanner emulator.
52+
Host: host,
53+
54+
// The instance and database will automatically be created on the Emulator.
55+
Project: projectId,
56+
Instance: instanceId,
57+
Database: databaseId,
58+
}
59+
connector, err := spannerdriver.CreateConnector(config)
60+
if err != nil {
61+
return err
62+
}
63+
db := sql.OpenDB(connector)
64+
defer func() { _ = db.Close() }()
65+
66+
rows, err := db.QueryContext(ctx, "SELECT 'Hello World!'")
67+
if err != nil {
68+
return fmt.Errorf("failed to execute query: %v", err)
69+
}
70+
defer rows.Close()
71+
72+
var msg string
73+
for rows.Next() {
74+
if err := rows.Scan(&msg); err != nil {
75+
return fmt.Errorf("failed to scan row values: %v", err)
76+
}
77+
fmt.Printf("%s\n", msg)
78+
}
79+
if err := rows.Err(); err != nil {
80+
return fmt.Errorf("failed to iterate over query results: %v", err)
81+
}
82+
return nil
83+
}
84+
85+
func main() {
86+
if err := emulator("emulator-project", "test-instance", "test-database"); err != nil {
87+
log.Fatal(err)
88+
}
89+
}
90+
91+
// startEmulator starts the Spanner Emulator in a Docker container.
92+
func startEmulator() (testcontainers.Container, string, error) {
93+
ctx := context.Background()
94+
req := testcontainers.ContainerRequest{
95+
AlwaysPullImage: true,
96+
Image: "gcr.io/cloud-spanner-emulator/emulator",
97+
ExposedPorts: []string{"9010/tcp"},
98+
WaitingFor: wait.ForAll(wait.ForListeningPort("9010/tcp"), wait.ForLog("gRPC server listening")),
99+
HostConfigModifier: func(hostConfig *container.HostConfig) {
100+
hostConfig.AutoRemove = true
101+
},
102+
}
103+
emulator, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
104+
ContainerRequest: req,
105+
Started: true,
106+
})
107+
if err != nil {
108+
return emulator, "", fmt.Errorf("failed to start the emulator: %v", err)
109+
}
110+
host, err := emulator.Host(ctx)
111+
if err != nil {
112+
return emulator, "", fmt.Errorf("failed to get host: %v", err)
113+
}
114+
mappedPort, err := emulator.MappedPort(ctx, "9010/tcp")
115+
if err != nil {
116+
return emulator, "", fmt.Errorf("failed to get mapped port: %v", err)
117+
}
118+
port := mappedPort.Int()
119+
120+
return emulator, fmt.Sprintf("%s:%v", host, port), nil
121+
}

examples/emulator_runner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import (
2626
databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
2727
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
2828
instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
29-
"github.com/docker/docker/api/types"
3029
"github.com/docker/docker/api/types/container"
30+
"github.com/docker/docker/api/types/image"
3131
"github.com/docker/docker/client"
3232
"github.com/docker/go-connections/nat"
3333
)
@@ -77,7 +77,7 @@ func startEmulator() error {
7777
return err
7878
}
7979
// Pull the Spanner Emulator docker image.
80-
reader, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", types.ImagePullOptions{})
80+
reader, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", image.PullOptions{})
8181
if err != nil {
8282
return err
8383
}

examples/go.mod

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ replace github.com/googleapis/go-sql-spanner => ../
99
require (
1010
cloud.google.com/go v0.119.0
1111
cloud.google.com/go/spanner v1.77.0
12-
github.com/docker/docker v25.0.6+incompatible
13-
github.com/docker/go-connections v0.4.0
12+
github.com/docker/docker v27.1.1+incompatible
13+
github.com/docker/go-connections v0.5.0
1414
github.com/googleapis/go-sql-spanner v1.0.1
15+
github.com/testcontainers/testcontainers-go v0.35.0
1516
google.golang.org/api v0.226.0
1617
)
1718

@@ -23,31 +24,55 @@ require (
2324
cloud.google.com/go/iam v1.4.0 // indirect
2425
cloud.google.com/go/longrunning v0.6.6 // indirect
2526
cloud.google.com/go/monitoring v1.24.0 // indirect
27+
dario.cat/mergo v1.0.0 // indirect
28+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
2629
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect
2730
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
28-
github.com/Microsoft/go-winio v0.6.1 // indirect
31+
github.com/Microsoft/go-winio v0.6.2 // indirect
32+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
2933
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3034
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
35+
github.com/containerd/containerd v1.7.18 // indirect
3136
github.com/containerd/log v0.1.0 // indirect
32-
github.com/distribution/reference v0.5.0 // indirect
37+
github.com/containerd/platforms v0.2.1 // indirect
38+
github.com/cpuguy83/dockercfg v0.3.2 // indirect
39+
github.com/davecgh/go-spew v1.1.1 // indirect
40+
github.com/distribution/reference v0.6.0 // indirect
3341
github.com/docker/go-units v0.5.0 // indirect
3442
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
3543
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
3644
github.com/felixge/httpsnoop v1.0.4 // indirect
3745
github.com/go-logr/logr v1.4.2 // indirect
3846
github.com/go-logr/stdr v1.2.2 // indirect
47+
github.com/go-ole/go-ole v1.2.6 // indirect
3948
github.com/gogo/protobuf v1.3.2 // indirect
4049
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4150
github.com/google/s2a-go v0.1.9 // indirect
4251
github.com/google/uuid v1.6.0 // indirect
4352
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
4453
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
54+
github.com/klauspost/compress v1.17.4 // indirect
55+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
56+
github.com/magiconair/properties v1.8.7 // indirect
57+
github.com/moby/docker-image-spec v1.3.1 // indirect
58+
github.com/moby/patternmatcher v0.6.0 // indirect
59+
github.com/moby/sys/sequential v0.5.0 // indirect
60+
github.com/moby/sys/user v0.1.0 // indirect
4561
github.com/moby/term v0.5.0 // indirect
4662
github.com/morikuni/aec v1.0.0 // indirect
4763
github.com/opencontainers/go-digest v1.0.0 // indirect
48-
github.com/opencontainers/image-spec v1.0.2 // indirect
64+
github.com/opencontainers/image-spec v1.1.0 // indirect
4965
github.com/pkg/errors v0.9.1 // indirect
5066
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
67+
github.com/pmezard/go-difflib v1.0.0 // indirect
68+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
69+
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
70+
github.com/shoenig/go-m1cpu v0.1.6 // indirect
71+
github.com/sirupsen/logrus v1.9.3 // indirect
72+
github.com/stretchr/testify v1.10.0 // indirect
73+
github.com/tklauser/go-sysconf v0.3.12 // indirect
74+
github.com/tklauser/numcpus v0.6.1 // indirect
75+
github.com/yusufpapurcu/wmi v1.2.3 // indirect
5176
go.opencensus.io v0.24.0 // indirect
5277
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
5378
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
@@ -60,18 +85,16 @@ require (
6085
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
6186
go.opentelemetry.io/otel/trace v1.34.0 // indirect
6287
golang.org/x/crypto v0.36.0 // indirect
63-
golang.org/x/mod v0.17.0 // indirect
6488
golang.org/x/net v0.37.0 // indirect
6589
golang.org/x/oauth2 v0.28.0 // indirect
6690
golang.org/x/sync v0.12.0 // indirect
6791
golang.org/x/sys v0.31.0 // indirect
6892
golang.org/x/text v0.23.0 // indirect
6993
golang.org/x/time v0.11.0 // indirect
70-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
7194
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
7295
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
7396
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
7497
google.golang.org/grpc v1.71.0 // indirect
7598
google.golang.org/protobuf v1.36.5 // indirect
76-
gotest.tools/v3 v3.5.1 // indirect
99+
gopkg.in/yaml.v3 v3.0.1 // indirect
77100
)

0 commit comments

Comments
 (0)