Skip to content

Commit

Permalink
feat: create standalone samples that run against emulator (#30)
Browse files Browse the repository at this point in the history
* feat: create standalone samples that run against emulator

* samples: refactor transactions sample to use emulator
  • Loading branch information
olavloite committed Sep 6, 2021
1 parent f260dd2 commit 22b127e
Show file tree
Hide file tree
Showing 7 changed files with 1,472 additions and 68 deletions.
21 changes: 21 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Spanner Go Sql Examples

This directory contains samples for how to use the Spanner go-sql driver. Each sample can be executed
as a standalone application without the need for any prior setup, other than that Docker must be installed
on your system. Each sample will automatically:
1. Download and start the [Spanner Emulator](https://cloud.google.com/spanner/docs/emulator) in a Docker container.
2. Create a sample database and execute the sample on the sample database.
3. Shutdown the Docker container that is running the emulator.

Running a sample is done by navigating to the corresponding sample directory and executing the following command:

```shell
# Change the 'helloword' directory below to any of the samples in this directory.
cd helloworld
go run main.go
```

## Prerequisites

Your system must have [Docker installed](https://docs.docker.com/get-docker/) for these samples to be executed,
as each sample will automatically start the Spanner Emulator in a Docker container.
162 changes: 162 additions & 0 deletions examples/emulator_runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package examples

import (
"context"
"fmt"
"log"
"os"
"time"

database "cloud.google.com/go/spanner/admin/database/apiv1"
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
instancepb "google.golang.org/genproto/googleapis/spanner/admin/instance/v1"
)

var cli *client.Client
var containerId string

func RunSampleOnEmulator(sample func(string, string, string) error, ddlStatements ...string) {
var err error
if err = startEmulator(); err != nil {
log.Fatalf("failed to start emulator: %v", err)
}
projectId, instanceId, databaseId := "my-project", "my-instance", "my-database"
if err = createInstance(projectId, instanceId); err != nil {
stopEmulator()
log.Fatalf("failed to create instance on emulator: %v", err)
}
if err = createSampleDB(projectId, instanceId, databaseId, ddlStatements...); err != nil {
stopEmulator()
log.Fatalf("failed to create database on emulator: %v", err)
}
err = sample(projectId, instanceId, databaseId)
stopEmulator()
if err != nil {
log.Fatal(err)
}
}

func startEmulator() error {
ctx := context.Background()
if err := os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010"); err != nil {
return err
}

// Initialize a Docker client.
var err error
cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
// Pull the Spanner Emulator docker image.
if _, err := cli.ImagePull(ctx, "gcr.io/cloud-spanner-emulator/emulator", types.ImagePullOptions{}); err != nil {
return err
}
// Create and start a container with the emulator.
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "gcr.io/cloud-spanner-emulator/emulator",
ExposedPorts: nat.PortSet{"9010": {}},
}, &container.HostConfig{
PortBindings: map[nat.Port][]nat.PortBinding{"9010": {{HostIP: "0.0.0.0", HostPort: "9010"}}},
}, nil, nil, "")
if err != nil {
return err
}
containerId = resp.ID
if err := cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{}); err != nil {
return err
}
// Wait max 10 seconds or until the emulator is running.
for c := 0; c < 20; c++ {
// Always wait at least 500 milliseconds to ensure that the emulator is actually ready, as the
// state can be reported as ready, while the emulator (or network interface) is actually not ready.
<-time.After(500 * time.Millisecond)
resp, err := cli.ContainerInspect(ctx, containerId)
if err != nil {
return fmt.Errorf("failed to inspect container state: %v", err)
}
if resp.State.Running {
break
}
}

return nil
}

func createInstance(projectId, instanceId string) error {
ctx := context.Background()
instanceAdmin, err := instance.NewInstanceAdminClient(ctx)
if err != nil {
return err
}
defer instanceAdmin.Close()
op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{
Parent: fmt.Sprintf("projects/%s", projectId),
InstanceId: instanceId,
Instance: &instancepb.Instance{
Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectId, "emulator-config"),
DisplayName: instanceId,
NodeCount: 1,
},
})
if err != nil {
return fmt.Errorf("could not create instance %s: %v", fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), err)
}
// Wait for the instance creation to finish.
if _, err := op.Wait(ctx); err != nil {
return fmt.Errorf("waiting for instance creation to finish failed: %v", err)
}
return nil
}

func createSampleDB(projectId, instanceId, databaseId string, statements ...string) error {
ctx := context.Background()
databaseAdminClient, err := database.NewDatabaseAdminClient(ctx)
if err != nil {
return err
}
defer databaseAdminClient.Close()
opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId),
CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId),
ExtraStatements: statements,
})
if err != nil {
return err
}
// Wait for the database creation to finish.
if _, err := opDB.Wait(ctx); err != nil {
return fmt.Errorf("waiting for database creation to finish failed: %v", err)
}
return nil
}

func stopEmulator() {
if cli == nil || containerId == "" {
return
}
ctx := context.Background()
timeout := 10 * time.Second
if err := cli.ContainerStop(ctx, containerId, &timeout); err != nil {
log.Printf("failed to stop emulator: %v\n", err)
}
}
17 changes: 17 additions & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/cloudspannerecosystem/go-sql-spanner/examples

go 1.14

replace github.com/cloudspannerecosystem/go-sql-spanner => ../

require (
cloud.google.com/go/spanner v1.23.1-0.20210727075241-3d6c6c7873e1
github.com/cloudspannerecosystem/go-sql-spanner v0.0.0-00010101000000-000000000000
github.com/containerd/containerd v1.5.5 // indirect
github.com/docker/docker v20.10.8+incompatible
github.com/docker/go-connections v0.4.0
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
google.golang.org/genproto v0.0.0-20210726143408-b02e89920bf0
)
Loading

0 comments on commit 22b127e

Please sign in to comment.