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 }