Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create standalone samples that run against emulator #30

Merged
merged 8 commits into from
Sep 6, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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