diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index dba1f52bebdb..0e2d9952efe9 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -52,25 +52,30 @@ objects: description: | An isolated set of Cloud Spanner resources on which databases can be hosted. + references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Official Documentation': 'https://cloud.google.com/spanner/' + api: 'https://cloud.google.com/spanner/docs/reference/rest/v1/projects.instances' exports: - name transport: !ruby/object:Api::Resource::Transport decoder: decode_response collection_url_response: !ruby/object:Api::Resource::ResponseList items: 'instances' +<%= indent(compile_file({}, 'templates/global_async.yaml.erb'), 4) %> properties: - !ruby/object:Api::Type::String name: 'name' description: | A unique identifier for the instance, which cannot be changed after - the instance is created. Values are of the form - projects//instances/[a-z][-a-z0-9]*[a-z0-9]. The final - segment of the name must be between 6 and 30 characters in length. + the instance is created. The name must be between 6 and 30 characters + in length. - !ruby/object:Api::Type::ResourceRef name: 'config' resource: 'InstanceConfig' imports: 'name' description: 'A reference to the instance configuration.' + required: true - !ruby/object:Api::Type::String name: 'displayName' description: | @@ -80,6 +85,7 @@ objects: - !ruby/object:Api::Type::Integer name: 'nodeCount' description: 'The number of nodes allocated to this instance.' + required: true - !ruby/object:Api::Type::KeyValuePairs name: 'labels' description: | @@ -109,6 +115,17 @@ objects: An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. + - !ruby/object:Api::Type::Enum + name: 'state' + description: | + Instance status: `CREATING` or `READY`. + output: true + # This attribute is not useful - we include it in Terraform for historical + # reasons, but you should most likely not use it. + exclude: true + values: + - :READY + - :CREATING - !ruby/object:Api::Resource name: 'Database' base_url: projects/{{project}}/instances/{{instance}}/databases diff --git a/products/spanner/terraform.yaml b/products/spanner/terraform.yaml index d4000cb82715..4c71ea908420 100644 --- a/products/spanner/terraform.yaml +++ b/products/spanner/terraform.yaml @@ -40,9 +40,36 @@ overrides: !ruby/object:Overrides::ResourceOverrides encoder: templates/terraform/encoders/spanner_database.go.erb decoder: templates/terraform/decoders/spanner_database.go.erb InstanceConfig: !ruby/object:Overrides::Terraform::ResourceOverride + # This is appropriate for a datasource - doesn't have a create method. exclude: true Instance: !ruby/object:Overrides::Terraform::ResourceOverride - exclude: true + id_format: "{{project}}/{{name}}" + import_format: + - "projects/{{project}}/instances/{{name}}" + - "{{project}}/{{name}}" + - "{{name}}" + properties: + displayName: !ruby/object:Overrides::Terraform::PropertyOverride + validation: !ruby/object:Provider::Terraform::Validation + regex: '^(?:[a-z](?:[-_a-z0-9]{2,28}[a-z0-9])?)$' + name: !ruby/object:Overrides::Terraform::PropertyOverride + validation: !ruby/object:Provider::Terraform::Validation + regex: '^(?:[a-z](?:[-_a-z0-9]{4,28}[a-z0-9])?)$' + # This resource supports a sort of odd autogenerate-if-not-set + # system, which is done in the encoder. Consequently we have + # to interpret "not set" as "use the name in state". + default_from_api: true + nodeCount: !ruby/object:Overrides::Terraform::PropertyOverride + name: num_nodes + config: !ruby/object:Overrides::Terraform::PropertyOverride + custom_expand: templates/terraform/custom_expand/spanner_instance_config.go.erb + state: !ruby/object:Overrides::Terraform::PropertyOverride + exclude: false + custom_code: !ruby/object:Provider::Terraform::CustomCode + encoder: templates/terraform/encoders/spanner_instance.go.erb + update_encoder: templates/terraform/encoders/spanner_instance_update.go.erb + decoder: templates/terraform/decoders/spanner_instance.go.erb + post_create: templates/terraform/post_create/sleep.go.erb # This is for copying files over files: !ruby/object:Provider::Config::Files diff --git a/templates/terraform/custom_expand/spanner_instance_config.go b/templates/terraform/custom_expand/spanner_instance_config.go new file mode 100644 index 000000000000..c38e8323a65a --- /dev/null +++ b/templates/terraform/custom_expand/spanner_instance_config.go @@ -0,0 +1,27 @@ +<%- # the license inside this block applies to this file + # Copyright 2018 Google Inc. + # 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. +-%> +func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { + r := regexp.MustCompile("projects/(.+)/notes/(.+)") + if r.MatchString(v.(string)) { + return v.(string), nil + } + + project, err := getProject(d, config) + if err != nil { + return nil, err + } + + return fmt.Sprintf("projects/%s/notes/%s", project, v.(string)), nil +} diff --git a/templates/terraform/custom_expand/spanner_instance_config.go.erb b/templates/terraform/custom_expand/spanner_instance_config.go.erb new file mode 100644 index 000000000000..ac1553bc931d --- /dev/null +++ b/templates/terraform/custom_expand/spanner_instance_config.go.erb @@ -0,0 +1,27 @@ +<%- # the license inside this block applies to this file + # Copyright 2018 Google Inc. + # 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. +-%> +func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { + r := regexp.MustCompile("projects/(.+)/instanceConfigs/(.+)") + if r.MatchString(v.(string)) { + return v.(string), nil + } + + project, err := getProject(d, config) + if err != nil { + return nil, err + } + + return fmt.Sprintf("projects/%s/instanceConfigs/%s", project, v.(string)), nil +} diff --git a/templates/terraform/decoders/spanner_database.go.erb b/templates/terraform/decoders/spanner_database.go.erb index 23ff4238d3c4..b200eb7689f2 100644 --- a/templates/terraform/decoders/spanner_database.go.erb +++ b/templates/terraform/decoders/spanner_database.go.erb @@ -1,13 +1,11 @@ config := meta.(*Config) d.SetId(res["name"].(string)) -log.Printf("[DEBUG] name = %s", res["name"]) if err := parseImportId([]string{"projects/(?P[^/]+)/instances/(?P[^/]+)/databases/(?P[^/]+)"}, d, config); err != nil { return nil, err } res["project"] = d.Get("project").(string) res["instance"] = d.Get("instance").(string) res["name"] = d.Get("name").(string) -log.Printf("[DEBUG] result %#v", res) id, err := replaceVars(d, config, "{{instance}}/{{name}}") if err != nil { return nil, err diff --git a/templates/terraform/decoders/spanner_instance.go.erb b/templates/terraform/decoders/spanner_instance.go.erb new file mode 100644 index 000000000000..774f577c9933 --- /dev/null +++ b/templates/terraform/decoders/spanner_instance.go.erb @@ -0,0 +1,13 @@ +config := meta.(*Config) +d.SetId(res["name"].(string)) +if err := parseImportId([]string{"projects/(?P[^/]+)/instances/(?P[^/]+)"}, d, config); err != nil { + return nil, err +} +res["project"] = d.Get("project").(string) +res["name"] = d.Get("name").(string) +id, err := replaceVars(d, config, "{{project}}/{{name}}") +if err != nil { + return nil, err +} +d.SetId(id) +return res, nil diff --git a/templates/terraform/encoders/spanner_instance.go.erb b/templates/terraform/encoders/spanner_instance.go.erb new file mode 100644 index 000000000000..4dfb7a82d094 --- /dev/null +++ b/templates/terraform/encoders/spanner_instance.go.erb @@ -0,0 +1,10 @@ +newObj := make(map[string]interface{}) +newObj["instance"] = obj +if obj["name"] == nil { + d.Set("name", resource.PrefixedUniqueId("tfgen-spanid-")[:30]) + newObj["instanceId"] = d.Get("name").(string) +} else { + newObj["instanceId"] = obj["name"] +} +delete(obj, "name") +return newObj, nil diff --git a/templates/terraform/encoders/spanner_instance_update.go.erb b/templates/terraform/encoders/spanner_instance_update.go.erb new file mode 100644 index 000000000000..fbb12ccec789 --- /dev/null +++ b/templates/terraform/encoders/spanner_instance_update.go.erb @@ -0,0 +1,19 @@ +project, err := getProject(d, meta.(*Config)) +if err != nil { + return nil, err +} +obj["name"] = fmt.Sprintf("projects/%s/instances/%s", project, obj["name"]) +newObj := make(map[string]interface{}) +newObj["instance"] = obj +updateMask := make([]string, 0) +if d.HasChange("num_nodes") { + updateMask = append(updateMask, "nodeCount") +} +if d.HasChange("display_name") { + updateMask = append(updateMask, "displayName") +} +if d.HasChange("labels") { + updateMask = append(updateMask, "labels") +} +newObj["fieldMask"] = strings.Join(updateMask, ",") +return newObj, nil diff --git a/templates/terraform/post_create/sleep.go.erb b/templates/terraform/post_create/sleep.go.erb new file mode 100644 index 000000000000..defb4f9bb242 --- /dev/null +++ b/templates/terraform/post_create/sleep.go.erb @@ -0,0 +1,3 @@ +// Spanner instances are, ironically, eventually consistent. +// We ensure that the Read sees the instance as created by sleeping, briefly. +time.Sleep(5 * time.Second) diff --git a/third_party/terraform/resources/resource_spanner_instance.go b/third_party/terraform/resources/resource_spanner_instance.go deleted file mode 100644 index cd052f090d82..000000000000 --- a/third_party/terraform/resources/resource_spanner_instance.go +++ /dev/null @@ -1,326 +0,0 @@ -package google - -import ( - "fmt" - "log" - "net/http" - "regexp" - "strings" - - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/helper/schema" - - "google.golang.org/api/googleapi" - "google.golang.org/api/spanner/v1" -) - -func resourceSpannerInstance() *schema.Resource { - return &schema.Resource{ - Create: resourceSpannerInstanceCreate, - Read: resourceSpannerInstanceRead, - Update: resourceSpannerInstanceUpdate, - Delete: resourceSpannerInstanceDelete, - Importer: &schema.ResourceImporter{ - State: resourceSpannerInstanceImportState, - }, - - Schema: map[string]*schema.Schema{ - - "config": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - - "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - - if len(value) < 6 && len(value) > 30 { - errors = append(errors, fmt.Errorf( - "%q must be between 6 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 - }, - }, - - "display_name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - - if len(value) < 4 && len(value) > 30 { - errors = append(errors, fmt.Errorf( - "%q must be between 4 and 30 characters in length", k)) - } - return - }, - }, - - "num_nodes": { - Type: schema.TypeInt, - Optional: true, - Default: 1, - }, - - "labels": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "project": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - - "state": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func resourceSpannerInstanceCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - cir := &spanner.CreateInstanceRequest{ - Instance: &spanner.Instance{}, - } - - if v, ok := d.GetOk("name"); ok { - cir.InstanceId = v.(string) - } else { - cir.InstanceId = genSpannerInstanceName() - d.Set("name", cir.InstanceId) - } - - if v, ok := d.GetOk("labels"); ok { - cir.Instance.Labels = convertStringMap(v.(map[string]interface{})) - } - - id, err := buildSpannerInstanceId(d, config) - if err != nil { - return err - } - - cir.Instance.Config = id.instanceConfigUri(d.Get("config").(string)) - cir.Instance.DisplayName = d.Get("display_name").(string) - cir.Instance.NodeCount = int64(d.Get("num_nodes").(int)) - - op, err := config.clientSpanner.Projects.Instances.Create( - id.parentProjectUri(), cir).Do() - if err != nil { - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusConflict { - return fmt.Errorf("Error, the name %s is not unique within project %s", id.Instance, id.Project) - } - return fmt.Errorf("Error, failed to create instance %s: %s", id.terraformId(), err) - } - - d.SetId(id.terraformId()) - - // Wait until it's created - timeoutMins := int(d.Timeout(schema.TimeoutCreate).Minutes()) - waitErr := spannerInstanceOperationWait(config, op, "Creating Spanner instance", timeoutMins) - if waitErr != nil { - // The resource didn't actually create - d.SetId("") - return waitErr - } - - log.Printf("[INFO] Spanner instance %s has been created", id.terraformId()) - return resourceSpannerInstanceRead(d, meta) -} - -func resourceSpannerInstanceRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - id, err := buildSpannerInstanceId(d, config) - if err != nil { - return err - } - - instance, err := config.clientSpanner.Projects.Instances.Get( - id.instanceUri()).Do() - if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("Spanner instance %s", id.terraformId())) - } - - d.Set("config", GetResourceNameFromSelfLink(instance.Config)) - d.Set("labels", instance.Labels) - d.Set("display_name", instance.DisplayName) - d.Set("num_nodes", instance.NodeCount) - d.Set("state", instance.State) - d.Set("project", id.Project) - - return nil -} - -func resourceSpannerInstanceUpdate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - log.Printf("[INFO] About to update Spanner Instance %s ", d.Id()) - uir := &spanner.UpdateInstanceRequest{ - Instance: &spanner.Instance{}, - } - - id, err := buildSpannerInstanceId(d, config) - if err != nil { - return err - } - - fieldMask := []string{} - if d.HasChange("num_nodes") { - fieldMask = append(fieldMask, "nodeCount") - uir.Instance.NodeCount = int64(d.Get("num_nodes").(int)) - } - if d.HasChange("display_name") { - fieldMask = append(fieldMask, "displayName") - uir.Instance.DisplayName = d.Get("display_name").(string) - } - if d.HasChange("labels") { - fieldMask = append(fieldMask, "labels") - uir.Instance.Labels = convertStringMap(d.Get("labels").(map[string]interface{})) - } - - uir.FieldMask = strings.Join(fieldMask, ",") - op, err := config.clientSpanner.Projects.Instances.Patch( - id.instanceUri(), uir).Do() - if err != nil { - return err - } - - // Wait until it's updated - timeoutMins := int(d.Timeout(schema.TimeoutUpdate).Minutes()) - err = spannerInstanceOperationWait(config, op, "Update Spanner Instance", timeoutMins) - if err != nil { - return err - } - - log.Printf("[INFO] Spanner Instance %s has been updated ", id.terraformId()) - return resourceSpannerInstanceRead(d, meta) -} - -func resourceSpannerInstanceDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - id, err := buildSpannerInstanceId(d, config) - if err != nil { - return err - } - - _, err = config.clientSpanner.Projects.Instances.Delete( - id.instanceUri()).Do() - if err != nil { - return fmt.Errorf("Error, failed to delete Spanner Instance %s in project %s: %s", id.Instance, id.Project, err) - } - - d.SetId("") - return nil -} - -func resourceSpannerInstanceImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - config := meta.(*Config) - id, err := importSpannerInstanceId(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("name", id.Instance) - d.SetId(id.terraformId()) - - return []*schema.ResourceData{d}, nil -} - -func buildSpannerInstanceId(d *schema.ResourceData, config *Config) (*spannerInstanceId, error) { - project, err := getProject(d, config) - if err != nil { - return nil, err - } - return &spannerInstanceId{ - Project: project, - Instance: d.Get("name").(string), - }, nil -} - -func genSpannerInstanceName() string { - return resource.PrefixedUniqueId("tfgen-spanid-")[:30] -} - -type spannerInstanceId struct { - Project string - Instance string -} - -func (s spannerInstanceId) terraformId() string { - return fmt.Sprintf("%s/%s", s.Project, s.Instance) -} - -func (s spannerInstanceId) parentProjectUri() string { - return fmt.Sprintf("projects/%s", s.Project) -} - -func (s spannerInstanceId) instanceUri() string { - return fmt.Sprintf("%s/instances/%s", s.parentProjectUri(), s.Instance) -} - -func (s spannerInstanceId) instanceConfigUri(c string) string { - return fmt.Sprintf("%s/instanceConfigs/%s", s.parentProjectUri(), c) -} - -func importSpannerInstanceId(id string) (*spannerInstanceId, error) { - if !regexp.MustCompile("^[a-z0-9-]+$").Match([]byte(id)) && - !regexp.MustCompile("^"+ProjectRegex+"/[a-z0-9-]+$").Match([]byte(id)) { - return nil, fmt.Errorf("Invalid spanner instance specifier. " + - "Expecting either {projectId}/{instanceId} OR " + - "{instanceId} (where project is to be derived from that specified in provider)") - } - - parts := strings.Split(id, "/") - if len(parts) == 1 { - log.Printf("[INFO] Spanner instance import format of {instanceId} specified: %s", id) - return &spannerInstanceId{Instance: parts[0]}, nil - } - - log.Printf("[INFO] Spanner instance import format of {projectId}/{instanceId} specified: %s", id) - return extractSpannerInstanceId(id) -} - -func extractSpannerInstanceId(id string) (*spannerInstanceId, error) { - if !regexp.MustCompile("^" + ProjectRegex + "/[a-z0-9-]+$").Match([]byte(id)) { - return nil, fmt.Errorf("Invalid spanner id format, expecting {projectId}/{instanceId}") - } - parts := strings.Split(id, "/") - return &spannerInstanceId{ - Project: parts[0], - Instance: parts[1], - }, nil -} diff --git a/third_party/terraform/tests/resource_spanner_instance_test.go b/third_party/terraform/tests/resource_spanner_instance_test.go index c28e01a491aa..f3231b58ae30 100644 --- a/third_party/terraform/tests/resource_spanner_instance_test.go +++ b/third_party/terraform/tests/resource_spanner_instance_test.go @@ -10,8 +10,6 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "strings" - "google.golang.org/api/googleapi" ) @@ -47,95 +45,6 @@ func TestSpannerInstanceId_parentProjectUri(t *testing.T) { expectEquals(t, expected, actual) } -func TestGenSpannerInstanceName(t *testing.T) { - s := genSpannerInstanceName() - if len(s) != 30 { - t.Fatalf("Expected a 30 char ID to be generated, instead found %d chars", len(s)) - } -} - -func TestImportSpannerInstanceId(t *testing.T) { - sid, e := importSpannerInstanceId("instance456") - if e != nil { - t.Errorf("Error should have been nil") - } - expectEquals(t, "", sid.Project) - expectEquals(t, "instance456", sid.Instance) -} - -func TestImportSpannerInstanceId_projectAndInstance(t *testing.T) { - sid, e := importSpannerInstanceId("project123/instance456") - if e != nil { - t.Errorf("Error should have been nil") - } - expectEquals(t, "project123", sid.Project) - expectEquals(t, "instance456", sid.Instance) -} - -func TestImportSpannerInstanceId_invalidLeadingSlash(t *testing.T) { - sid, e := importSpannerInstanceId("/instance456") - expectInvalidSpannerInstanceImport(t, sid, e) -} - -func TestImportSpannerInstanceId_invalidTrailingSlash(t *testing.T) { - sid, e := importSpannerInstanceId("project123/") - expectInvalidSpannerInstanceImport(t, sid, e) -} - -func TestImportSpannerInstanceId_invalidSingleSlash(t *testing.T) { - sid, e := importSpannerInstanceId("/") - expectInvalidSpannerInstanceImport(t, sid, e) -} - -func TestImportSpannerInstanceId_invalidMultiSlash(t *testing.T) { - sid, e := importSpannerInstanceId("project123/instance456/db789") - expectInvalidSpannerInstanceImport(t, sid, e) -} - -func TestImportSpannerInstanceId_projectId(t *testing.T) { - shouldPass := []string{ - "project-id/instance", - "123123/instance", - "hashicorptest.net:project-123/instance", - "123/456", - } - - shouldFail := []string{ - "project-id#/instance", - "project-id/instance#", - "hashicorptest.net:project-123:invalid:project/instance", - "hashicorptest.net:/instance", - } - - for _, element := range shouldPass { - _, e := importSpannerInstanceId(element) - if e != nil { - t.Error("importSpannerInstanceId should pass on '" + element + "' but doesn't") - } - } - - for _, element := range shouldFail { - _, e := importSpannerInstanceId(element) - if e == nil { - t.Error("importSpannerInstanceId should fail on '" + element + "' but doesn't") - } - } -} - -func expectInvalidSpannerInstanceImport(t *testing.T, sid *spannerInstanceId, e error) { - if sid != nil { - t.Errorf("Expected spannerInstanceId to be nil") - return - } - if e == nil { - t.Errorf("Expected an Error but did not get one") - return - } - if !strings.HasPrefix(e.Error(), "Invalid spanner instance specifier") { - t.Errorf("Expecting Error starting with 'Invalid spanner instance specifier'") - } -} - func expectEquals(t *testing.T, expected, actual string) { if actual != expected { t.Fatalf("Expected %s, but got %s", expected, actual) diff --git a/third_party/terraform/utils/iam_spanner_instance.go b/third_party/terraform/utils/iam_spanner_instance.go index c02512060a93..a4787c21ae25 100644 --- a/third_party/terraform/utils/iam_spanner_instance.go +++ b/third_party/terraform/utils/iam_spanner_instance.go @@ -2,6 +2,8 @@ package google import ( "fmt" + "regexp" + "strings" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/schema" @@ -110,3 +112,35 @@ func (u *SpannerInstanceIamUpdater) GetMutexKey() string { func (u *SpannerInstanceIamUpdater) DescribeResource() string { return fmt.Sprintf("Spanner Instance: %s/%s", u.project, u.instance) } + +type spannerInstanceId struct { + Project string + Instance string +} + +func (s spannerInstanceId) terraformId() string { + return fmt.Sprintf("%s/%s", s.Project, s.Instance) +} + +func (s spannerInstanceId) parentProjectUri() string { + return fmt.Sprintf("projects/%s", s.Project) +} + +func (s spannerInstanceId) instanceUri() string { + return fmt.Sprintf("%s/instances/%s", s.parentProjectUri(), s.Instance) +} + +func (s spannerInstanceId) instanceConfigUri(c string) string { + return fmt.Sprintf("%s/instanceConfigs/%s", s.parentProjectUri(), c) +} + +func extractSpannerInstanceId(id string) (*spannerInstanceId, error) { + if !regexp.MustCompile("^" + ProjectRegex + "/[a-z0-9-]+$").Match([]byte(id)) { + return nil, fmt.Errorf("Invalid spanner id format, expecting {projectId}/{instanceId}") + } + parts := strings.Split(id, "/") + return &spannerInstanceId{ + Project: parts[0], + Instance: parts[1], + }, nil +} diff --git a/third_party/terraform/utils/spanner_instance_operation.go b/third_party/terraform/utils/spanner_instance_operation.go index 3c541950c0da..2831cc4833dd 100644 --- a/third_party/terraform/utils/spanner_instance_operation.go +++ b/third_party/terraform/utils/spanner_instance_operation.go @@ -13,9 +13,13 @@ func (w *SpannerInstanceOperationWaiter) QueryOp() (interface{}, error) { return w.Service.Projects.Instances.Operations.Get(w.Op.Name).Do() } -func spannerInstanceOperationWait(config *Config, op *spanner.Operation, activity string, timeoutMinutes int) error { +func spannerOperationWaitTime(spanner *spanner.Service, op *spanner.Operation, _ string, activity string, timeoutMinutes int) error { + if op.Name == "" { + // This was a synchronous call - there is no operation to wait for. + return nil + } w := &SpannerInstanceOperationWaiter{ - Service: config.clientSpanner, + Service: spanner, } if err := w.SetOp(op); err != nil { return err diff --git a/third_party/terraform/website/docs/r/spanner_instance.html.markdown b/third_party/terraform/website/docs/r/spanner_instance.html.markdown deleted file mode 100644 index bf06008275ec..000000000000 --- a/third_party/terraform/website/docs/r/spanner_instance.html.markdown +++ /dev/null @@ -1,73 +0,0 @@ ---- -layout: "google" -page_title: "Google: google_spanner_instance" -sidebar_current: "docs-google-spanner-instance" -description: |- - Creates and manages a Google Spanner Instance. ---- - -# google\_spanner\_instance - -Creates and manages a Google Spanner Instance. For more information, see the [official documentation](https://cloud.google.com/spanner/), or the [JSON API](https://cloud.google.com/spanner/docs/reference/rest/v1/projects.instances). - -## Example Usage - -Example creating a Spanner instance. - -```hcl -resource "google_spanner_instance" "main" { - config = "regional-europe-west1" - display_name = "main-instance" - name = "main-instance" - num_nodes = 1 -} -``` - -## Argument Reference - -The following arguments are supported: - -* `config` - (Required) The name of the instance's configuration (similar but not - quite the same as a region) which defines defines the geographic placement and - replication of your databases in this instance. It determines where your data - is stored. Values are typically of the form `regional-europe-west1` , `us-central` etc. - In order to obtain a valid list please consult the - [Configuration section of the docs](https://cloud.google.com/spanner/docs/instances). - -* `display_name` - (Required) The descriptive name for this instance as it appears - in UIs. Can be updated, however should be kept globally unique to avoid confusion. - -- - - - -* `name` - (Optional, Computed) The unique name (ID) of the instance. If the name is left - blank, Terraform will randomly generate one when the instance is first - created. - -* `num_nodes` - (Optional, Computed) The number of nodes allocated to this instance. - Defaults to `1`. This can be updated after creation. - -* `project` - (Optional) The ID of the project in which the resource belongs. If it - is not provided, the provider project is used. - -* `labels` - (Optional) A mapping (key/value pairs) of labels to assign to the instance. - -## Attributes Reference - -In addition to the arguments listed above, the following computed attributes are -exported: - -* `state` - The current state of the instance. - -## Import - -Instances can be imported using their `name` and optionally -the `project` in which it is defined (Often used when the project is different -to that defined in the provider), The format is thus either `{instanceId}` or -`{projectId}/{instanceId}`. e.g. - -``` -$ terraform import google_spanner_instance.master instance123 - -$ terraform import google_spanner_instance.master project123/instance456 - -```