From 31cc3f3185f4c74011cf76d1a36420c2691bda8e Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Thu, 17 Jan 2019 11:40:59 -0800 Subject: [PATCH 1/9] Initial autogeneration of Spanner instance. --- products/spanner/api.yaml | 11 + products/spanner/terraform.yaml | 26 +- .../custom_expand/spanner_instance_config.go | 27 ++ .../spanner_instance_config.go.erb | 27 ++ .../decoders/spanner_database.go.erb | 2 - .../decoders/spanner_instance.go.erb | 13 + .../encoders/spanner_instance.go.erb | 10 + .../encoders/spanner_instance_update.go.erb | 19 + templates/terraform/post_create/sleep.go.erb | 3 + .../resources/resource_spanner_instance.go | 326 ------------------ .../tests/resource_spanner_instance_test.go | 75 ---- .../terraform/utils/iam_spanner_instance.go | 34 ++ .../utils/spanner_instance_operation.go | 8 +- 13 files changed, 175 insertions(+), 406 deletions(-) create mode 100644 templates/terraform/custom_expand/spanner_instance_config.go create mode 100644 templates/terraform/custom_expand/spanner_instance_config.go.erb create mode 100644 templates/terraform/decoders/spanner_instance.go.erb create mode 100644 templates/terraform/encoders/spanner_instance.go.erb create mode 100644 templates/terraform/encoders/spanner_instance_update.go.erb create mode 100644 templates/terraform/post_create/sleep.go.erb delete mode 100644 third_party/terraform/resources/resource_spanner_instance.go diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index dba1f52bebdb..dbe6d4c1c58a 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -58,6 +58,7 @@ objects: 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' @@ -109,6 +110,16 @@ 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: An explanation of the status of the instance. + 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..9107800ce887 100644 --- a/products/spanner/terraform.yaml +++ b/products/spanner/terraform.yaml @@ -40,9 +40,33 @@ 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}}/{{instance}}/{{name}}" + import_format: + - "projects/{{project}}/instances/{{name}}" + - "{{project}}/{{name}}" + - "{{name}}" + properties: + name: !ruby/object:Overrides::Terraform::PropertyOverride + validation: !ruby/object:Provider::Terraform::Validation + regex: '^(?:[a-z](?:[-_a-z0-9]{0,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..cc53a6efb332 100644 --- a/third_party/terraform/tests/resource_spanner_instance_test.go +++ b/third_party/terraform/tests/resource_spanner_instance_test.go @@ -47,81 +47,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") 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 From 21ed3d2df2690aa39b0be0d97c1ee3771696225e Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Thu, 17 Jan 2019 13:16:52 -0800 Subject: [PATCH 2/9] Remove old website (although it's still accurate), correct ID, remove unused code. --- products/spanner/terraform.yaml | 7 +- .../tests/resource_spanner_instance_test.go | 16 ---- .../docs/r/spanner_instance.html.markdown | 73 ------------------- 3 files changed, 5 insertions(+), 91 deletions(-) delete mode 100644 third_party/terraform/website/docs/r/spanner_instance.html.markdown diff --git a/products/spanner/terraform.yaml b/products/spanner/terraform.yaml index 9107800ce887..4c71ea908420 100644 --- a/products/spanner/terraform.yaml +++ b/products/spanner/terraform.yaml @@ -43,15 +43,18 @@ overrides: !ruby/object:Overrides::ResourceOverrides # This is appropriate for a datasource - doesn't have a create method. exclude: true Instance: !ruby/object:Overrides::Terraform::ResourceOverride - id_format: "{{project}}/{{instance}}/{{name}}" + 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]{0,28}[a-z0-9])?)$' + 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". diff --git a/third_party/terraform/tests/resource_spanner_instance_test.go b/third_party/terraform/tests/resource_spanner_instance_test.go index cc53a6efb332..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,20 +45,6 @@ func TestSpannerInstanceId_parentProjectUri(t *testing.T) { expectEquals(t, expected, actual) } -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/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 - -``` From f4581851f46e7f6ad4f790a77698c895eab9df2a Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Thu, 17 Jan 2019 16:00:42 -0800 Subject: [PATCH 3/9] Make documentation more accurate - add links, required annotations, etc. --- products/spanner/api.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index dbe6d4c1c58a..313349d55d3f 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -52,6 +52,10 @@ 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 @@ -64,14 +68,14 @@ objects: 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: | @@ -81,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: | From 06756284291c0e9dc049727ec36701f03df6feb1 Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Thu, 17 Jan 2019 16:01:56 -0800 Subject: [PATCH 4/9] Better string for 'state'. --- products/spanner/api.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index 313349d55d3f..a732f5109dca 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -117,7 +117,7 @@ objects: Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. - !ruby/object:Api::Type::Enum name: 'state' - description: An explanation of the status of the instance. + 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. From b478a2feb27deb89b32bda51e626af071a854de6 Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Thu, 17 Jan 2019 17:10:18 -0800 Subject: [PATCH 5/9] Moving a colon around to make ansible yaml parser happy. --- products/spanner/api.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index a732f5109dca..0e2d9952efe9 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -117,7 +117,8 @@ objects: Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. - !ruby/object:Api::Type::Enum name: 'state' - description: Instance status: `CREATING` or `READY`. + 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. From c6fca69b1d990ff843eb292786d78c30b3201aa5 Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Fri, 18 Jan 2019 19:15:20 +0000 Subject: [PATCH 6/9] Docs fixes, swapping out ForceNews. --- products/spanner/api.yaml | 34 ++++----------- products/spanner/terraform.yaml | 11 ++++- .../examples/spanner_instance_basic.tf.erb | 8 ++++ templates/terraform/post_create/sleep.go.erb | 5 ++- .../tests/resource_spanner_instance_test.go | 43 ------------------- third_party/terraform/utils/provider.go.erb | 3 +- 6 files changed, 30 insertions(+), 74 deletions(-) create mode 100644 templates/terraform/examples/spanner_instance_basic.tf.erb diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index 0e2d9952efe9..10391a1c831d 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -74,7 +74,13 @@ objects: name: 'config' resource: 'InstanceConfig' imports: 'name' - description: 'A reference to the instance configuration.' + description: | + 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). required: true - !ruby/object:Api::Type::String name: 'displayName' @@ -85,34 +91,10 @@ objects: - !ruby/object:Api::Type::Integer name: 'nodeCount' description: 'The number of nodes allocated to this instance.' - required: true + default_value: 1 - !ruby/object:Api::Type::KeyValuePairs name: 'labels' description: | - Cloud Labels are a flexible and lightweight mechanism for organizing - cloud resources into groups that reflect a customer's organizational - needs and deployment strategies. Cloud Labels can be used to filter - collections of resources. They can be used to control how resource - metrics are aggregated. And they can be used as arguments to policy - management rules (e.g. route, firewall, load balancing, etc.). - - Label keys must be between 1 and 63 characters long and must conform - to the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. - Label values must be between 0 and 63 characters long and must conform - to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. - - No more than 64 labels can be associated with a given resource. - - See https://goo.gl/xmQnxf for more information on and examples of - labels. - - If you plan to use labels in your own code, please note that - additional characters may be allowed in the future. And so you are - advised to use an internal label representation, such as JSON, which - doesn't rely upon specific characters being disallowed. For example, - representing labels as the string: name + "_" + value would prove - problematic if we were to allow "_" in a future release. - An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. - !ruby/object:Api::Type::Enum diff --git a/products/spanner/terraform.yaml b/products/spanner/terraform.yaml index 4c71ea908420..f2b4e89976a8 100644 --- a/products/spanner/terraform.yaml +++ b/products/spanner/terraform.yaml @@ -48,10 +48,19 @@ overrides: !ruby/object:Overrides::ResourceOverrides - "projects/{{project}}/instances/{{name}}" - "{{project}}/{{name}}" - "{{name}}" + examples: + - !ruby/object:Provider::Terraform::Examples + name: "spanner_instance_basic" + primary_resource_id: "example" + version: <%= version_name %> + description: | + {{description}} + + If not provided, a random string starting with `tf-` will be selected. properties: displayName: !ruby/object:Overrides::Terraform::PropertyOverride validation: !ruby/object:Provider::Terraform::Validation - regex: '^(?:[a-z](?:[-_a-z0-9]{2,28}[a-z0-9])?)$' + regex: '^(?:[a-zA-Z](?:[- _a-zA-Z0-9]{2,28}[a-zA-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])?)$' diff --git a/templates/terraform/examples/spanner_instance_basic.tf.erb b/templates/terraform/examples/spanner_instance_basic.tf.erb new file mode 100644 index 000000000000..34140d32ed39 --- /dev/null +++ b/templates/terraform/examples/spanner_instance_basic.tf.erb @@ -0,0 +1,8 @@ +resource "google_spanner_instance" "example" { + config = "regional-us-central1" + display_name = "Test Spanner Instance" + num_nodes = 2 + labels { + "foo" = "bar" + } +} diff --git a/templates/terraform/post_create/sleep.go.erb b/templates/terraform/post_create/sleep.go.erb index defb4f9bb242..30b726f3bebc 100644 --- a/templates/terraform/post_create/sleep.go.erb +++ b/templates/terraform/post_create/sleep.go.erb @@ -1,3 +1,4 @@ -// Spanner instances are, ironically, eventually consistent. -// We ensure that the Read sees the instance as created by sleeping, briefly. +// This is useful if the resource in question doesn't have a perfectly consistent API +// That is, the Operation for Create might return before the Get operation shows the +// completed state of the resource. time.Sleep(5 * time.Second) diff --git a/third_party/terraform/tests/resource_spanner_instance_test.go b/third_party/terraform/tests/resource_spanner_instance_test.go index f3231b58ae30..fb3c7c0e3711 100644 --- a/third_party/terraform/tests/resource_spanner_instance_test.go +++ b/third_party/terraform/tests/resource_spanner_instance_test.go @@ -2,15 +2,10 @@ package google import ( "fmt" - "net/http" "testing" - "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" - - "google.golang.org/api/googleapi" ) // Unit Tests @@ -131,44 +126,6 @@ func TestAccSpannerInstance_update(t *testing.T) { }) } -func testAccCheckSpannerInstanceDestroy(s *terraform.State) error { - config := testAccProvider.Meta().(*Config) - - for _, rs := range s.RootModule().Resources { - if rs.Type != "google_spanner_instance" { - continue - } - - if rs.Primary.ID == "" { - return fmt.Errorf("Unable to verify delete of spanner instance, ID is empty") - } - - instanceName := rs.Primary.Attributes["name"] - project, err := getTestProject(rs.Primary, config) - if err != nil { - return err - } - - id := spannerInstanceId{ - Project: project, - Instance: instanceName, - } - _, err = config.clientSpanner.Projects.Instances.Get( - id.instanceUri()).Do() - - if err == nil { - return fmt.Errorf("Spanner instance still exists") - } - - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound { - return nil - } - return errwrap.Wrapf("Error verifying spanner instance deleted: {{err}}", err) - } - - return nil -} - func testAccSpannerInstance_basic(name string) string { return fmt.Sprintf(` resource "google_spanner_instance" "basic" { diff --git a/third_party/terraform/utils/provider.go.erb b/third_party/terraform/utils/provider.go.erb index f12c20914c48..617d2f48a282 100644 --- a/third_party/terraform/utils/provider.go.erb +++ b/third_party/terraform/utils/provider.go.erb @@ -146,6 +146,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { GeneratedRedisResourcesMap, GeneratedResourceManagerResourcesMap, GeneratedSourceRepoResourcesMap, + GeneratedSpannerResourcesMap, GeneratedStorageResourcesMap, GeneratedMonitoringResourcesMap, map[string]*schema.Resource{ @@ -216,11 +217,9 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { <% unless version == 'ga' -%> "google_service_networking_connection": resourceServiceNetworkingConnection(), <% end -%> - "google_spanner_instance": resourceSpannerInstance(), "google_spanner_instance_iam_binding": ResourceIamBindingWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), "google_spanner_instance_iam_member": ResourceIamMemberWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), "google_spanner_instance_iam_policy": ResourceIamPolicyWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), - "google_spanner_database": resourceSpannerDatabase(), "google_spanner_database_iam_binding": ResourceIamBindingWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc), "google_spanner_database_iam_member": ResourceIamMemberWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc), "google_spanner_database_iam_policy": ResourceIamPolicyWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc), From def6f0862528d1956918f98180b1e51eed0e793f Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Fri, 18 Jan 2019 15:13:38 -0800 Subject: [PATCH 7/9] Move name autogeneration notice. --- products/spanner/terraform.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/products/spanner/terraform.yaml b/products/spanner/terraform.yaml index f2b4e89976a8..4c86b51f2fd4 100644 --- a/products/spanner/terraform.yaml +++ b/products/spanner/terraform.yaml @@ -53,15 +53,15 @@ overrides: !ruby/object:Overrides::ResourceOverrides name: "spanner_instance_basic" primary_resource_id: "example" version: <%= version_name %> - description: | - {{description}} - - If not provided, a random string starting with `tf-` will be selected. properties: displayName: !ruby/object:Overrides::Terraform::PropertyOverride validation: !ruby/object:Provider::Terraform::Validation regex: '^(?:[a-zA-Z](?:[- _a-zA-Z0-9]{2,28}[a-zA-Z0-9])?)$' name: !ruby/object:Overrides::Terraform::PropertyOverride + description: | + {{description}} + + If not provided, a random string starting with `tf-` will be selected. 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 From 76372ac08efa86454977201ac2a3875ea3dbd0b7 Mon Sep 17 00:00:00 2001 From: Nathan McKinley Date: Tue, 22 Jan 2019 10:58:40 -0800 Subject: [PATCH 8/9] Mark name as required and input. --- products/spanner/api.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/products/spanner/api.yaml b/products/spanner/api.yaml index 10391a1c831d..099be397ac7f 100644 --- a/products/spanner/api.yaml +++ b/products/spanner/api.yaml @@ -70,6 +70,8 @@ objects: A unique identifier for the instance, which cannot be changed after the instance is created. The name must be between 6 and 30 characters in length. + input: true + required: true - !ruby/object:Api::Type::ResourceRef name: 'config' resource: 'InstanceConfig' From 25e59c0de5726ee9e54748df31d0828e671fa039 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Tue, 22 Jan 2019 20:38:18 +0000 Subject: [PATCH 9/9] Update tracked submodules -> HEAD on Tue Jan 22 20:38:18 UTC 2019 Tracked submodules are build/terraform-beta build/terraform build/ansible build/inspec. --- build/ansible | 2 +- build/terraform | 2 +- build/terraform-beta | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/ansible b/build/ansible index 439dcc2e4e85..8eb9167f5481 160000 --- a/build/ansible +++ b/build/ansible @@ -1 +1 @@ -Subproject commit 439dcc2e4e8589c7e5eeb86a824ee5d445ec75e7 +Subproject commit 8eb9167f5481cdd1c3ef96138075746319bc03bd diff --git a/build/terraform b/build/terraform index b554f9032502..738bdb9aacbc 160000 --- a/build/terraform +++ b/build/terraform @@ -1 +1 @@ -Subproject commit b554f90325022f7638769b4c4ec2fdde0fff0878 +Subproject commit 738bdb9aacbc29c3ccd0ed05824b64203581ebdb diff --git a/build/terraform-beta b/build/terraform-beta index 7f8cb24483e6..4d49fa9747be 160000 --- a/build/terraform-beta +++ b/build/terraform-beta @@ -1 +1 @@ -Subproject commit 7f8cb24483e63f757af5a0dd55f1056c32205a76 +Subproject commit 4d49fa9747bedda673894896c0dfe18865468746