-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
rosbo
merged 14 commits into
hashicorp:master
from
nickithewatt:spanner-support-database
Aug 14, 2017
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
205d7e7
Add google_spanner_instance resource
nickithewatt dd1b3f2
Add google_spanner_instance import functionality and tests
nickithewatt 683eb4e
Add google_spanner_instance docs
nickithewatt 121e64b
Code cleanup
nickithewatt 0fcb3aa
Adjust resource ID format to handle uniqueness within project (not gl…
nickithewatt d233e5f
Correct label config & minor method name refactoring
nickithewatt 76b2e6f
Added state as computed attribute
nickithewatt 4bbe53f
Check for patch error
nickithewatt c45595f
Add google_spanner_database resource and tests
nickithewatt 06f92f9
Add google_spanner_database docs
nickithewatt f7d7384
Adjust resource ID: handle uniqueness within project/instance (not
nickithewatt ddbabb6
Add computed state attribute
nickithewatt 1ad886b
Merge branch 'master' into spanner-support-database
rosbo 73ff2a9
Rename import spanner test
rosbo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, add the |
||
|
||
"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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
to this:
Would fail with an Error code 3
Duplicate name in schema: t1
There was a problem hiding this comment.
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.