diff --git a/google/iam_spanner_instance.go b/google/iam_spanner_instance.go index c02512060a9..a4787c21ae2 100644 --- a/google/iam_spanner_instance.go +++ b/google/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/google/provider.go b/google/provider.go index f3dd00320c2..7018cf63618 100644 --- a/google/provider.go +++ b/google/provider.go @@ -135,6 +135,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { GeneratedRedisResourcesMap, GeneratedResourceManagerResourcesMap, GeneratedSourceRepoResourcesMap, + GeneratedSpannerResourcesMap, GeneratedStorageResourcesMap, GeneratedMonitoringResourcesMap, map[string]*schema.Resource{ @@ -197,11 +198,9 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_kms_crypto_key": resourceKmsCryptoKey(), "google_kms_crypto_key_iam_binding": ResourceIamBindingWithImport(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), "google_kms_crypto_key_iam_member": ResourceIamMemberWithImport(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), - "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), diff --git a/google/provider_spanner_gen.go b/google/provider_spanner_gen.go index 34720f715e3..634d491ed94 100644 --- a/google/provider_spanner_gen.go +++ b/google/provider_spanner_gen.go @@ -17,5 +17,6 @@ package google import "github.com/hashicorp/terraform/helper/schema" var GeneratedSpannerResourcesMap = map[string]*schema.Resource{ + "google_spanner_instance": resourceSpannerInstance(), "google_spanner_database": resourceSpannerDatabase(), } diff --git a/google/resource_spanner_database.go b/google/resource_spanner_database.go index a117407eb64..2bc481e9544 100644 --- a/google/resource_spanner_database.go +++ b/google/resource_spanner_database.go @@ -239,14 +239,12 @@ func resourceSpannerDatabaseEncoder(d *schema.ResourceData, meta interface{}, ob func resourceSpannerDatabaseDecoder(d *schema.ResourceData, meta interface{}, res map[string]interface{}) (map[string]interface{}, error) { 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/google/resource_spanner_instance.go b/google/resource_spanner_instance.go index cd052f090d8..6b66dc57620 100644 --- a/google/resource_spanner_instance.go +++ b/google/resource_spanner_instance.go @@ -1,16 +1,30 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + package google import ( "fmt" "log" - "net/http" + "reflect" "regexp" + "strconv" "strings" + "time" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - - "google.golang.org/api/googleapi" "google.golang.org/api/spanner/v1" ) @@ -20,307 +34,429 @@ func resourceSpannerInstance() *schema.Resource { Read: resourceSpannerInstanceRead, Update: resourceSpannerInstanceUpdate, Delete: resourceSpannerInstanceDelete, + Importer: &schema.ResourceImporter{ - State: resourceSpannerInstanceImportState, + State: resourceSpannerInstanceImport, }, - Schema: map[string]*schema.Schema{ + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(240 * time.Second), + Update: schema.DefaultTimeout(240 * time.Second), + Delete: schema.DefaultTimeout(240 * time.Second), + }, + Schema: map[string]*schema.Schema{ "config": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, + "display_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(`^(?:[a-zA-Z](?:[- _a-zA-Z0-9]{2,28}[a-zA-Z0-9])?)$`), }, - "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 - }, + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ValidateFunc: validateRegexp(`^(?:[a-z](?:[-_a-z0-9]{4,28}[a-z0-9])?)$`), }, - - "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 - }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "num_nodes": { Type: schema.TypeInt, Optional: true, Default: 1, }, - - "labels": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + "state": { + Type: schema.TypeString, + Computed: true, }, - "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) + obj := make(map[string]interface{}) + nameProp, err := expandSpannerInstanceName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + configProp, err := expandSpannerInstanceConfig(d.Get("config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("config"); !isEmptyValue(reflect.ValueOf(configProp)) && (ok || !reflect.DeepEqual(v, configProp)) { + obj["config"] = configProp + } + displayNameProp, err := expandSpannerInstanceDisplayName(d.Get("display_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(displayNameProp)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { + obj["displayName"] = displayNameProp + } + nodeCountProp, err := expandSpannerInstanceNum_nodes(d.Get("num_nodes"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("num_nodes"); !isEmptyValue(reflect.ValueOf(nodeCountProp)) && (ok || !reflect.DeepEqual(v, nodeCountProp)) { + obj["nodeCount"] = nodeCountProp + } + labelsProp, err := expandSpannerInstanceLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp } - if v, ok := d.GetOk("labels"); ok { - cir.Instance.Labels = convertStringMap(v.(map[string]interface{})) + obj, err = resourceSpannerInstanceEncoder(d, meta, obj) + if err != nil { + return err } - id, err := buildSpannerInstanceId(d, config) + url, err := replaceVars(d, config, "https://spanner.googleapis.com/v1/projects/{{project}}/instances") 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)) + log.Printf("[DEBUG] Creating new Instance: %#v", obj) + res, err := sendRequestWithTimeout(config, "POST", url, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Instance: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "{{project}}/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) - op, err := config.clientSpanner.Projects.Instances.Create( - id.parentProjectUri(), cir).Do() + project, err := getProject(d, config) 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) + return err + } + op := &spanner.Operation{} + err = Convert(res, op) + if err != nil { + return err } - d.SetId(id.terraformId()) + waitErr := spannerOperationWaitTime( + config.clientSpanner, op, project, "Creating Instance", + int(d.Timeout(schema.TimeoutCreate).Minutes())) - // 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 + return fmt.Errorf("Error waiting to create Instance: %s", waitErr) } - log.Printf("[INFO] Spanner instance %s has been created", id.terraformId()) + log.Printf("[DEBUG] Finished creating Instance %q: %#v", d.Id(), res) + + // 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) + return resourceSpannerInstanceRead(d, meta) } func resourceSpannerInstanceRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - id, err := buildSpannerInstanceId(d, config) + url, err := replaceVars(d, config, "https://spanner.googleapis.com/v1/projects/{{project}}/instances/{{name}}") + if err != nil { + return err + } + + res, err := sendRequest(config, "GET", url, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("SpannerInstance %q", d.Id())) + } + + res, err = resourceSpannerInstanceDecoder(d, meta, res) if err != nil { return err } - instance, err := config.clientSpanner.Projects.Instances.Get( - id.instanceUri()).Do() + project, err := getProject(d, config) if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("Spanner instance %s", id.terraformId())) + return err + } + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) } - 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) + if err := d.Set("name", flattenSpannerInstanceName(res["name"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("config", flattenSpannerInstanceConfig(res["config"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("display_name", flattenSpannerInstanceDisplayName(res["displayName"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("num_nodes", flattenSpannerInstanceNum_nodes(res["nodeCount"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("labels", flattenSpannerInstanceLabels(res["labels"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } + if err := d.Set("state", flattenSpannerInstanceState(res["state"], d)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } 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) + obj := make(map[string]interface{}) + configProp, err := expandSpannerInstanceConfig(d.Get("config"), d, config) if err != nil { return err + } else if v, ok := d.GetOkExists("config"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, configProp)) { + obj["config"] = configProp } - - fieldMask := []string{} - if d.HasChange("num_nodes") { - fieldMask = append(fieldMask, "nodeCount") - uir.Instance.NodeCount = int64(d.Get("num_nodes").(int)) + displayNameProp, err := expandSpannerInstanceDisplayName(d.Get("display_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { + obj["displayName"] = displayNameProp } - if d.HasChange("display_name") { - fieldMask = append(fieldMask, "displayName") - uir.Instance.DisplayName = d.Get("display_name").(string) + nodeCountProp, err := expandSpannerInstanceNum_nodes(d.Get("num_nodes"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("num_nodes"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nodeCountProp)) { + obj["nodeCount"] = nodeCountProp } - if d.HasChange("labels") { - fieldMask = append(fieldMask, "labels") - uir.Instance.Labels = convertStringMap(d.Get("labels").(map[string]interface{})) + labelsProp, err := expandSpannerInstanceLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + obj, err = resourceSpannerInstanceUpdateEncoder(d, meta, obj) + + url, err := replaceVars(d, config, "https://spanner.googleapis.com/v1/projects/{{project}}/instances/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Instance %q: %#v", d.Id(), obj) + res, err := sendRequestWithTimeout(config, "PATCH", url, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Instance %q: %s", d.Id(), err) } - uir.FieldMask = strings.Join(fieldMask, ",") - op, err := config.clientSpanner.Projects.Instances.Patch( - id.instanceUri(), uir).Do() + project, err := getProject(d, config) + if err != nil { + return err + } + op := &spanner.Operation{} + err = Convert(res, op) 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) + err = spannerOperationWaitTime( + config.clientSpanner, op, project, "Updating Instance", + int(d.Timeout(schema.TimeoutUpdate).Minutes())) + 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) + url, err := replaceVars(d, config, "https://spanner.googleapis.com/v1/projects/{{project}}/instances/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Instance %q", d.Id()) + res, err := sendRequestWithTimeout(config, "DELETE", url, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Instance") + } + + project, err := getProject(d, config) if err != nil { return err } + op := &spanner.Operation{} + err = Convert(res, op) + if err != nil { + return err + } + + err = spannerOperationWaitTime( + config.clientSpanner, op, project, "Deleting Instance", + int(d.Timeout(schema.TimeoutDelete).Minutes())) - _, 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) + return err } - d.SetId("") + log.Printf("[DEBUG] Finished deleting Instance %q: %#v", d.Id(), res) return nil } -func resourceSpannerInstanceImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { +func resourceSpannerInstanceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { config := meta.(*Config) - id, err := importSpannerInstanceId(d.Id()) - if err != nil { + if err := parseImportId([]string{"projects/(?P[^/]+)/instances/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config); 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 + // Replace import id for the resource id + id, err := replaceVars(d, config, "{{project}}/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) } - - d.Set("name", id.Instance) - d.SetId(id.terraformId()) + d.SetId(id) 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 +func flattenSpannerInstanceName(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenSpannerInstanceConfig(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return v } - return &spannerInstanceId{ - Project: project, - Instance: d.Get("name").(string), - }, nil + return ConvertSelfLinkToV1(v.(string)) } -func genSpannerInstanceName() string { - return resource.PrefixedUniqueId("tfgen-spanid-")[:30] +func flattenSpannerInstanceDisplayName(v interface{}, d *schema.ResourceData) interface{} { + return v } -type spannerInstanceId struct { - Project string - Instance string +func flattenSpannerInstanceNum_nodes(v interface{}, d *schema.ResourceData) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil { + return intVal + } // let terraform core handle it if we can't convert the string to an int. + } + return v +} + +func flattenSpannerInstanceLabels(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenSpannerInstanceState(v interface{}, d *schema.ResourceData) interface{} { + return v } -func (s spannerInstanceId) terraformId() string { - return fmt.Sprintf("%s/%s", s.Project, s.Instance) +func expandSpannerInstanceName(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { + return v, nil } -func (s spannerInstanceId) parentProjectUri() string { - return fmt.Sprintf("projects/%s", s.Project) +func expandSpannerInstanceConfig(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 } -func (s spannerInstanceId) instanceUri() string { - return fmt.Sprintf("%s/instances/%s", s.parentProjectUri(), s.Instance) +func expandSpannerInstanceDisplayName(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { + return v, nil } -func (s spannerInstanceId) instanceConfigUri(c string) string { - return fmt.Sprintf("%s/instanceConfigs/%s", s.parentProjectUri(), c) +func expandSpannerInstanceNum_nodes(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { + return v, nil } -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)") +func expandSpannerInstanceLabels(v interface{}, d *schema.ResourceData, config *Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) } + return m, nil +} - 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 +func resourceSpannerInstanceEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + 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 +} - log.Printf("[INFO] Spanner instance import format of {projectId}/{instanceId} specified: %s", id) - return extractSpannerInstanceId(id) +func resourceSpannerInstanceUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + 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 } -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}") +func resourceSpannerInstanceDecoder(d *schema.ResourceData, meta interface{}, res map[string]interface{}) (map[string]interface{}, error) { + 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 } - parts := strings.Split(id, "/") - return &spannerInstanceId{ - Project: parts[0], - Instance: parts[1], - }, nil + d.SetId(id) + return res, nil } diff --git a/google/resource_spanner_instance_generated_test.go b/google/resource_spanner_instance_generated_test.go new file mode 100644 index 00000000000..94cad392f4a --- /dev/null +++ b/google/resource_spanner_instance_generated_test.go @@ -0,0 +1,87 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccSpannerInstance_spannerInstanceBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(10), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSpannerInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpannerInstance_spannerInstanceBasicExample(context), + }, + { + ResourceName: "google_spanner_instance.example", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccSpannerInstance_spannerInstanceBasicExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_spanner_instance" "example" { + config = "regional-us-central1" + display_name = "Test Spanner Instance" + num_nodes = 2 + labels { + "foo" = "bar" + } +} +`, context) +} + +func testAccCheckSpannerInstanceDestroy(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_spanner_instance" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := testAccProvider.Meta().(*Config) + + url, err := replaceVarsForTest(rs, "https://spanner.googleapis.com/v1/projects/{{project}}/instances/{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", url, nil) + if err == nil { + return fmt.Errorf("SpannerInstance still exists at %s", url) + } + } + + return nil +} diff --git a/google/resource_spanner_instance_test.go b/google/resource_spanner_instance_test.go index c28e01a491a..fb3c7c0e371 100644 --- a/google/resource_spanner_instance_test.go +++ b/google/resource_spanner_instance_test.go @@ -2,17 +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" - - "strings" - - "google.golang.org/api/googleapi" ) // Unit Tests @@ -47,95 +40,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) @@ -222,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/google/spanner_instance_operation.go b/google/spanner_instance_operation.go index 3c541950c0d..2831cc4833d 100644 --- a/google/spanner_instance_operation.go +++ b/google/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/website/docs/r/cloudbuild_trigger.html.markdown b/website/docs/r/cloudbuild_trigger.html.markdown index 6827812c26a..e2bc12a12b8 100644 --- a/website/docs/r/cloudbuild_trigger.html.markdown +++ b/website/docs/r/cloudbuild_trigger.html.markdown @@ -82,7 +82,7 @@ The following arguments are supported: * `ignored_files` - (Optional) ignoredFiles and includedFiles are file glob matches using http://godoc/pkg/path/filepath#Match - extended with support for "**". + extended with support for `**`. If ignoredFiles and changed files are both empty, then they are not used to determine whether or not to trigger a build. If ignoredFiles is not empty, then we ignore any files that match any @@ -92,7 +92,7 @@ The following arguments are supported: * `included_files` - (Optional) ignoredFiles and includedFiles are file glob matches using http://godoc/pkg/path/filepath#Match - extended with support for "**". + extended with support for `**`. If any of the files altered in the commit pass the ignoredFiles filter and includedFiles is empty, then as far as this filter is concerned, we should trigger the build. diff --git a/website/docs/r/spanner_instance.html.markdown b/website/docs/r/spanner_instance.html.markdown index bf06008275e..ced123d3685 100644 --- a/website/docs/r/spanner_instance.html.markdown +++ b/website/docs/r/spanner_instance.html.markdown @@ -1,25 +1,53 @@ --- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- layout: "google" page_title: "Google: google_spanner_instance" sidebar_current: "docs-google-spanner-instance" description: |- - Creates and manages a Google Spanner Instance. + An isolated set of Cloud Spanner resources on which databases can be + hosted. --- # 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). +An isolated set of Cloud Spanner resources on which databases can be +hosted. -## Example Usage -Example creating a Spanner instance. +To get more information about Instance, see: + +* [API documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/projects.instances) +* How-to Guides + * [Official Documentation](https://cloud.google.com/spanner/) + + +## Example Usage - Spanner Instance Basic + ```hcl -resource "google_spanner_instance" "main" { - config = "regional-europe-west1" - display_name = "main-instance" - name = "main-instance" - num_nodes = 1 +resource "google_spanner_instance" "example" { + config = "regional-us-central1" + display_name = "Test Spanner Instance" + num_nodes = 2 + labels { + "foo" = "bar" + } } ``` @@ -27,47 +55,72 @@ resource "google_spanner_instance" "main" { 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` - + (Required) + 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. + + If not provided, a random string starting with `tf-` will be selected. + +* `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. Must be + unique per project and between 4 and 30 characters in length. + - - - -* `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. +* `num_nodes` - + (Optional) + The number of nodes allocated to this instance. -* `project` - (Optional) The ID of the project in which the resource belongs. If it - is not provided, the provider project is used. +* `labels` - + (Optional) + An object containing a list of "key": value pairs. + Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. +* `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: +In addition to the arguments listed above, the following computed attributes are exported: -* `state` - The current state of the instance. -## Import +* `state` - + Instance status: `CREATING` or `READY`. -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 +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: -$ terraform import google_spanner_instance.master project123/instance456 +- `create` - Default is 4 minutes. +- `update` - Default is 4 minutes. +- `delete` - Default is 4 minutes. + +## Import +Instance can be imported using any of these accepted formats: + +``` +$ terraform import google_spanner_instance.default projects/{{project}}/instances/{{name}} +$ terraform import google_spanner_instance.default {{project}}/{{name}} +$ terraform import google_spanner_instance.default {{name}} ``` + +-> If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource.