Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enableDropProtection support to spanner database #8011

Merged
merged 12 commits into from
Jul 25, 2023
22 changes: 19 additions & 3 deletions mmv1/products/spanner/Database.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
--- !ruby/object:Api::Resource
name: 'Database'
base_url: projects/{{project}}/instances/{{instance}}/databases
immutable: true
update_verb: :PATCH
update_mask: true
description: |
A Cloud Spanner Database which is hosted on a Spanner instance.
references: !ruby/object:Api::Resource::ReferenceLinks
Expand Down Expand Up @@ -71,8 +72,8 @@ virtual_fields:
name: 'deletion_protection'
default_value: true
description: |
Whether or not to allow Terraform to destroy the instance. Unless this field is set to false
in Terraform state, a `terraform destroy` or `terraform apply` that would delete the instance will fail.
Whether or not to allow Terraform to destroy the database. Defaults to true. Unless this field is set to false
in Terraform state, a `terraform destroy` or `terraform apply` that would delete the database will fail.
custom_code: !ruby/object:Provider::Terraform::CustomCode
constants: 'templates/terraform/constants/spanner_database.go.erb'
encoder: templates/terraform/encoders/spanner_database.go.erb
Expand All @@ -90,6 +91,7 @@ parameters:
resource: 'Instance'
imports: 'name'
description: 'The instance to create the database on.'
immutable: true
required: true
custom_expand: 'templates/terraform/custom_expand/resourceref_with_validation.go.erb'
properties:
Expand Down Expand Up @@ -140,21 +142,35 @@ properties:
- :CREATING
- !ruby/object:Api::Type::NestedObject
name: 'encryptionConfig'
immutable: true
description: |
Encryption configuration for the database
properties:
- !ruby/object:Api::Type::String
name: 'kmsKeyName'
immutable: true
required: 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.
- !ruby/object:Api::Type::Enum
name: 'databaseDialect'
immutable: true
description: |
The dialect of the Cloud Spanner Database.
If it is not provided, "GOOGLE_STANDARD_SQL" will be used.
values:
- :GOOGLE_STANDARD_SQL
- :POSTGRESQL
default_from_api: true
- !ruby/object:Api::Type::Boolean
name: 'enableDropProtection'
rahul2393 marked this conversation as resolved.
Show resolved Hide resolved
rahul2393 marked this conversation as resolved.
Show resolved Hide resolved
default_value: false
description: |
Whether drop protection is enabled for this database. Defaults to false.
rahul2393 marked this conversation as resolved.
Show resolved Hide resolved
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.
3 changes: 3 additions & 0 deletions mmv1/templates/terraform/decoders/spanner_database.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ if err := tpgresource.ParseImportId([]string{"projects/(?P<project>[^/]+)/instan
res["project"] = d.Get("project").(string)
res["instance"] = d.Get("instance").(string)
res["name"] = d.Get("name").(string)
if _, ok := res["enableDropProtection"]; !ok {
res["enableDropProtection"] = false
}
shuyama1 marked this conversation as resolved.
Show resolved Hide resolved
id, err := tpgresource.ReplaceVars(d, config, "{{instance}}/{{name}}")
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions mmv1/templates/terraform/encoders/spanner_database.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ delete(obj, "instance")
<% unless compiler == "terraformvalidator-codegen" -%>
delete(obj, "versionRetentionPeriod")
delete(obj, "extraStatements")
delete(obj, "enableDropProtection")
<% end -%>
return obj, nil
34 changes: 33 additions & 1 deletion mmv1/templates/terraform/post_create/spanner_database.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,36 @@ if retentionPeriodOk || ddlOk {
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)
}
}

11 changes: 7 additions & 4 deletions mmv1/templates/terraform/pre_update/spanner_database.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
# limitations under the License.
-%>

if len(obj["statements"].([]string)) == 0 {
// Return early to avoid making an API call that errors,
// due to containing no DDL SQL statements
return resourceSpannerDatabaseRead(d, meta)
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) {
Expand Down
49 changes: 26 additions & 23 deletions mmv1/templates/terraform/update_encoder/spanner_database.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,34 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-%>
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))
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"])
//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)
}
updateDdls = append(updateDdls, retentionDdl)
}

obj["statements"] = updateDdls
delete(obj, "name")
delete(obj, "versionRetentionPeriod")
delete(obj, "instance")
delete(obj, "extraStatements")
return obj, nil
obj["statements"] = updateDdls
delete(obj, "name")
delete(obj, "versionRetentionPeriod")
delete(obj, "instance")
delete(obj, "extraStatements")
}
return obj, nil
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,82 @@ resource "google_spanner_database" "basic" {
`, instanceName, instanceName, databaseName, databaseName, databaseName)
}

func TestAccSpannerDatabase_enableDropProtection(t *testing.T) {
t.Parallel()

rnd := RandString(t, 10)
instanceName := fmt.Sprintf("tf-test-%s", rnd)
databaseName := fmt.Sprintf("tfgen_%s", rnd)

VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckSpannerDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccSpannerDatabase_enableDropProtection(instanceName, databaseName),
},
{
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
Config: testAccSpannerDatabase_enableDropProtectionUpdate(instanceName, databaseName),
shuyama1 marked this conversation as resolved.
Show resolved Hide resolved
},
{
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
},
})
}

func testAccSpannerDatabase_enableDropProtection(instanceName, databaseName string) string {
return fmt.Sprintf(`
resource "google_spanner_instance" "basic" {
name = "%s"
config = "regional-us-central1"
display_name = "%s-display"
num_nodes = 1
}

resource "google_spanner_database" "basic" {
instance = google_spanner_instance.basic.name
name = "%s"
enable_drop_protection = true
deletion_protection = false
ddl = [
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
]
}
`, instanceName, instanceName, databaseName)
}

func testAccSpannerDatabase_enableDropProtectionUpdate(instanceName, databaseName string) string {
return fmt.Sprintf(`
resource "google_spanner_instance" "basic" {
name = "%s"
config = "regional-us-central1"
display_name = "%s-display"
num_nodes = 1
}

resource "google_spanner_database" "basic" {
instance = google_spanner_instance.basic.name
name = "%s"
enable_drop_protection = false
deletion_protection = false
ddl = [
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
]
}
`, instanceName, instanceName, databaseName)
}

// Unit Tests for validation of retention period argument
func TestValidateDatabaseRetentionPeriod(t *testing.T) {
t.Parallel()
Expand Down