| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| // ---------------------------------------------------------------------------- |
| // |
| // *** AUTO GENERATED CODE *** Type: MMv1 *** |
| // |
| // ---------------------------------------------------------------------------- |
| // |
| // 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 spanner |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "net/http" |
| "reflect" |
| "strings" |
| "time" |
| |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" |
| |
| "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" |
| transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport" |
| "github.com/hashicorp/terraform-provider-google-beta/google-beta/verify" |
| ) |
| |
| // customizeDiff func for additional checks on google_spanner_database properties: |
| func resourceSpannerDBDdlCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error { |
| old, new := diff.GetChange("ddl") |
| oldDdls := old.([]interface{}) |
| newDdls := new.([]interface{}) |
| var err error |
| |
| if len(newDdls) < len(oldDdls) { |
| err = diff.ForceNew("ddl") |
| if err != nil { |
| return fmt.Errorf("ForceNew failed for ddl, old - %v and new - %v", oldDdls, newDdls) |
| } |
| return nil |
| } |
| |
| for i := range oldDdls { |
| if newDdls[i].(string) != oldDdls[i].(string) { |
| err = diff.ForceNew("ddl") |
| if err != nil { |
| return fmt.Errorf("ForceNew failed for ddl, old - %v and new - %v", oldDdls, newDdls) |
| } |
| return nil |
| } |
| } |
| return nil |
| } |
| |
| func resourceSpannerDBDdlCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { |
| // separate func to allow unit testing |
| return resourceSpannerDBDdlCustomDiffFunc(diff) |
| } |
| |
| func resourceSpannerDBVirtualUpdate(d *schema.ResourceData, resourceSchema map[string]*schema.Schema) bool { |
| // deletion_protection is the only virtual field |
| if d.HasChange("deletion_protection") { |
| for field := range resourceSchema { |
| if field == "deletion_protection" { |
| continue |
| } |
| if d.HasChange(field) { |
| return false |
| } |
| } |
| return true |
| } |
| return false |
| } |
| |
| func ResourceSpannerDatabase() *schema.Resource { |
| return &schema.Resource{ |
| Create: resourceSpannerDatabaseCreate, |
| Read: resourceSpannerDatabaseRead, |
| Update: resourceSpannerDatabaseUpdate, |
| Delete: resourceSpannerDatabaseDelete, |
| |
| Importer: &schema.ResourceImporter{ |
| State: resourceSpannerDatabaseImport, |
| }, |
| |
| Timeouts: &schema.ResourceTimeout{ |
| Create: schema.DefaultTimeout(20 * time.Minute), |
| Update: schema.DefaultTimeout(20 * time.Minute), |
| Delete: schema.DefaultTimeout(20 * time.Minute), |
| }, |
| |
| CustomizeDiff: customdiff.All( |
| resourceSpannerDBDdlCustomDiff, |
| tpgresource.DefaultProviderProject, |
| ), |
| |
| Schema: map[string]*schema.Schema{ |
| "instance": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, |
| Description: `The instance to create the database on.`, |
| }, |
| "name": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| ValidateFunc: verify.ValidateRegexp(`^[a-z][a-z0-9_\-]*[a-z0-9]$`), |
| Description: `A unique identifier for the database, which cannot be changed after |
| the instance is created. Values are of the form [a-z][-a-z0-9]*[a-z0-9].`, |
| }, |
| "database_dialect": { |
| Type: schema.TypeString, |
| Computed: true, |
| Optional: true, |
| ForceNew: true, |
| ValidateFunc: verify.ValidateEnum([]string{"GOOGLE_STANDARD_SQL", "POSTGRESQL", ""}), |
| Description: `The dialect of the Cloud Spanner Database. |
| If it is not provided, "GOOGLE_STANDARD_SQL" will be used. Possible values: ["GOOGLE_STANDARD_SQL", "POSTGRESQL"]`, |
| }, |
| "ddl": { |
| Type: schema.TypeList, |
| Optional: true, |
| Description: `An optional list of DDL statements to run inside the newly created |
| database. Statements can create tables, indexes, etc. These statements |
| execute atomically with the creation of the database: if there is an |
| error in any statement, the database is not created.`, |
| Elem: &schema.Schema{ |
| Type: schema.TypeString, |
| }, |
| }, |
| "enable_drop_protection": { |
| Type: schema.TypeBool, |
| Optional: true, |
| Description: `Whether drop protection is enabled for this database. Defaults to false. |
| Drop protection is different from |
| the "deletion_protection" attribute in the following ways: |
| (1) "deletion_protection" only protects the database from deletions in Terraform. |
| whereas setting “enableDropProtection” to true protects the database from deletions in all interfaces. |
| (2) Setting "enableDropProtection" to true also prevents the deletion of the parent instance containing the database. |
| "deletion_protection" attribute does not provide protection against the deletion of the parent instance.`, |
| Default: false, |
| }, |
| "encryption_config": { |
| Type: schema.TypeList, |
| Optional: true, |
| ForceNew: true, |
| Description: `Encryption configuration for the database`, |
| MaxItems: 1, |
| Elem: &schema.Resource{ |
| Schema: map[string]*schema.Schema{ |
| "kms_key_name": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| Description: `Fully qualified name of the KMS key to use to encrypt this database. This key must exist |
| in the same location as the Spanner Database.`, |
| }, |
| }, |
| }, |
| }, |
| "version_retention_period": { |
| Type: schema.TypeString, |
| Computed: true, |
| Optional: true, |
| Description: `The retention period for the database. The retention period must be between 1 hour |
| and 7 days, and can be specified in days, hours, minutes, or seconds. For example, |
| the values 1d, 24h, 1440m, and 86400s are equivalent. Default value is 1h. |
| If this property is used, you must avoid adding new DDL statements to 'ddl' that |
| update the database's version_retention_period.`, |
| }, |
| "state": { |
| Type: schema.TypeString, |
| Computed: true, |
| Description: `An explanation of the status of the database.`, |
| }, |
| "deletion_protection": { |
| Type: schema.TypeBool, |
| Optional: true, |
| Description: `Whether Terraform will be prevented from destroying the database. Defaults to true. |
| When a'terraform destroy' or 'terraform apply' would delete the database, |
| the command will fail if this field is not set to false in Terraform state. |
| When the field is set to true or unset in Terraform state, a 'terraform apply' |
| or 'terraform destroy' that would delete the database will fail. |
| When the field is set to false, deleting the database is allowed.`, |
| Default: true, |
| }, |
| "project": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| ForceNew: true, |
| }, |
| }, |
| UseJSONNumber: true, |
| } |
| } |
| |
| func resourceSpannerDatabaseCreate(d *schema.ResourceData, meta interface{}) error { |
| config := meta.(*transport_tpg.Config) |
| userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) |
| if err != nil { |
| return err |
| } |
| |
| obj := make(map[string]interface{}) |
| nameProp, err := expandSpannerDatabaseName(d.Get("name"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { |
| obj["name"] = nameProp |
| } |
| versionRetentionPeriodProp, err := expandSpannerDatabaseVersionRetentionPeriod(d.Get("version_retention_period"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("version_retention_period"); !tpgresource.IsEmptyValue(reflect.ValueOf(versionRetentionPeriodProp)) && (ok || !reflect.DeepEqual(v, versionRetentionPeriodProp)) { |
| obj["versionRetentionPeriod"] = versionRetentionPeriodProp |
| } |
| extraStatementsProp, err := expandSpannerDatabaseDdl(d.Get("ddl"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("ddl"); !tpgresource.IsEmptyValue(reflect.ValueOf(extraStatementsProp)) && (ok || !reflect.DeepEqual(v, extraStatementsProp)) { |
| obj["extraStatements"] = extraStatementsProp |
| } |
| encryptionConfigProp, err := expandSpannerDatabaseEncryptionConfig(d.Get("encryption_config"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("encryption_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(encryptionConfigProp)) && (ok || !reflect.DeepEqual(v, encryptionConfigProp)) { |
| obj["encryptionConfig"] = encryptionConfigProp |
| } |
| databaseDialectProp, err := expandSpannerDatabaseDatabaseDialect(d.Get("database_dialect"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("database_dialect"); !tpgresource.IsEmptyValue(reflect.ValueOf(databaseDialectProp)) && (ok || !reflect.DeepEqual(v, databaseDialectProp)) { |
| obj["databaseDialect"] = databaseDialectProp |
| } |
| enableDropProtectionProp, err := expandSpannerDatabaseEnableDropProtection(d.Get("enable_drop_protection"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("enable_drop_protection"); !tpgresource.IsEmptyValue(reflect.ValueOf(enableDropProtectionProp)) && (ok || !reflect.DeepEqual(v, enableDropProtectionProp)) { |
| obj["enableDropProtection"] = enableDropProtectionProp |
| } |
| instanceProp, err := expandSpannerDatabaseInstance(d.Get("instance"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("instance"); !tpgresource.IsEmptyValue(reflect.ValueOf(instanceProp)) && (ok || !reflect.DeepEqual(v, instanceProp)) { |
| obj["instance"] = instanceProp |
| } |
| |
| obj, err = resourceSpannerDatabaseEncoder(d, meta, obj) |
| if err != nil { |
| return err |
| } |
| |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases") |
| if err != nil { |
| return err |
| } |
| |
| log.Printf("[DEBUG] Creating new Database: %#v", obj) |
| billingProject := "" |
| |
| project, err := tpgresource.GetProject(d, config) |
| if err != nil { |
| return fmt.Errorf("Error fetching project for Database: %s", err) |
| } |
| billingProject = project |
| |
| // err == nil indicates that the billing_project value was found |
| if bp, err := tpgresource.GetBillingProject(d, config); err == nil { |
| billingProject = bp |
| } |
| |
| headers := make(http.Header) |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "POST", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutCreate), |
| Headers: headers, |
| }) |
| if err != nil { |
| return fmt.Errorf("Error creating Database: %s", err) |
| } |
| |
| // Store the ID now |
| id, err := tpgresource.ReplaceVars(d, config, "{{instance}}/{{name}}") |
| if err != nil { |
| return fmt.Errorf("Error constructing id: %s", err) |
| } |
| d.SetId(id) |
| |
| // Use the resource in the operation response to populate |
| // identity fields and d.Id() before read |
| var opRes map[string]interface{} |
| err = SpannerOperationWaitTimeWithResponse( |
| config, res, &opRes, project, "Creating Database", userAgent, |
| d.Timeout(schema.TimeoutCreate)) |
| if err != nil { |
| // The resource didn't actually create |
| d.SetId("") |
| |
| return fmt.Errorf("Error waiting to create Database: %s", err) |
| } |
| |
| opRes, err = resourceSpannerDatabaseDecoder(d, meta, opRes) |
| if err != nil { |
| return fmt.Errorf("Error decoding response from operation: %s", err) |
| } |
| if opRes == nil { |
| return fmt.Errorf("Error decoding response from operation, could not find object") |
| } |
| |
| if err := d.Set("name", flattenSpannerDatabaseName(opRes["name"], d, config)); err != nil { |
| return err |
| } |
| |
| // This may have caused the ID to update - update it if so. |
| id, err = tpgresource.ReplaceVars(d, config, "{{instance}}/{{name}}") |
| if err != nil { |
| return fmt.Errorf("Error constructing id: %s", err) |
| } |
| d.SetId(id) |
| |
| // Note: Databases that are created with POSTGRESQL dialect do not support extra DDL |
| // statements at the time of database creation. To avoid users needing to run |
| // `terraform apply` twice to get their desired outcome, the provider does not set |
| // `extraStatements` in the call to the `create` endpoint and all DDL (other than |
| // <CREATE DATABASE>) is run post-create, by calling the `updateDdl` endpoint |
| |
| _, ok := opRes["name"] |
| if !ok { |
| return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") |
| } |
| |
| retention, retentionPeriodOk := d.GetOk("version_retention_period") |
| retentionPeriod := retention.(string) |
| ddl, ddlOk := d.GetOk("ddl") |
| ddlStatements := ddl.([]interface{}) |
| |
| if retentionPeriodOk || ddlOk { |
| |
| obj := make(map[string]interface{}) |
| updateDdls := []string{} |
| |
| if ddlOk { |
| for i := 0; i < len(ddlStatements); i++ { |
| if ddlStatements[i] != nil { |
| updateDdls = append(updateDdls, ddlStatements[i].(string)) |
| } |
| } |
| } |
| |
| if retentionPeriodOk { |
| dbName := d.Get("name") |
| retentionDdl := fmt.Sprintf("ALTER DATABASE `%s` SET OPTIONS (version_retention_period=\"%s\")", dbName, retentionPeriod) |
| if dialect, ok := d.GetOk("database_dialect"); ok && dialect == "POSTGRESQL" { |
| retentionDdl = fmt.Sprintf("ALTER DATABASE \"%s\" SET spanner.version_retention_period TO \"%s\"", dbName, retentionPeriod) |
| } |
| updateDdls = append(updateDdls, retentionDdl) |
| } |
| |
| // Skip API call if there are no new ddl entries (due to ignoring nil values) |
| if len(updateDdls) > 0 { |
| log.Printf("[DEBUG] Applying extra DDL statements to the new Database: %#v", updateDdls) |
| |
| obj["statements"] = updateDdls |
| |
| url, err = tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}/ddl") |
| if err != nil { |
| return err |
| } |
| |
| res, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "PATCH", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutUpdate), |
| }) |
| if err != nil { |
| return fmt.Errorf("Error executing DDL statements on Database: %s", err) |
| } |
| |
| // Use the resource in the operation response to populate |
| // identity fields and d.Id() before read |
| var opRes map[string]interface{} |
| err = SpannerOperationWaitTimeWithResponse( |
| config, res, &opRes, project, "Creating Database", userAgent, |
| d.Timeout(schema.TimeoutCreate)) |
| if err != nil { |
| // The resource didn't actually create |
| d.SetId("") |
| return fmt.Errorf("Error waiting to run DDL against newly-created Database: %s", err) |
| } |
| } |
| } |
| |
| enableDropProtection, enableDropProtectionOk := d.GetOk("enable_drop_protection") |
| dropProtection := enableDropProtection.(bool) |
| if enableDropProtectionOk && dropProtection { |
| updateMask := []string{"enableDropProtection"} |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}") |
| if err != nil { |
| return err |
| } |
| // updateMask is a URL parameter but not present in the schema, so ReplaceVars |
| // won't set it |
| url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) |
| if err != nil { |
| return err |
| } |
| obj := map[string]interface{}{"enableDropProtection": dropProtection} |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "PATCH", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutUpdate), |
| }) |
| if err != nil { |
| return fmt.Errorf("Error updating enableDropDatabaseProtection on Database: %s", err) |
| } else { |
| log.Printf("[DEBUG] Finished updating enableDropDatabaseProtection %q: %#v", d.Id(), res) |
| } |
| } |
| |
| log.Printf("[DEBUG] Finished creating Database %q: %#v", d.Id(), res) |
| |
| return resourceSpannerDatabaseRead(d, meta) |
| } |
| |
| func resourceSpannerDatabaseRead(d *schema.ResourceData, meta interface{}) error { |
| config := meta.(*transport_tpg.Config) |
| userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) |
| if err != nil { |
| return err |
| } |
| |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}") |
| if err != nil { |
| return err |
| } |
| |
| billingProject := "" |
| |
| project, err := tpgresource.GetProject(d, config) |
| if err != nil { |
| return fmt.Errorf("Error fetching project for Database: %s", err) |
| } |
| billingProject = project |
| |
| // err == nil indicates that the billing_project value was found |
| if bp, err := tpgresource.GetBillingProject(d, config); err == nil { |
| billingProject = bp |
| } |
| |
| headers := make(http.Header) |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "GET", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Headers: headers, |
| }) |
| if err != nil { |
| return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("SpannerDatabase %q", d.Id())) |
| } |
| |
| res, err = resourceSpannerDatabaseDecoder(d, meta, res) |
| if err != nil { |
| return err |
| } |
| |
| if res == nil { |
| // Decoding the object has resulted in it being gone. It may be marked deleted |
| log.Printf("[DEBUG] Removing SpannerDatabase because it no longer exists.") |
| d.SetId("") |
| return nil |
| } |
| |
| // Explicitly set virtual fields to default values if unset |
| if _, ok := d.GetOkExists("deletion_protection"); !ok { |
| if err := d.Set("deletion_protection", true); err != nil { |
| return fmt.Errorf("Error setting deletion_protection: %s", err) |
| } |
| } |
| if err := d.Set("project", project); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| |
| if err := d.Set("name", flattenSpannerDatabaseName(res["name"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("version_retention_period", flattenSpannerDatabaseVersionRetentionPeriod(res["versionRetentionPeriod"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("state", flattenSpannerDatabaseState(res["state"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("encryption_config", flattenSpannerDatabaseEncryptionConfig(res["encryptionConfig"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("database_dialect", flattenSpannerDatabaseDatabaseDialect(res["databaseDialect"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("enable_drop_protection", flattenSpannerDatabaseEnableDropProtection(res["enableDropProtection"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| if err := d.Set("instance", flattenSpannerDatabaseInstance(res["instance"], d, config)); err != nil { |
| return fmt.Errorf("Error reading Database: %s", err) |
| } |
| |
| return nil |
| } |
| |
| func resourceSpannerDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { |
| config := meta.(*transport_tpg.Config) |
| userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) |
| if err != nil { |
| return err |
| } |
| |
| billingProject := "" |
| |
| project, err := tpgresource.GetProject(d, config) |
| if err != nil { |
| return fmt.Errorf("Error fetching project for Database: %s", err) |
| } |
| billingProject = project |
| |
| obj := make(map[string]interface{}) |
| enableDropProtectionProp, err := expandSpannerDatabaseEnableDropProtection(d.Get("enable_drop_protection"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("enable_drop_protection"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, enableDropProtectionProp)) { |
| obj["enableDropProtection"] = enableDropProtectionProp |
| } |
| |
| obj, err = resourceSpannerDatabaseUpdateEncoder(d, meta, obj) |
| if err != nil { |
| return err |
| } |
| |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}") |
| if err != nil { |
| return err |
| } |
| |
| log.Printf("[DEBUG] Updating Database %q: %#v", d.Id(), obj) |
| headers := make(http.Header) |
| updateMask := []string{} |
| |
| if d.HasChange("enable_drop_protection") { |
| updateMask = append(updateMask, "enableDropProtection") |
| } |
| // updateMask is a URL parameter but not present in the schema, so ReplaceVars |
| // won't set it |
| url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) |
| if err != nil { |
| return err |
| } |
| |
| if obj["statements"] != nil { |
| if len(obj["statements"].([]string)) == 0 { |
| // Return early to avoid making an API call that errors, |
| // due to containing no DDL SQL statements |
| d.Partial(false) |
| return resourceSpannerDatabaseRead(d, meta) |
| } |
| } |
| |
| if resourceSpannerDBVirtualUpdate(d, ResourceSpannerDatabase().Schema) { |
| if d.Get("deletion_protection") != nil { |
| if err := d.Set("deletion_protection", d.Get("deletion_protection")); err != nil { |
| return fmt.Errorf("Error reading Instance: %s", err) |
| } |
| } |
| return nil |
| } |
| |
| // err == nil indicates that the billing_project value was found |
| if bp, err := tpgresource.GetBillingProject(d, config); err == nil { |
| billingProject = bp |
| } |
| |
| // if updateMask is empty we are not updating anything so skip the post |
| if len(updateMask) > 0 { |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "PATCH", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutUpdate), |
| Headers: headers, |
| }) |
| |
| if err != nil { |
| return fmt.Errorf("Error updating Database %q: %s", d.Id(), err) |
| } else { |
| log.Printf("[DEBUG] Finished updating Database %q: %#v", d.Id(), res) |
| } |
| |
| err = SpannerOperationWaitTime( |
| config, res, project, "Updating Database", userAgent, |
| d.Timeout(schema.TimeoutUpdate)) |
| |
| if err != nil { |
| return err |
| } |
| } |
| d.Partial(true) |
| |
| if d.HasChange("version_retention_period") || d.HasChange("ddl") { |
| obj := make(map[string]interface{}) |
| |
| versionRetentionPeriodProp, err := expandSpannerDatabaseVersionRetentionPeriod(d.Get("version_retention_period"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("version_retention_period"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, versionRetentionPeriodProp)) { |
| obj["versionRetentionPeriod"] = versionRetentionPeriodProp |
| } |
| extraStatementsProp, err := expandSpannerDatabaseDdl(d.Get("ddl"), d, config) |
| if err != nil { |
| return err |
| } else if v, ok := d.GetOkExists("ddl"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, extraStatementsProp)) { |
| obj["extraStatements"] = extraStatementsProp |
| } |
| |
| obj, err = resourceSpannerDatabaseUpdateEncoder(d, meta, obj) |
| if err != nil { |
| return err |
| } |
| |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}/ddl") |
| if err != nil { |
| return err |
| } |
| |
| headers := make(http.Header) |
| |
| if obj["statements"] != nil { |
| if len(obj["statements"].([]string)) == 0 { |
| // Return early to avoid making an API call that errors, |
| // due to containing no DDL SQL statements |
| d.Partial(false) |
| return resourceSpannerDatabaseRead(d, meta) |
| } |
| } |
| |
| if resourceSpannerDBVirtualUpdate(d, ResourceSpannerDatabase().Schema) { |
| if d.Get("deletion_protection") != nil { |
| if err := d.Set("deletion_protection", d.Get("deletion_protection")); err != nil { |
| return fmt.Errorf("Error reading Instance: %s", err) |
| } |
| } |
| return nil |
| } |
| |
| // err == nil indicates that the billing_project value was found |
| if bp, err := tpgresource.GetBillingProject(d, config); err == nil { |
| billingProject = bp |
| } |
| |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "PATCH", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutUpdate), |
| Headers: headers, |
| }) |
| if err != nil { |
| return fmt.Errorf("Error updating Database %q: %s", d.Id(), err) |
| } else { |
| log.Printf("[DEBUG] Finished updating Database %q: %#v", d.Id(), res) |
| } |
| |
| err = SpannerOperationWaitTime( |
| config, res, project, "Updating Database", userAgent, |
| d.Timeout(schema.TimeoutUpdate)) |
| if err != nil { |
| return err |
| } |
| } |
| |
| d.Partial(false) |
| |
| return resourceSpannerDatabaseRead(d, meta) |
| } |
| |
| func resourceSpannerDatabaseDelete(d *schema.ResourceData, meta interface{}) error { |
| config := meta.(*transport_tpg.Config) |
| userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) |
| if err != nil { |
| return err |
| } |
| |
| billingProject := "" |
| |
| project, err := tpgresource.GetProject(d, config) |
| if err != nil { |
| return fmt.Errorf("Error fetching project for Database: %s", err) |
| } |
| billingProject = project |
| |
| url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}") |
| if err != nil { |
| return err |
| } |
| |
| var obj map[string]interface{} |
| |
| // err == nil indicates that the billing_project value was found |
| if bp, err := tpgresource.GetBillingProject(d, config); err == nil { |
| billingProject = bp |
| } |
| |
| headers := make(http.Header) |
| if d.Get("deletion_protection").(bool) { |
| return fmt.Errorf("cannot destroy instance without setting deletion_protection=false and running `terraform apply`") |
| } |
| |
| log.Printf("[DEBUG] Deleting Database %q", d.Id()) |
| res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ |
| Config: config, |
| Method: "DELETE", |
| Project: billingProject, |
| RawURL: url, |
| UserAgent: userAgent, |
| Body: obj, |
| Timeout: d.Timeout(schema.TimeoutDelete), |
| Headers: headers, |
| }) |
| if err != nil { |
| return transport_tpg.HandleNotFoundError(err, d, "Database") |
| } |
| |
| err = SpannerOperationWaitTime( |
| config, res, project, "Deleting Database", userAgent, |
| d.Timeout(schema.TimeoutDelete)) |
| |
| if err != nil { |
| return err |
| } |
| |
| log.Printf("[DEBUG] Finished deleting Database %q: %#v", d.Id(), res) |
| return nil |
| } |
| |
| func resourceSpannerDatabaseImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { |
| config := meta.(*transport_tpg.Config) |
| if err := tpgresource.ParseImportId([]string{ |
| "^projects/(?P<project>[^/]+)/instances/(?P<instance>[^/]+)/databases/(?P<name>[^/]+)$", |
| "^instances/(?P<instance>[^/]+)/databases/(?P<name>[^/]+)$", |
| "^(?P<project>[^/]+)/(?P<instance>[^/]+)/(?P<name>[^/]+)$", |
| "^(?P<instance>[^/]+)/(?P<name>[^/]+)$", |
| }, d, config); err != nil { |
| return nil, err |
| } |
| |
| // Replace import id for the resource id |
| id, err := tpgresource.ReplaceVars(d, config, "{{instance}}/{{name}}") |
| if err != nil { |
| return nil, fmt.Errorf("Error constructing id: %s", err) |
| } |
| d.SetId(id) |
| |
| // Explicitly set virtual fields to default values on import |
| if err := d.Set("deletion_protection", true); err != nil { |
| return nil, fmt.Errorf("Error setting deletion_protection: %s", err) |
| } |
| |
| return []*schema.ResourceData{d}, nil |
| } |
| |
| func flattenSpannerDatabaseName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| if v == nil { |
| return v |
| } |
| return tpgresource.NameFromSelfLinkStateFunc(v) |
| } |
| |
| func flattenSpannerDatabaseVersionRetentionPeriod(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| return v |
| } |
| |
| func flattenSpannerDatabaseState(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| return v |
| } |
| |
| func flattenSpannerDatabaseEncryptionConfig(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| if v == nil { |
| return nil |
| } |
| original := v.(map[string]interface{}) |
| if len(original) == 0 { |
| return nil |
| } |
| transformed := make(map[string]interface{}) |
| transformed["kms_key_name"] = |
| flattenSpannerDatabaseEncryptionConfigKmsKeyName(original["kmsKeyName"], d, config) |
| return []interface{}{transformed} |
| } |
| func flattenSpannerDatabaseEncryptionConfigKmsKeyName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| return v |
| } |
| |
| func flattenSpannerDatabaseDatabaseDialect(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| return v |
| } |
| |
| func flattenSpannerDatabaseEnableDropProtection(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| return v |
| } |
| |
| func flattenSpannerDatabaseInstance(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { |
| if v == nil { |
| return v |
| } |
| return tpgresource.ConvertSelfLinkToV1(v.(string)) |
| } |
| |
| func expandSpannerDatabaseName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseVersionRetentionPeriod(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseDdl(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseEncryptionConfig(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| l := v.([]interface{}) |
| if len(l) == 0 || l[0] == nil { |
| return nil, nil |
| } |
| raw := l[0] |
| original := raw.(map[string]interface{}) |
| transformed := make(map[string]interface{}) |
| |
| transformedKmsKeyName, err := expandSpannerDatabaseEncryptionConfigKmsKeyName(original["kms_key_name"], d, config) |
| if err != nil { |
| return nil, err |
| } else if val := reflect.ValueOf(transformedKmsKeyName); val.IsValid() && !tpgresource.IsEmptyValue(val) { |
| transformed["kmsKeyName"] = transformedKmsKeyName |
| } |
| |
| return transformed, nil |
| } |
| |
| func expandSpannerDatabaseEncryptionConfigKmsKeyName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseDatabaseDialect(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseEnableDropProtection(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| return v, nil |
| } |
| |
| func expandSpannerDatabaseInstance(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { |
| f, err := tpgresource.ParseGlobalFieldValue("instances", v.(string), "project", d, config, true) |
| if err != nil { |
| return nil, fmt.Errorf("Invalid value for instance: %s", err) |
| } |
| return f.RelativeLink(), nil |
| } |
| |
| func resourceSpannerDatabaseEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { |
| obj["createStatement"] = fmt.Sprintf("CREATE DATABASE `%s`", obj["name"]) |
| if dialect, ok := obj["databaseDialect"]; ok && dialect == "POSTGRESQL" { |
| obj["createStatement"] = fmt.Sprintf("CREATE DATABASE \"%s\"", obj["name"]) |
| } |
| |
| // Extra DDL statements are removed from the create request and instead applied to the database in |
| // a post-create action, to accommodate retrictions when creating PostgreSQL-enabled databases. |
| // https://cloud.google.com/spanner/docs/create-manage-databases#create_a_database |
| log.Printf("[DEBUG] Preparing to create new Database. Any extra DDL statements will be applied to the Database in a separate API call") |
| |
| delete(obj, "name") |
| delete(obj, "instance") |
| |
| delete(obj, "versionRetentionPeriod") |
| delete(obj, "extraStatements") |
| delete(obj, "enableDropProtection") |
| return obj, nil |
| } |
| |
| func resourceSpannerDatabaseUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { |
| |
| if obj["versionRetentionPeriod"] != nil || obj["extraStatements"] != nil { |
| old, new := d.GetChange("ddl") |
| oldDdls := old.([]interface{}) |
| newDdls := new.([]interface{}) |
| updateDdls := []string{} |
| |
| //Only new ddl statments to be add to update call |
| for i := len(oldDdls); i < len(newDdls); i++ { |
| if newDdls[i] != nil { |
| updateDdls = append(updateDdls, newDdls[i].(string)) |
| } |
| } |
| |
| //Add statement to update version_retention_period property, if needed |
| if d.HasChange("version_retention_period") { |
| dbName := d.Get("name") |
| retentionDdl := fmt.Sprintf("ALTER DATABASE `%s` SET OPTIONS (version_retention_period=\"%s\")", dbName, obj["versionRetentionPeriod"]) |
| if dialect, ok := d.GetOk("database_dialect"); ok && dialect == "POSTGRESQL" { |
| retentionDdl = fmt.Sprintf("ALTER DATABASE \"%s\" SET spanner.version_retention_period TO \"%s\"", dbName, obj["versionRetentionPeriod"]) |
| } |
| updateDdls = append(updateDdls, retentionDdl) |
| } |
| |
| obj["statements"] = updateDdls |
| delete(obj, "name") |
| delete(obj, "versionRetentionPeriod") |
| delete(obj, "instance") |
| delete(obj, "extraStatements") |
| } |
| return obj, nil |
| } |
| |
| func resourceSpannerDatabaseDecoder(d *schema.ResourceData, meta interface{}, res map[string]interface{}) (map[string]interface{}, error) { |
| config := meta.(*transport_tpg.Config) |
| d.SetId(res["name"].(string)) |
| if err := tpgresource.ParseImportId([]string{"projects/(?P<project>[^/]+)/instances/(?P<instance>[^/]+)/databases/(?P<name>[^/]+)"}, 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) |
| id, err := tpgresource.ReplaceVars(d, config, "{{instance}}/{{name}}") |
| if err != nil { |
| return nil, err |
| } |
| d.SetId(id) |
| return res, nil |
| } |