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

Add Google Spanner Support (google_spanner_database) #271

Merged
merged 14 commits into from
Aug 14, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
57 changes: 57 additions & 0 deletions google/import_spanner_database_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package google

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)

func TestAccSpannerDatabase_importInstanceDatabase(t *testing.T) {
resourceName := "google_spanner_database.basic"
instanceName := fmt.Sprintf("span-iname-%s", acctest.RandString(10))
dbName := fmt.Sprintf("span-dbname-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpannerDatabaseDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccSpannerDatabase_basicImport(instanceName, dbName),
},

resource.TestStep{
ResourceName: resourceName,
ImportStateId: instanceName + "/" + dbName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccSpannerDatabase_importProjectInstanceDatabase(t *testing.T) {
resourceName := "google_spanner_database.basic"
instanceName := fmt.Sprintf("span-iname-%s", acctest.RandString(10))
dbName := fmt.Sprintf("span-dbname-%s", acctest.RandString(10))
var projectId = multiEnvSearch([]string{"GOOGLE_PROJECT", "GCLOUD_PROJECT", "CLOUDSDK_CORE_PROJECT"})

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpannerDatabaseDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccSpannerDatabase_basicImportWithProject(projectId, instanceName, dbName),
},

resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func Provider() terraform.ResourceProvider {
"google_dns_record_set": resourceDnsRecordSet(),
"google_sourcerepo_repository": resourceSourceRepoRepository(),
"google_spanner_instance": resourceSpannerInstance(),
"google_spanner_database": resourceSpannerDatabase(),
"google_sql_database": resourceSqlDatabase(),
"google_sql_database_instance": resourceSqlDatabaseInstance(),
"google_sql_user": resourceSqlUser(),
Expand Down
244 changes: 244 additions & 0 deletions google/resource_spanner_database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package google

import (
"fmt"
"log"
"net/http"
"regexp"
"strings"

"github.com/hashicorp/terraform/helper/schema"

"google.golang.org/api/googleapi"
"google.golang.org/api/spanner/v1"
)

func resourceSpannerDatabase() *schema.Resource {
return &schema.Resource{
Create: resourceSpannerDatabaseCreate,
Read: resourceSpannerDatabaseRead,
Delete: resourceSpannerDatabaseDelete,
Importer: &schema.ResourceImporter{
State: resourceSpannerDatabaseImportState,
},

Schema: map[string]*schema.Schema{

"instance": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

if len(value) < 2 && len(value) > 30 {
errors = append(errors, fmt.Errorf(
"%q must be between 2 and 30 characters in length", k))
}
if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q can only contain lowercase letters, numbers and hyphens", k))
}
if !regexp.MustCompile("^[a-z]").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must start with a letter", k))
}
if !regexp.MustCompile("[a-z0-9]$").MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must end with a number or a letter", k))
}
return
},
},

"project": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},

"ddl": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an update method for this. If you decide not to implement it in this PR, please file an issue and add a TODO here link to the issue.

Copy link
Contributor Author

@nickithewatt nickithewatt Aug 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure an update makes sense here? My understanding is that the DDL update here is more additive style statements to apply to the database, rather than the definitive list of statements, i.e. could include update statements as well. Without support for clauses like DROP IF EXISTS, this could be quite hard to make idempotent.

Moving from this:

resource "google_spanner_database" "db1" {
  instance = "${google_spanner_instance.instance1.name}"
  name     = "mydb1"

  ddl           =  [
    "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
    "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)"]
}

to this:

resource "google_spanner_database" "db1" {
  instance = "${google_spanner_instance.instance1.name}"
  name     = "mydb1"

  ddl           =  [
    "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
    "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
    "CREATE TABLE t3 (t3 INT64 NOT NULL,) PRIMARY KEY(t3)"]
}

Would fail with an Error code 3 Duplicate name in schema: t1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. We should leave the ForceNew.

Elem: &schema.Schema{Type: schema.TypeString},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add the Computed state field.


"state": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceSpannerDatabaseCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

id, err := buildSpannerDatabaseId(d, config)
if err != nil {
return err
}

cdr := &spanner.CreateDatabaseRequest{}
cdr.CreateStatement = fmt.Sprintf("CREATE DATABASE `%s`", id.Database)
if v, ok := d.GetOk("ddl"); ok {
cdr.ExtraStatements = convertStringArr(v.([]interface{}))
}

