From e686ea5d9b5b084a9f993083b4596c395d708b16 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Thu, 26 Apr 2018 07:45:21 -0700 Subject: [PATCH] Add support for IAM on Spanner Instances. Support managing IAM policies on Spanner instances. --- google/iam_spanner_instance.go | 130 +++++++++++ google/provider.go | 3 + google/resource_spanner_instance_iam_test.go | 215 +++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 google/iam_spanner_instance.go create mode 100644 google/resource_spanner_instance_iam_test.go diff --git a/google/iam_spanner_instance.go b/google/iam_spanner_instance.go new file mode 100644 index 00000000..c3eb4082 --- /dev/null +++ b/google/iam_spanner_instance.go @@ -0,0 +1,130 @@ +package google + +import ( + "fmt" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + spanner "google.golang.org/api/spanner/v1" +) + +var IamSpannerInstanceSchema = map[string]*schema.Schema{ + "instance": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, +} + +type SpannerInstanceIamUpdater struct { + project string + instance string + Config *Config +} + +func NewSpannerInstanceIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) { + project, err := getProject(d, config) + if err != nil { + return nil, err + } + + return &SpannerInstanceIamUpdater{ + project: project, + instance: d.Get("instance").(string), + Config: config, + }, nil +} + +func SpannerInstanceIdParseFunc(d *schema.ResourceData, config *Config) error { + id, err := extractSpannerInstanceId(d.Id()) + if err != nil { + return err + } + d.Set("instance", id.Instance) + d.Set("project", id.Project) + + // Explicitly set the id so imported resources have the same ID format as non-imported ones. + d.SetId(id.terraformId()) + return nil +} + +func (u *SpannerInstanceIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + p, err := u.Config.clientSpanner.Projects.Instances.GetIamPolicy(spannerInstanceId{ + Project: u.project, + Instance: u.instance, + }.instanceUri(), &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 *SpannerInstanceIamUpdater) 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) + } + + _, err = u.Config.clientSpanner.Projects.Instances.SetIamPolicy(spannerInstanceId{ + Project: u.project, + Instance: u.instance, + }.instanceUri(), &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 *SpannerInstanceIamUpdater) GetResourceId() string { + return spannerInstanceId{ + Project: u.project, + Instance: u.instance, + }.terraformId() +} + +func (u *SpannerInstanceIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-spanner-instance-%s-%s", u.project, u.instance) +} + +func (u *SpannerInstanceIamUpdater) DescribeResource() string { + return fmt.Sprintf("Spanner Instance: %s/%s", u.project, u.instance) +} + +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 +} diff --git a/google/provider.go b/google/provider.go index 890c91b8..ce613c0a 100644 --- a/google/provider.go +++ b/google/provider.go @@ -172,6 +172,9 @@ func Provider() terraform.ResourceProvider { "google_kms_crypto_key_iam_member": ResourceIamMemberWithImport(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc), "google_sourcerepo_repository": resourceSourceRepoRepository(), "google_spanner_instance": resourceSpannerInstance(), + "google_spanner_instance_iam_binding": ResourceIamBindingWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), + "google_spanner_instance_iam_member": ResourceIamMemberWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), + "google_spanner_instance_iam_policy": ResourceIamPolicyWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc), "google_spanner_database": resourceSpannerDatabase(), "google_sql_database": resourceSqlDatabase(), "google_sql_database_instance": resourceSqlDatabaseInstance(), diff --git a/google/resource_spanner_instance_iam_test.go b/google/resource_spanner_instance_iam_test.go new file mode 100644 index 00000000..0609c8fc --- /dev/null +++ b/google/resource_spanner_instance_iam_test.go @@ -0,0 +1,215 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccSpannerInstanceIamBinding(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + role := "roles/spanner.databaseAdmin" + project := getTestProjectFromEnv() + instance := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccSpannerInstanceIamBinding_basic(account, instance, role), + }, + resource.TestStep{ + ResourceName: "google_spanner_instance_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s %s", spannerInstanceId{ + Project: project, + Instance: instance, + }.terraformId(), role), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccSpannerInstanceIamBinding_update(account, instance, role), + }, + resource.TestStep{ + ResourceName: "google_spanner_instance_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s %s", spannerInstanceId{ + Project: project, + Instance: instance, + }.terraformId(), role), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSpannerInstanceIamMember(t *testing.T) { + t.Parallel() + + project := getTestProjectFromEnv() + account := acctest.RandomWithPrefix("tf-test") + role := "roles/spanner.databaseAdmin" + instance := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccSpannerInstanceIamMember_basic(account, instance, role), + }, + resource.TestStep{ + ResourceName: "google_spanner_instance_iam_member.foo", + ImportStateId: fmt.Sprintf("%s %s serviceAccount:%s@%s.iam.gserviceaccount.com", spannerInstanceId{ + Instance: instance, + Project: project, + }.terraformId(), role, account, project), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSpannerInstanceIamPolicy(t *testing.T) { + t.Parallel() + + project := getTestProjectFromEnv() + account := acctest.RandomWithPrefix("tf-test") + role := "roles/spanner.databaseAdmin" + instance := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccSpannerInstanceIamPolicy_basic(account, instance, role), + }, + // Test a few import formats + resource.TestStep{ + ResourceName: "google_spanner_instance_iam_policy.foo", + ImportStateId: fmt.Sprintf("%s", spannerInstanceId{ + Instance: instance, + Project: project, + }.terraformId()), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccSpannerInstanceIamBinding_basic(account, instance, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Spanner Iam Testing Account" +} + +resource "google_spanner_instance" "instance" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + num_nodes = 1 +} + +resource "google_spanner_instance_iam_binding" "foo" { + project = "${google_spanner_instance.instance.project}" + instance = "${google_spanner_instance.instance.name}" + role = "%s" + members = ["serviceAccount:${google_service_account.test_account.email}"] +} +`, account, instance, instance, roleId) +} + +func testAccSpannerInstanceIamBinding_update(account, instance, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Spanner Iam Testing Account" +} + +resource "google_service_account" "test_account_2" { + account_id = "%s-2" + display_name = "Spanner Iam Testing Account" +} + +resource "google_spanner_instance" "instance" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + num_nodes = 1 +} + +resource "google_spanner_instance_iam_binding" "foo" { + project = "${google_spanner_instance.instance.project}" + instance = "${google_spanner_instance.instance.name}" + role = "%s" + members = [ + "serviceAccount:${google_service_account.test_account.email}", + "serviceAccount:${google_service_account.test_account_2.email}" + ] +} +`, account, account, instance, instance, roleId) +} + +func testAccSpannerInstanceIamMember_basic(account, instance, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Spanner Iam Testing Account" +} + +resource "google_spanner_instance" "instance" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + num_nodes = 1 +} + +resource "google_spanner_instance_iam_member" "foo" { + project = "${google_spanner_instance.instance.project}" + instance = "${google_spanner_instance.instance.name}" + role = "%s" + member = "serviceAccount:${google_service_account.test_account.email}" +} +`, account, instance, instance, roleId) +} + +func testAccSpannerInstanceIamPolicy_basic(account, instance, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Spanner Iam Testing Account" +} + +resource "google_spanner_instance" "instance" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + num_nodes = 1 +} + +data "google_iam_policy" "foo" { + binding { + role = "%s" + + members = ["serviceAccount:${google_service_account.test_account.email}"] + } +} + +resource "google_spanner_instance_iam_policy" "foo" { + project = "${google_spanner_instance.instance.project}" + instance = "${google_spanner_instance.instance.name}" + policy_data = "${data.google_iam_policy.foo.policy_data}" +} +`, account, instance, instance, roleId) +}