Repository: GoogleCloudPlatform/terraform-validator
Pull request #412 (2021-12-01 00:48:37)

Add support for spanner_database with IAM
Added spanner database IAM validator support. 
Downstream change is in https://github.com/GoogleCloudPlatform/terraform-validator/pull/388

<!--
Replace each [ ] with [X] to check it. Switch to the preview view to make it easier to click on links.
These steps will speed up the review process, and we appreciate you spending time on them before sending
your code to be reviewed.
-->
If this PR is for Terraform, I acknowledge that I have:

- [X] Searched through the [issue tracker](https://github.com/hashicorp/terraform-provider-google/issues) for an open issue that this either resolves or contributes to, commented on it to claim it, and written "fixes {url}" or "part of {url}" in this PR description. If there were no relevant open issues, I opened one and commented that I would like to work on it (not necessary for very small changes).
- [X] [Generated Terraform](https://github.com/GoogleCloudPlatform/magic-modules#generating-downstream-tools), and ran `make test` and `make lint` to ensure it passes unit and linter tests.
- [X] Ensured that all new fields I added that can be set by a user appear in at least one [example](https://github.com/GoogleCloudPlatform/magic-modules/tree/master/templates/terraform/examples) (for generated resources) or [third_party test](https://github.com/GoogleCloudPlatform/magic-modules/tree/master/third_party/terraform/tests) (for handwritten resources or update tests).
- [X] [Ran](https://github.com/hashicorp/terraform-provider-google/blob/master/.github/CONTRIBUTING.md#tests) relevant acceptance tests (If the acceptance tests do not yet pass or you are unable to run them, please let your reviewer know).
- [x] Read the [Release Notes Guide](https://github.com/GoogleCloudPlatform/magic-modules/blob/master/.ci/RELEASE_NOTES_GUIDE.md) before writing my release note below.

<!-- AUTOCHANGELOG for Downstream PRs.

Please select one of the following "release-note:" headings:
    - release-note:enhancement
    - release-note:bug
    - release-note:note
    - release-note:new-resource
    - release-note:new-datasource
    - release-note:deprecation
    - release-note:breaking-change
    - release-note:none
    
Unless you choose release-note:none, please add a release note.

See .ci/RELEASE_NOTES_GUIDE.md for writing good release notes.

You can add more release note blocks if you want more than one CHANGELOG
entry for this PR.
-->
**Release Note Template for Downstream PRs (will be copied)**

```release-note:enhancement
Added spanner database IAM validator support
```

Derived from https://github.com/GoogleCloudPlatform/magic-modules/pull/5462


Patch:
diff --git a/converters/google/resources/iam_spanner_database.go b/converters/google/resources/iam_spanner_database.go
new file mode 100644
index 00000000..8fc8f083
--- /dev/null
+++ b/converters/google/resources/iam_spanner_database.go
@@ -0,0 +1,165 @@
+package google
+
+import (
+	"fmt"
+
+	"github.com/hashicorp/errwrap"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"google.golang.org/api/cloudresourcemanager/v1"
+	"google.golang.org/api/spanner/v1"
+)
+
+var IamSpannerDatabaseSchema = map[string]*schema.Schema{
+	"instance": {
+		Type:     schema.TypeString,
+		Required: true,
+		ForceNew: true,
+	},
+	"database": {
+		Type:     schema.TypeString,
+		Required: true,
+		ForceNew: true,
+	},
+
+	"project": {
+		Type:     schema.TypeString,
+		Optional: true,
+		Computed: true,
+		ForceNew: true,
+	},
+}
+
+type SpannerDatabaseIamUpdater struct {
+	project  string
+	instance string
+	database string
+	d        TerraformResourceData
+	Config   *Config
+}
+
+func NewSpannerDatabaseIamUpdater(d TerraformResourceData, config *Config) (ResourceIamUpdater, error) {
+	project, err := getProject(d, config)
+	if err != nil {
+		return nil, err
+	}
+
+	return &SpannerDatabaseIamUpdater{
+		project:  project,
+		instance: d.Get("instance").(string),
+		database: d.Get("database").(string),
+		d:        d,
+		Config:   config,
+	}, nil
+}
+
+func SpannerDatabaseIdParseFunc(d *schema.ResourceData, config *Config) error {
+	return parseImportId([]string{"(?P<project>[^/]+)/(?P<instance>[^/]+)/(?P<database>[^/]+)"}, d, config)
+}
+
+func (u *SpannerDatabaseIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
+	userAgent, err := generateUserAgentString(u.d, u.Config.userAgent)
+	if err != nil {
+		return nil, err
+	}
+
+	p, err := u.Config.NewSpannerClient(userAgent).Projects.Instances.Databases.GetIamPolicy(spannerDatabaseId{
+		Project:  u.project,
+		Database: u.database,
+		Instance: u.instance,
+	}.databaseUri(), &spanner.GetIamPolicyRequest{}).Do()
+
+	if err != nil {
+		return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	cloudResourcePolicy, err := spannerToResourceManagerPolicy(p)
+
+	if err != nil {
+		return nil, errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	return cloudResourcePolicy, nil
+}
+
+func (u *SpannerDatabaseIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
+	spannerPolicy, err := resourceManagerToSpannerPolicy(policy)
+
+	if err != nil {
+		return errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	userAgent, err := generateUserAgentString(u.d, u.Config.userAgent)
+	if err != nil {
+		return err
+	}
+
+	_, err = u.Config.NewSpannerClient(userAgent).Projects.Instances.Databases.SetIamPolicy(spannerDatabaseId{
+		Project:  u.project,
+		Database: u.database,
+		Instance: u.instance,
+	}.databaseUri(), &spanner.SetIamPolicyRequest{
+		Policy: spannerPolicy,
+	}).Do()
+
+	if err != nil {
+		return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err)
+	}
+
+	return nil
+}
+
+func (u *SpannerDatabaseIamUpdater) GetResourceId() string {
+	return spannerDatabaseId{
+		Project:  u.project,
+		Instance: u.instance,
+		Database: u.database,
+	}.terraformId()
+}
+
+func (u *SpannerDatabaseIamUpdater) GetMutexKey() string {
+	return fmt.Sprintf("iam-spanner-database-%s-%s-%s", u.project, u.instance, u.database)
+}
+
+func (u *SpannerDatabaseIamUpdater) DescribeResource() string {
+	return fmt.Sprintf("Spanner Database: %s/%s/%s", u.project, u.instance, u.database)
+}
+
+func resourceManagerToSpannerPolicy(p *cloudresourcemanager.Policy) (*spanner.Policy, error) {
+	out := &spanner.Policy{}
+	err := Convert(p, out)
+	if err != nil {
+		return nil, errwrap.Wrapf("Cannot convert a resourcemanager policy to a spanner policy: {{err}}", err)
+	}
+	return out, nil
+}
+
+func spannerToResourceManagerPolicy(p *spanner.Policy) (*cloudresourcemanager.Policy, error) {
+	out := &cloudresourcemanager.Policy{}
+	err := Convert(p, out)
+	if err != nil {
+		return nil, errwrap.Wrapf("Cannot convert a spanner policy to a resourcemanager policy: {{err}}", err)
+	}
+	return out, nil
+}
+
+type spannerDatabaseId struct {
+	Project  string
+	Instance string
+	Database string
+}
+
+func (s spannerDatabaseId) terraformId() string {
+	return fmt.Sprintf("%s/%s/%s", s.Project, s.Instance, s.Database)
+}
+
+func (s spannerDatabaseId) parentProjectUri() string {
+	return fmt.Sprintf("projects/%s", s.Project)
+}
+
+func (s spannerDatabaseId) parentInstanceUri() string {
+	return fmt.Sprintf("%s/instances/%s", s.parentProjectUri(), s.Instance)
+}
+
+func (s spannerDatabaseId) databaseUri() string {
+	return fmt.Sprintf("%s/databases/%s", s.parentInstanceUri(), s.Database)
+}
diff --git a/converters/google/resources/resource_converters.go b/converters/google/resources/resource_converters.go
index 53174bf4..dbeac100 100644
--- a/converters/google/resources/resource_converters.go
+++ b/converters/google/resources/resource_converters.go
@@ -45,6 +45,10 @@ func ResourceConverters() map[string][]ResourceConverter {
 		"google_bigquery_dataset_iam_binding":              {resourceConverterBigqueryDatasetIamBinding()},
 		"google_bigquery_dataset_iam_member":               {resourceConverterBigqueryDatasetIamMember()},
 		"google_bigquery_table":                            {resourceConverterBigQueryTable()},
+		"google_spanner_database":                          {resourceConverterSpannerDatabase()},
+		"google_spanner_database_iam_policy":               {resourceConverterSpannerDatabaseIamPolicy()},
+		"google_spanner_database_iam_binding":              {resourceConverterSpannerDatabaseIamBinding()},
+		"google_spanner_database_iam_member":               {resourceConverterSpannerDatabaseIamMember()},
 		"google_spanner_instance":                          {resourceConverterSpannerInstance()},
 		"google_project_service":                           {resourceConverterServiceUsage()},
 		"google_pubsub_subscription":                       {resourceConverterPubsubSubscription()},
diff --git a/converters/google/resources/spanner_database_iam.go b/converters/google/resources/spanner_database_iam.go
new file mode 100644
index 00000000..1e98f398
--- /dev/null
+++ b/converters/google/resources/spanner_database_iam.go
@@ -0,0 +1,103 @@
+package google
+
+import "fmt"
+
+func resourceConverterSpannerDatabaseIamPolicy() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         "spanner.googleapis.com/Database",
+		Convert:           GetSpannerDatabaseIamPolicyCaiObject,
+		MergeCreateUpdate: MergeSpannerDatabaseIamPolicy,
+	}
+}
+
+func resourceConverterSpannerDatabaseIamBinding() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         "spanner.googleapis.com/Database",
+		Convert:           GetSpannerDatabaseIamBindingCaiObject,
+		FetchFullResource: FetchSpannerDatabaseIamPolicy,
+		MergeCreateUpdate: MergeSpannerDatabaseIamBinding,
+		MergeDelete:       MergeSpannerDatabaseIamBindingDelete,
+	}
+}
+
+func resourceConverterSpannerDatabaseIamMember() ResourceConverter {
+	return ResourceConverter{
+		AssetType:         "spanner.googleapis.com/Database",
+		Convert:           GetSpannerDatabaseIamMemberCaiObject,
+		FetchFullResource: FetchSpannerDatabaseIamPolicy,
+		MergeCreateUpdate: MergeSpannerDatabaseIamMember,
+		MergeDelete:       MergeSpannerDatabaseIamMemberDelete,
+	}
+}
+
+func GetSpannerDatabaseIamPolicyCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newSpannerDatabaseIamAsset(d, config, expandIamPolicyBindings)
+}
+
+func GetSpannerDatabaseIamBindingCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newSpannerDatabaseIamAsset(d, config, expandIamRoleBindings)
+}
+
+func GetSpannerDatabaseIamMemberCaiObject(d TerraformResourceData, config *Config) ([]Asset, error) {
+	return newSpannerDatabaseIamAsset(d, config, expandIamMemberBindings)
+}
+
+func MergeSpannerDatabaseIamPolicy(existing, incoming Asset) Asset {
+	existing.IAMPolicy = incoming.IAMPolicy
+	return existing
+}
+
+func MergeSpannerDatabaseIamBinding(existing, incoming Asset) Asset {
+	return mergeIamAssets(existing, incoming, mergeAuthoritativeBindings)
+}
+
+func MergeSpannerDatabaseIamBindingDelete(existing, incoming Asset) Asset {
+	return mergeDeleteIamAssets(existing, incoming, mergeDeleteAuthoritativeBindings)
+}
+
+func MergeSpannerDatabaseIamMember(existing, incoming Asset) Asset {
+	return mergeIamAssets(existing, incoming, mergeAdditiveBindings)
+}
+
+func MergeSpannerDatabaseIamMemberDelete(existing, incoming Asset) Asset {
+	return mergeDeleteIamAssets(existing, incoming, mergeDeleteAdditiveBindings)
+}
+
+func newSpannerDatabaseIamAsset(
+	d TerraformResourceData,
+	config *Config,
+	expandBindings func(d TerraformResourceData) ([]IAMBinding, error),
+) ([]Asset, error) {
+	bindings, err := expandBindings(d)
+	if err != nil {
+		return []Asset{}, fmt.Errorf("expanding bindings: %v", err)
+	}
+
+	name, err := assetName(d, config, "//spanner.googleapis.com/{{spannerDatabaseId}}")
+	if err != nil {
+		return []Asset{}, err
+	}
+
+	return []Asset{{
+		Name: name,
+		Type: "spanner.googleapis.com/Database",
+		IAMPolicy: &IAMPolicy{
+			Bindings: bindings,
+		},
+	}}, nil
+}
+
+func FetchSpannerDatabaseIamPolicy(d TerraformResourceData, config *Config) (Asset, error) {
+	// Check if the identity field returns a value
+	if _, ok := d.GetOk("{{spannerDatabaseId}}"); !ok {
+		return Asset{}, ErrEmptyIdentityField
+	}
+
+	return fetchIamPolicy(
+		NewSpannerDatabaseIamUpdater,
+		d,
+		config,
+		"//spanner.googleapis.com/{{spannerDatabaseId}}",
+		"spanner.googleapis.com/Database",
+	)
+}


Test patch:
diff --git a/test/cli_test.go b/test/cli_test.go
index 3e1c26a4..ed843b8f 100644
--- a/test/cli_test.go
+++ b/test/cli_test.go
@@ -116,6 +116,10 @@ func TestCLI(t *testing.T) {
 		{name: "example_project_service"},
 		{name: "example_pubsub_subscription"},
 		{name: "example_pubsub_topic"},
+		{name: "example_spanner_database"},
+		{name: "example_spanner_database_iam_binding"},
+		{name: "example_spanner_database_iam_member"},
+		{name: "example_spanner_database_iam_policy"},
 		{name: "example_sql_database_instance"},
 		{name: "example_storage_bucket"},
 		{name: "example_storage_bucket_iam_member_random_suffix"},
diff --git a/test/read_test.go b/test/read_test.go
index 532fd3b0..ce68b140 100644
--- a/test/read_test.go
+++ b/test/read_test.go
@@ -75,6 +75,10 @@ func TestReadPlannedAssetsCoverage(t *testing.T) {
 		{name: "example_project_service"},
 		{name: "example_pubsub_subscription"},
 		{name: "example_pubsub_topic"},
+		{name: "example_spanner_database"},
+		{name: "example_spanner_database_iam_binding"},
+		{name: "example_spanner_database_iam_member"},
+		{name: "example_spanner_database_iam_policy"},
 		{name: "example_sql_database_instance"},
 		{name: "example_storage_bucket"},
 		{name: "example_storage_bucket_iam_member_random_suffix"},
diff --git a/testdata/templates/example_spanner_database.json b/testdata/templates/example_spanner_database.json
new file mode 100644
index 00000000..a65dc8e8
--- /dev/null
+++ b/testdata/templates/example_spanner_database.json
@@ -0,0 +1,39 @@
+[
+  {
+    "name": "//spanner.googleapis.com/projects/{{.Provider.project}}/instances/placeholder-BpLnfgDs/databases/my-database",
+    "asset_type": "spanner.googleapis.com/Database",
+    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "resource": {
+      "version": "v1",
+      "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/spanner/v1/rest",
+      "discovery_name": "Database",
+      "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}",
+      "data": {
+        "createStatement": "CREATE DATABASE `my-database`",
+        "extraStatements": [
+          "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
+          "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)"
+        ]
+      }
+    }
+  },
+  {
+    "name": "//spanner.googleapis.com/projects/{{.Provider.project}}/instances/placeholder-c2WD8F2q",
+    "asset_type": "spanner.googleapis.com/Instance",
+    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "resource": {
+      "version": "v1",
+      "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/spanner/v1/rest",
+      "discovery_name": "Instance",
+      "parent": "//cloudresourcemanager.googleapis.com/projects/{{.Provider.project}}",
+      "data": {
+        "instance": {
+          "config": "projects/{{.Provider.project}}/instanceConfigs/regional-europe-west1",
+          "displayName": "main-instance",
+          "nodeCount": 1
+        },
+        "instanceId": ""
+      }
+    }
+  }
+]
diff --git a/testdata/templates/example_spanner_database.tf b/testdata/templates/example_spanner_database.tf
new file mode 100644
index 00000000..0b203f2b
--- /dev/null
+++ b/testdata/templates/example_spanner_database.tf
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+terraform {
+  required_providers {
+    google = {
+      source = "hashicorp/google"
+      version = "~> {{.Provider.version}}"
+    }
+  }
+}
+
+provider "google" {
+  {{if .Provider.credentials }}credentials = "{{.Provider.credentials}}"{{end}}
+}
+
+
+resource "google_spanner_instance" "main" {
+  config       = "regional-europe-west1"
+  display_name = "main-instance"
+  num_nodes    = 1
+}
+
+resource "google_spanner_database" "database" {
+  instance = google_spanner_instance.main.name
+  name     = "my-database"
+  ddl = [
+    "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
+    "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
+  ]
+  deletion_protection = false
+}
diff --git a/testdata/templates/example_spanner_database.tfplan.json b/testdata/templates/example_spanner_database.tfplan.json
new file mode 100644
index 00000000..d4145ab8
--- /dev/null
+++ b/testdata/templates/example_spanner_database.tfplan.json
@@ -0,0 +1,189 @@
+{
+  "format_version": "0.2",
+  "terraform_version": "1.0.10",
+  "planned_values": {
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database.database",
+          "mode": "managed",
+          "type": "google_spanner_database",
+          "name": "database",
+          "provider_name": "registry.terraform.io/hashicorp/google",
+          "schema_version": 0,
+          "values": {
+            "ddl": [
+              "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
+              "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)"
+            ],
+            "deletion_protection": false,
+            "encryption_config": [],
+            "name": "my-database",
+            "timeouts": null
+          },
+          "sensitive_values": {
+            "ddl": [
+              false,
+              false
+            ],
+            "encryption_config": []
+          }
+        },
+        {
+          "address": "google_spanner_instance.main",
+          "mode": "managed",
+          "type": "google_spanner_instance",
+          "name": "main",
+          "provider_name": "registry.terraform.io/hashicorp/google",
+          "schema_version": 0,
+          "values": {
+            "config": "regional-europe-west1",
+            "display_name": "main-instance",
+            "force_destroy": false,
+            "labels": null,
+            "num_nodes": 1,
+            "timeouts": null
+          },
+          "sensitive_values": {}
+        }
+      ]
+    }
+  },
+  "resource_changes": [
+    {
+      "address": "google_spanner_database.database",
+      "mode": "managed",
+      "type": "google_spanner_database",
+      "name": "database",
+      "provider_name": "registry.terraform.io/hashicorp/google",
+      "change": {
+        "actions": [
+          "create"
+        ],
+        "before": null,
+        "after": {
+          "ddl": [
+            "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
+            "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)"
+          ],
+          "deletion_protection": false,
+          "encryption_config": [],
+          "name": "my-database",
+          "timeouts": null
+        },
+        "after_unknown": {
+          "ddl": [
+            false,
+            false
+          ],
+          "encryption_config": [],
+          "id": true,
+          "instance": true,
+          "project": true,
+          "state": true
+        },
+        "before_sensitive": false,
+        "after_sensitive": {
+          "ddl": [
+            false,
+            false
+          ],
+          "encryption_config": []
+        }
+      }
+    },
+    {
+      "address": "google_spanner_instance.main",
+      "mode": "managed",
+      "type": "google_spanner_instance",
+      "name": "main",
+      "provider_name": "registry.terraform.io/hashicorp/google",
+      "change": {
+        "actions": [
+          "create"
+        ],
+        "before": null,
+        "after": {
+          "config": "regional-europe-west1",
+          "display_name": "main-instance",
+          "force_destroy": false,
+          "labels": null,
+          "num_nodes": 1,
+          "timeouts": null
+        },
+        "after_unknown": {
+          "id": true,
+          "name": true,
+          "processing_units": true,
+          "project": true,
+          "state": true
+        },
+        "before_sensitive": false,
+        "after_sensitive": {}
+      }
+    }
+  ],
+  "configuration": {
+    "provider_config": {
+      "google": {
+        "name": "google",
+        "expressions": {
+          "project": {
+            "constant_value": "{{.Provider.project}}"
+          }
+        }
+      }
+    },
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database.database",
+          "mode": "managed",
+          "type": "google_spanner_database",
+          "name": "database",
+          "provider_config_key": "google",
+          "expressions": {
+            "ddl": {
+              "constant_value": [
+                "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
+                "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)"
+              ]
+            },
+            "deletion_protection": {
+              "constant_value": false
+            },
+            "instance": {
+              "references": [
+                "google_spanner_instance.main.name",
+                "google_spanner_instance.main"
+              ]
+            },
+            "name": {
+              "constant_value": "my-database"
+            }
+          },
+          "schema_version": 0
+        },
+        {
+          "address": "google_spanner_instance.main",
+          "mode": "managed",
+          "type": "google_spanner_instance",
+          "name": "main",
+          "provider_config_key": "google",
+          "expressions": {
+            "config": {
+              "constant_value": "regional-europe-west1"
+            },
+            "display_name": {
+              "constant_value": "main-instance"
+            },
+            "num_nodes": {
+              "constant_value": 1
+            }
+          },
+          "schema_version": 0
+        }
+      ]
+    }
+  }
+}
diff --git a/testdata/templates/example_spanner_database_iam_binding.json b/testdata/templates/example_spanner_database_iam_binding.json
new file mode 100644
index 00000000..43a38fcc
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_binding.json
@@ -0,0 +1,17 @@
+[
+  {
+    "name": "//spanner.googleapis.com/placeholder-BpLnfgDs",
+    "asset_type": "spanner.googleapis.com/Database",
+    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "iam_policy": {
+      "bindings": [
+        {
+          "role": "roles/compute.networkUser",
+          "members": [
+            "user:jane@example.com"
+          ]
+        }
+      ]
+    }
+  }
+]
diff --git a/testdata/templates/example_spanner_database_iam_binding.tf b/testdata/templates/example_spanner_database_iam_binding.tf
new file mode 100644
index 00000000..4d1fc0fe
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_binding.tf
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+terraform {
+  required_providers {
+    google = {
+      source = "hashicorp/google"
+      version = "~> {{.Provider.version}}"
+    }
+  }
+}
+
+provider "google" {
+  {{if .Provider.credentials }}credentials = "{{.Provider.credentials}}"{{end}}
+}
+
+resource "google_spanner_database_iam_binding" "database" {
+  instance = "my-instance"
+  database = "my-database"
+  role     = "roles/compute.networkUser"
+
+  members = [
+    "user:jane@example.com",
+  ]
+}
diff --git a/testdata/templates/example_spanner_database_iam_binding.tfplan.json b/testdata/templates/example_spanner_database_iam_binding.tfplan.json
new file mode 100644
index 00000000..0656bec1
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_binding.tfplan.json
@@ -0,0 +1,113 @@
+{
+  "format_version": "0.2",
+  "terraform_version": "1.0.10",
+  "planned_values": {
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_binding.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_binding",
+          "name": "database",
+          "provider_name": "registry.terraform.io/hashicorp/google",
+          "schema_version": 0,
+          "values": {
+            "condition": [],
+            "database": "my-database",
+            "instance": "my-instance",
+            "members": [
+              "user:jane@example.com"
+            ],
+            "role": "roles/compute.networkUser"
+          },
+          "sensitive_values": {
+            "condition": [],
+            "members": [
+              false
+            ]
+          }
+        }
+      ]
+    }
+  },
+  "resource_changes": [
+    {
+      "address": "google_spanner_database_iam_binding.database",
+      "mode": "managed",
+      "type": "google_spanner_database_iam_binding",
+      "name": "database",
+      "provider_name": "registry.terraform.io/hashicorp/google",
+      "change": {
+        "actions": [
+          "create"
+        ],
+        "before": null,
+        "after": {
+          "condition": [],
+          "database": "my-database",
+          "instance": "my-instance",
+          "members": [
+            "user:jane@example.com"
+          ],
+          "role": "roles/compute.networkUser"
+        },
+        "after_unknown": {
+          "condition": [],
+          "etag": true,
+          "id": true,
+          "members": [
+            false
+          ],
+          "project": true
+        },
+        "before_sensitive": false,
+        "after_sensitive": {
+          "condition": [],
+          "members": [
+            false
+          ]
+        }
+      }
+    }
+  ],
+  "configuration": {
+    "provider_config": {
+      "google": {
+        "name": "google",
+        "expressions": {
+          "project": {
+            "constant_value": "{{.Provider.project}}"
+          }
+        }
+      }
+    },
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_binding.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_binding",
+          "name": "database",
+          "provider_config_key": "google",
+          "expressions": {
+            "database": {
+              "constant_value": "my-database"
+            },
+            "instance": {
+              "constant_value": "my-instance"
+            },
+            "members": {
+              "constant_value": [
+                "user:jane@example.com"
+              ]
+            },
+            "role": {
+              "constant_value": "roles/compute.networkUser"
+            }
+          },
+          "schema_version": 0
+        }
+      ]
+    }
+  }
+}
diff --git a/testdata/templates/example_spanner_database_iam_member.json b/testdata/templates/example_spanner_database_iam_member.json
new file mode 100644
index 00000000..43a38fcc
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_member.json
@@ -0,0 +1,17 @@
+[
+  {
+    "name": "//spanner.googleapis.com/placeholder-BpLnfgDs",
+    "asset_type": "spanner.googleapis.com/Database",
+    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "iam_policy": {
+      "bindings": [
+        {
+          "role": "roles/compute.networkUser",
+          "members": [
+            "user:jane@example.com"
+          ]
+        }
+      ]
+    }
+  }
+]
diff --git a/testdata/templates/example_spanner_database_iam_member.tf b/testdata/templates/example_spanner_database_iam_member.tf
new file mode 100644
index 00000000..f7da1ce4
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_member.tf
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+terraform {
+  required_providers {
+    google = {
+      source = "hashicorp/google"
+      version = "~> {{.Provider.version}}"
+    }
+  }
+}
+
+provider "google" {
+  {{if .Provider.credentials }}credentials = "{{.Provider.credentials}}"{{end}}
+}
+
+
+resource "google_spanner_database_iam_member" "database" {
+  instance = "my-instance"
+  database = "my-database"
+  role     = "roles/compute.networkUser"
+  member   = "user:jane@example.com"
+}
diff --git a/testdata/templates/example_spanner_database_iam_member.tfplan.json b/testdata/templates/example_spanner_database_iam_member.tfplan.json
new file mode 100644
index 00000000..caa21ef0
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_member.tfplan.json
@@ -0,0 +1,98 @@
+{
+  "format_version": "0.2",
+  "terraform_version": "1.0.10",
+  "planned_values": {
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_member.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_member",
+          "name": "database",
+          "provider_name": "registry.terraform.io/hashicorp/google",
+          "schema_version": 0,
+          "values": {
+            "condition": [],
+            "database": "my-database",
+            "instance": "my-instance",
+            "member": "user:jane@example.com",
+            "role": "roles/compute.networkUser"
+          },
+          "sensitive_values": {
+            "condition": []
+          }
+        }
+      ]
+    }
+  },
+  "resource_changes": [
+    {
+      "address": "google_spanner_database_iam_member.database",
+      "mode": "managed",
+      "type": "google_spanner_database_iam_member",
+      "name": "database",
+      "provider_name": "registry.terraform.io/hashicorp/google",
+      "change": {
+        "actions": [
+          "create"
+        ],
+        "before": null,
+        "after": {
+          "condition": [],
+          "database": "my-database",
+          "instance": "my-instance",
+          "member": "user:jane@example.com",
+          "role": "roles/compute.networkUser"
+        },
+        "after_unknown": {
+          "condition": [],
+          "etag": true,
+          "id": true,
+          "project": true
+        },
+        "before_sensitive": false,
+        "after_sensitive": {
+          "condition": []
+        }
+      }
+    }
+  ],
+  "configuration": {
+    "provider_config": {
+      "google": {
+        "name": "google",
+        "expressions": {
+          "project": {
+            "constant_value": "{{.Provider.project}}"
+          }
+        }
+      }
+    },
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_member.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_member",
+          "name": "database",
+          "provider_config_key": "google",
+          "expressions": {
+            "database": {
+              "constant_value": "my-database"
+            },
+            "instance": {
+              "constant_value": "my-instance"
+            },
+            "member": {
+              "constant_value": "user:jane@example.com"
+            },
+            "role": {
+              "constant_value": "roles/compute.networkUser"
+            }
+          },
+          "schema_version": 0
+        }
+      ]
+    }
+  }
+}
diff --git a/testdata/templates/example_spanner_database_iam_policy.json b/testdata/templates/example_spanner_database_iam_policy.json
new file mode 100644
index 00000000..8f42a9b3
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_policy.json
@@ -0,0 +1,17 @@
+[
+  {
+    "name": "//spanner.googleapis.com/placeholder-BpLnfgDs",
+    "asset_type": "spanner.googleapis.com/Database",
+    "ancestry_path": "{{.Ancestry}}/project/{{.Provider.project}}",
+    "iam_policy": {
+      "bindings": [
+        {
+          "role": "roles/editor",
+          "members": [
+            "user:jane@example.com"
+          ]
+        }
+      ]
+    }
+  }
+]
diff --git a/testdata/templates/example_spanner_database_iam_policy.tf b/testdata/templates/example_spanner_database_iam_policy.tf
new file mode 100644
index 00000000..b8ab00d9
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_policy.tf
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+terraform {
+  required_providers {
+    google = {
+      source = "hashicorp/google"
+      version = "~> {{.Provider.version}}"
+    }
+  }
+}
+
+provider "google" {
+  {{if .Provider.credentials }}credentials = "{{.Provider.credentials}}"{{end}}
+}
+
+resource "google_spanner_database_iam_policy" "database" {
+  instance    = "my-instance"
+  database    = "my-database"
+  policy_data = jsonencode(
+    {
+      bindings = [
+        {
+          members = [
+            "user:jane@example.com",
+          ]
+          role = "roles/editor"
+        }
+      ]
+    }
+  )
+}
diff --git a/testdata/templates/example_spanner_database_iam_policy.tfplan.json b/testdata/templates/example_spanner_database_iam_policy.tfplan.json
new file mode 100644
index 00000000..7d9d01f7
--- /dev/null
+++ b/testdata/templates/example_spanner_database_iam_policy.tfplan.json
@@ -0,0 +1,153 @@
+{
+  "format_version": "0.2",
+  "terraform_version": "1.0.10",
+  "planned_values": {
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_policy.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_policy",
+          "name": "database",
+          "provider_name": "registry.terraform.io/hashicorp/google",
+          "schema_version": 0,
+          "values": {
+            "database": "my-database",
+            "instance": "my-instance",
+            "policy_data": "{\"bindings\":[{\"members\":[\"user:jane@example.com\"],\"role\":\"roles/editor\"}]}"
+          },
+          "sensitive_values": {}
+        }
+      ]
+    }
+  },
+  "resource_changes": [
+    {
+      "address": "google_spanner_database_iam_policy.database",
+      "mode": "managed",
+      "type": "google_spanner_database_iam_policy",
+      "name": "database",
+      "provider_name": "registry.terraform.io/hashicorp/google",
+      "change": {
+        "actions": [
+          "create"
+        ],
+        "before": null,
+        "after": {
+          "database": "my-database",
+          "instance": "my-instance",
+          "policy_data": "{\"bindings\":[{\"members\":[\"user:jane@example.com\"],\"role\":\"roles/editor\"}]}"
+        },
+        "after_unknown": {
+          "etag": true,
+          "id": true,
+          "project": true
+        },
+        "before_sensitive": false,
+        "after_sensitive": {}
+      }
+    }
+  ],
+  "prior_state": {
+    "format_version": "0.2",
+    "terraform_version": "1.0.10",
+    "values": {
+      "root_module": {
+        "resources": [
+          {
+            "address": "data.google_iam_policy.admin",
+            "mode": "data",
+            "type": "google_iam_policy",
+            "name": "admin",
+            "provider_name": "registry.terraform.io/hashicorp/google",
+            "schema_version": 0,
+            "values": {
+              "audit_config": null,
+              "binding": [
+                {
+                  "condition": [],
+                  "members": [
+                    "user:jane@example.com"
+                  ],
+                  "role": "roles/editor"
+                }
+              ],
+              "id": "1120114865",
+              "policy_data": "{\"bindings\":[{\"members\":[\"user:jane@example.com\"],\"role\":\"roles/editor\"}]}"
+            },
+            "sensitive_values": {
+              "binding": [
+                {
+                  "condition": [],
+                  "members": [
+                    false
+                  ]
+                }
+              ]
+            }
+          }
+        ]
+      }
+    }
+  },
+  "configuration": {
+    "provider_config": {
+      "google": {
+        "name": "google",
+        "expressions": {
+          "project": {
+            "constant_value": "{{.Provider.project}}"
+          }
+        }
+      }
+    },
+    "root_module": {
+      "resources": [
+        {
+          "address": "google_spanner_database_iam_policy.database",
+          "mode": "managed",
+          "type": "google_spanner_database_iam_policy",
+          "name": "database",
+          "provider_config_key": "google",
+          "expressions": {
+            "database": {
+              "constant_value": "my-database"
+            },
+            "instance": {
+              "constant_value": "my-instance"
+            },
+            "policy_data": {
+              "references": [
+                "data.google_iam_policy.admin.policy_data",
+                "data.google_iam_policy.admin"
+              ]
+            }
+          },
+          "schema_version": 0
+        },
+        {
+          "address": "data.google_iam_policy.admin",
+          "mode": "data",
+          "type": "google_iam_policy",
+          "name": "admin",
+          "provider_config_key": "google",
+          "expressions": {
+            "binding": [
+              {
+                "members": {
+                  "constant_value": [
+                    "user:jane@example.com"
+                  ]
+                },
+                "role": {
+                  "constant_value": "roles/editor"
+                }
+              }
+            ]
+          },
+          "schema_version": 0
+        }
+      ]
+    }
+  }
+}