op, err := config.clientSpanner.Projects.Instances.Databases.Create(
id.parentInstanceUri(), cdr).Do()
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusConflict {
return fmt.Errorf("Error, A database with name %s already exists in this instance", id.Database)
}
return fmt.Errorf("Error, failed to create database %s: %s", id.Database, err)
}

d.SetId(id.terraformId())

// Wait until it's created
timeoutMins := int(d.Timeout(schema.TimeoutCreate).Minutes())
waitErr := spannerDatabaseOperationWait(config, op, "Creating Spanner database", timeoutMins)
if waitErr != nil {
// The resource didn't actually create
d.SetId("")
return waitErr
}

log.Printf("[INFO] Spanner database %s has been created", id.terraformId())
return resourceSpannerDatabaseRead(d, meta)
}

func resourceSpannerDatabaseRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

id, err := buildSpannerDatabaseId(d, config)
if err != nil {
return err
}

db, err := config.clientSpanner.Projects.Instances.Databases.Get(
id.databaseUri()).Do()
if err != nil {
return handleNotFoundError(err, d, fmt.Sprintf("Spanner database %q", id.databaseUri()))
}

d.Set("state", db.State)
return nil
}

func resourceSpannerDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

id, err := buildSpannerDatabaseId(d, config)
if err != nil {
return err
}

_, err = config.clientSpanner.Projects.Instances.Databases.DropDatabase(
id.databaseUri()).Do()
if err != nil {
return fmt.Errorf("Error, failed to delete Spanner Database %s: %s", id.databaseUri(), err)
}

d.SetId("")
return nil
}

func resourceSpannerDatabaseImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
config := meta.(*Config)
id, err := importSpannerDatabaseId(d.Id())
if err != nil {
return nil, err
}

if id.Project != "" {
d.Set("project", id.Project)
} else {
project, err := getProject(d, config)
if err != nil {
return nil, err
}
id.Project = project
}

d.Set("instance", id.Instance)
d.Set("name", id.Database)
d.SetId(id.terraformId())

return []*schema.ResourceData{d}, nil
}

func buildSpannerDatabaseId(d *schema.ResourceData, config *Config) (*spannerDatabaseId, error) {
project, err := getProject(d, config)
if err != nil {
return nil, err
}
dbName := d.Get("name").(string)
instanceName := d.Get("instance").(string)

return &spannerDatabaseId{
Project: project,
Instance: instanceName,
Database: dbName,
}, nil
}

type spannerDatabaseId struct {
Project string
Instance string
Database string
}

func (s spannerDatabaseId) terraformId() string {
return fmt.Sprintf("%s/%s/%s", s.Project, s.Instance, s.Database)
}

func (s spannerDatabaseId) parentProjectUri() string {
return fmt.Sprintf("projects/%s", s.Project)
}

func (s spannerDatabaseId) parentInstanceUri() string {
return fmt.Sprintf("%s/instances/%s", s.parentProjectUri(), s.Instance)
}

func (s spannerDatabaseId) databaseUri() string {
return fmt.Sprintf("%s/databases/%s", s.parentInstanceUri(), s.Database)
}

func importSpannerDatabaseId(id string) (*spannerDatabaseId, error) {
if !regexp.MustCompile("^[a-z0-9-]+/[a-z0-9-]+$").Match([]byte(id)) &&
!regexp.MustCompile("^[a-z0-9-]+/[a-z0-9-]+/[a-z0-9-]+$").Match([]byte(id)) {
return nil, fmt.Errorf("Invalid spanner database specifier. " +
"Expecting either {projectId}/{instanceId}/{dbId} OR " +
"{instanceId}/{dbId} (where project will be derived from the provider)")
}

parts := strings.Split(id, "/")
if len(parts) == 2 {
log.Printf("[INFO] Spanner database import format of {instanceId}/{dbId} specified: %s", id)
return &spannerDatabaseId{Instance: parts[0], Database: parts[1]}, nil
}

log.Printf("[INFO] Spanner database import format of {projectId}/{instanceId}/{dbId} specified: %s", id)
return extractSpannerDatabaseId(id)
}

func extractSpannerDatabaseId(id string) (*spannerDatabaseId, error) {
if !regexp.MustCompile("^[a-z0-9-]+/[a-z0-9-]+/[a-z0-9-]+$").Match([]byte(id)) {
return nil, fmt.Errorf("Invalid spanner id format, expecting {projectId}/{instanceId}/{databaseId}")
}
parts := strings.Split(id, "/")
return &spannerDatabaseId{
Project: parts[0],
Instance: parts[1],
Database: parts[2],
}, nil
}
Loading