diff --git a/.ci/magic-modules/create-pr.sh b/.ci/magic-modules/create-pr.sh index 041f98dcb2a2..885492e1d0c9 100755 --- a/.ci/magic-modules/create-pr.sh +++ b/.ci/magic-modules/create-pr.sh @@ -82,7 +82,7 @@ if [ "$BRANCH_NAME" = "$ORIGINAL_PR_BRANCH" ]; then fi git checkout -b "$BRANCH_NAME" - if INSPEC_PR=$(hub pull-request -b "$INSPEC_REPO_USER/inspec:master" -F ./downstream_body); then + if INSPEC_PR=$(hub pull-request -b "$INSPEC_REPO_USER/inspec-gcp:master" -F ./downstream_body); then DEPENDENCIES="${DEPENDENCIES}depends: $INSPEC_PR ${NEWLINE}" else echo "InSpec - did not generate a PR." diff --git a/build/inspec b/build/inspec index 257d3f760307..f359ebbf01dc 160000 --- a/build/inspec +++ b/build/inspec @@ -1 +1 @@ -Subproject commit 257d3f760307559938568aeed74261df5cde4b87 +Subproject commit f359ebbf01dc1294dc5338ad2dc380a888a14563 diff --git a/build/terraform b/build/terraform index 56f6530712f9..d622ec466e68 160000 --- a/build/terraform +++ b/build/terraform @@ -1 +1 @@ -Subproject commit 56f6530712f9cbfa5db40e9238d35703931fa951 +Subproject commit d622ec466e688b09c601d34f5ce1c7e5796dadf3 diff --git a/products/compute/inspec.yaml b/products/compute/inspec.yaml index 6523b93ef4ab..bcd603cee34d 100644 --- a/products/compute/inspec.yaml +++ b/products/compute/inspec.yaml @@ -19,6 +19,87 @@ manifest: !ruby/object:Provider::Inspec::Manifest summary: 'InSpec resources for verifying GCP infrastructure' description: | InSpec resources for verifying GCP infrastructure +overrides: !ruby/object:Provider::ResourceOverrides + Address: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Autoscaler: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + BackendBucket: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + BackendService: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Disk: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + DiskType: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Firewall: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + ForwardingRule: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + GlobalAddress: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + GlobalForwardingRule: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + HealthCheck: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + HttpHealthCheck: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + HttpsHealthCheck: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Image: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Instance: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + InstanceGroup: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + InstanceGroupManager: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + InstanceTemplate: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + InterconnectAttachment: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + License: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + MachineType: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Network: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Region: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + RegionAutoscaler: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + RegionDisk: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + RegionDiskType: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Route: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Router: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Snapshot: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + SslCertificate: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + SslPolicy: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + Subnetwork: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetHttpProxy: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetHttpsProxy: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetPool: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetTcpProxy: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetVpnGateway: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + TargetSslProxy: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + UrlMap: !ruby/object:Provider::Chef::ResourceOverride + exclude: true + VpnTunnel: !ruby/object:Provider::Chef::ResourceOverride + exclude: true files: !ruby/object:Provider::Config::Files style: functions: diff --git a/provider/inspec.rb b/provider/inspec.rb index 7f66da907f30..69439d7b94de 100644 --- a/provider/inspec.rb +++ b/provider/inspec.rb @@ -21,6 +21,7 @@ module Provider # Code generator for Example Cookbooks that manage Google Cloud Platform # resources. class Inspec < Provider::Core + include Google::RubyUtils # Settings for the provider class Config < Provider::Config attr_reader :manifest @@ -53,6 +54,14 @@ def generate_resource(data) ) end + # Returns the url that this object can be retrieved from + # based off of the self link + def url(object) + url = object.self_link_url[1] + return url.join('') if url.is_a?(Array) + url.split("\n").join('') + end + # TODO? def generate_resource_tests(data) end diff --git a/provider/terraform/tests/resource_google_organization_iam_custom_role_test.go b/provider/terraform/tests/resource_google_organization_iam_custom_role_test.go new file mode 100644 index 000000000000..6d7fef648515 --- /dev/null +++ b/provider/terraform/tests/resource_google_organization_iam_custom_role_test.go @@ -0,0 +1,245 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccOrganizationIamCustomRole_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + roleId := "tfIamCustomRole" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleOrganizationIamCustomRole_basic(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRole( + "google_organization_iam_custom_role.foo", + "My Custom Role", + "foo", + "GA", + []string{"resourcemanager.projects.list"}), + }, + { + Config: testAccCheckGoogleOrganizationIamCustomRole_update(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRole( + "google_organization_iam_custom_role.foo", + "My Custom Role Updated", + "bar", + "BETA", + []string{"resourcemanager.projects.list", "resourcemanager.organizations.get"}), + }, + { + ResourceName: "google_organization_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccOrganizationIamCustomRole_undelete(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + roleId := "tfIamCustomRole" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleOrganizationIamCustomRole_basic(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRoleDeletionStatus("google_organization_iam_custom_role.foo", false), + }, + // Soft-delete + { + Config: testAccCheckGoogleOrganizationIamCustomRole_deleted(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRoleDeletionStatus("google_organization_iam_custom_role.foo", true), + }, + // Undelete + { + Config: testAccCheckGoogleOrganizationIamCustomRole_basic(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRoleDeletionStatus("google_organization_iam_custom_role.foo", false), + }, + }, + }) +} + +func TestAccOrganizationIamCustomRole_createAfterDestroy(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + roleId := "tfIamCustomRole" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleOrganizationIamCustomRole_basic(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRole( + "google_organization_iam_custom_role.foo", + "My Custom Role", + "foo", + "GA", + []string{"resourcemanager.projects.list"}), + }, + // Destroy resources + { + Config: " ", + Destroy: true, + }, + // Re-create with no existing state + { + Config: testAccCheckGoogleOrganizationIamCustomRole_basic(org, roleId), + Check: testAccCheckGoogleOrganizationIamCustomRole( + "google_organization_iam_custom_role.foo", + "My Custom Role", + "foo", + "GA", + []string{"resourcemanager.projects.list"}), + }, + }, + }) +} + +func testAccCheckGoogleOrganizationIamCustomRoleDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_organization_iam_custom_role" { + continue + } + + role, err := config.clientIAM.Organizations.Roles.Get(rs.Primary.ID).Do() + + if err != nil { + return err + } + + if !role.Deleted { + return fmt.Errorf("Iam custom role still exists") + } + + } + + return nil +} + +func testAccCheckGoogleOrganizationIamCustomRole(n, title, description, stage string, permissions []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + role, err := config.clientIAM.Organizations.Roles.Get(rs.Primary.ID).Do() + + if err != nil { + return err + } + + if title != role.Title { + return fmt.Errorf("Incorrect title. Expected %q, got %q", title, role.Title) + } + + if description != role.Description { + return fmt.Errorf("Incorrect description. Expected %q, got %q", description, role.Description) + } + + if stage != role.Stage { + return fmt.Errorf("Incorrect stage. Expected %q, got %q", stage, role.Stage) + } + + sort.Strings(permissions) + sort.Strings(role.IncludedPermissions) + if !reflect.DeepEqual(permissions, role.IncludedPermissions) { + return fmt.Errorf("Incorrect permissions. Expected %q, got %q", permissions, role.IncludedPermissions) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationIamCustomRoleDeletionStatus(n string, deleted bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + role, err := config.clientIAM.Organizations.Roles.Get(rs.Primary.ID).Do() + + if err != nil { + return err + } + + if deleted != role.Deleted { + return fmt.Errorf("Incorrect deletion status. Expected %t, got %t", deleted, role.Deleted) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationIamCustomRole_basic(orgId, roleId string) string { + return fmt.Sprintf(` +resource "google_organization_iam_custom_role" "foo" { + role_id = "%s" + org_id = "%s" + title = "My Custom Role" + description = "foo" + permissions = ["resourcemanager.projects.list"] +} +`, roleId, orgId) +} + +func testAccCheckGoogleOrganizationIamCustomRole_deleted(orgId, roleId string) string { + return fmt.Sprintf(` +resource "google_organization_iam_custom_role" "foo" { + role_id = "%s" + org_id = "%s" + title = "My Custom Role" + description = "foo" + permissions = ["resourcemanager.projects.list"] + deleted = true +} +`, roleId, orgId) +} + +func testAccCheckGoogleOrganizationIamCustomRole_update(orgId, roleId string) string { + return fmt.Sprintf(` +resource "google_organization_iam_custom_role" "foo" { + role_id = "%s" + org_id = "%s" + title = "My Custom Role Updated" + description = "bar" + permissions = ["resourcemanager.projects.list", "resourcemanager.organizations.get"] + stage = "BETA" +} +`, roleId, orgId) +} diff --git a/provider/terraform/tests/resource_google_organization_iam_test.go b/provider/terraform/tests/resource_google_organization_iam_test.go new file mode 100644 index 000000000000..8b0df19e9d99 --- /dev/null +++ b/provider/terraform/tests/resource_google_organization_iam_test.go @@ -0,0 +1,206 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +// Bindings and members are tested serially to avoid concurrent updates of the org's IAM policy. +// When concurrent changes happen, the behavior is to abort and ask the user to retry allowing +// them to see the new diff instead of blindly overriding the policy stored in GCP. This desired +// behavior however induces flakiness in our acceptance tests, hence the need for running them +// serially. +// Policies are *not tested*, because testing them will ruin changes made to the test org. +func TestAccOrganizationIam(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + account := acctest.RandomWithPrefix("tf-test") + roleId := "tfIamTest" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Binding creation + Config: testAccOrganizationIamBinding_basic(account, roleId, org), + Check: testAccCheckGoogleOrganizationIamBindingExists("foo", "test-role", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_organization_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s organizations/%s/roles/%s", org, org, roleId), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccOrganizationIamBinding_update(account, roleId, org), + Check: testAccCheckGoogleOrganizationIamBindingExists("foo", "test-role", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + fmt.Sprintf("serviceAccount:%s-2@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_organization_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s organizations/%s/roles/%s", org, org, roleId), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccOrganizationIamMember_basic(account, org), + Check: testAccCheckGoogleOrganizationIamMemberExists("foo", "roles/browser", + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + ), + }, + { + ResourceName: "google_organization_iam_member.foo", + ImportStateId: fmt.Sprintf("%s roles/browser serviceAccount:%s@%s.iam.gserviceaccount.com", org, account, getTestProjectFromEnv()), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGoogleOrganizationIamBindingExists(bindingResourceName, roleResourceName string, members []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + bindingRs, ok := s.RootModule().Resources["google_organization_iam_binding."+bindingResourceName] + if !ok { + return fmt.Errorf("Not found: %s", bindingResourceName) + } + + roleRs, ok := s.RootModule().Resources["google_organization_iam_custom_role."+roleResourceName] + if !ok { + return fmt.Errorf("Not found: %s", roleResourceName) + } + + config := testAccProvider.Meta().(*Config) + p, err := config.clientResourceManager.Organizations.GetIamPolicy("organizations/"+bindingRs.Primary.Attributes["org_id"], &cloudresourcemanager.GetIamPolicyRequest{}).Do() + if err != nil { + return err + } + + for _, binding := range p.Bindings { + if binding.Role == roleRs.Primary.ID { + sort.Strings(members) + sort.Strings(binding.Members) + + if reflect.DeepEqual(members, binding.Members) { + return nil + } + + return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members) + } + } + + return fmt.Errorf("No binding for role %q", roleRs.Primary.ID) + } +} + +func testAccCheckGoogleOrganizationIamMemberExists(n, role, member string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["google_organization_iam_member."+n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + config := testAccProvider.Meta().(*Config) + p, err := config.clientResourceManager.Organizations.GetIamPolicy("organizations/"+rs.Primary.Attributes["org_id"], &cloudresourcemanager.GetIamPolicyRequest{}).Do() + if err != nil { + return err + } + + for _, binding := range p.Bindings { + if binding.Role == role { + for _, m := range binding.Members { + if m == member { + return nil + } + } + + return fmt.Errorf("Missing member %q, got %v", member, binding.Members) + } + } + + return fmt.Errorf("No binding for role %q", role) + } +} + +// We are using a custom role since iam_binding is authoritative on the member list and +// we want to avoid removing members from an existing role to prevent unwanted side effects. +func testAccOrganizationIamBinding_basic(account, role, org string) string { + return fmt.Sprintf(` +resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_organization_iam_custom_role" "test-role" { + role_id = "%s" + org_id = "%s" + title = "Iam Testing Role" + permissions = ["genomics.datasets.get"] +} + +resource "google_organization_iam_binding" "foo" { + org_id = "%s" + role = "${google_organization_iam_custom_role.test-role.id}" + members = ["serviceAccount:${google_service_account.test-account.email}"] +} +`, account, role, org, org) +} + +func testAccOrganizationIamBinding_update(account, role, org string) string { + return fmt.Sprintf(` +resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_organization_iam_custom_role" "test-role" { + role_id = "%s" + org_id = "%s" + title = "Iam Testing Role" + permissions = ["genomics.datasets.get"] +} + +resource "google_service_account" "test-account-2" { + account_id = "%s-2" + display_name = "Iam Testing Account" +} + +resource "google_organization_iam_binding" "foo" { + org_id = "%s" + role = "${google_organization_iam_custom_role.test-role.id}" + members = [ + "serviceAccount:${google_service_account.test-account.email}", + "serviceAccount:${google_service_account.test-account-2.email}" + ] +} +`, account, role, org, account, org) +} + +func testAccOrganizationIamMember_basic(account, org string) string { + return fmt.Sprintf(` +resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_organization_iam_member" "foo" { + org_id = "%s" + role = "roles/browser" + member = "serviceAccount:${google_service_account.test-account.email}" +} +`, account, org) +} diff --git a/provider/terraform/tests/resource_google_organization_policy_test.go b/provider/terraform/tests/resource_google_organization_policy_test.go new file mode 100644 index 000000000000..b62a0bac9272 --- /dev/null +++ b/provider/terraform/tests/resource_google_organization_policy_test.go @@ -0,0 +1,391 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +var DENIED_ORG_POLICIES = []string{ + "doubleclicksearch.googleapis.com", + "replicapoolupdater.googleapis.com", +} + +// Since each test here is acting on the same organization, run the tests serially to +// avoid race conditions and aborted operations. +func TestAccOrganizationPolicy(t *testing.T) { + testCases := map[string]func(t *testing.T){ + "boolean": testAccOrganizationPolicy_boolean, + "list_allowAll": testAccOrganizationPolicy_list_allowAll, + "list_allowSome": testAccOrganizationPolicy_list_allowSome, + "list_denySome": testAccOrganizationPolicy_list_denySome, + "list_update": testAccOrganizationPolicy_list_update, + "restore_policy": testAccOrganizationPolicy_restore_defaultTrue, + } + + for name, tc := range testCases { + // shadow the tc variable into scope so that when + // the loop continues, if t.Run hasn't executed tc(t) + // yet, we don't have a race condition + // see https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } +} + +func testAccOrganizationPolicy_boolean(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + // Test creation of an enforced boolean policy + Config: testAccOrganizationPolicyConfig_boolean(org, true), + Check: testAccCheckGoogleOrganizationBooleanPolicy("bool", true), + }, + { + // Test update from enforced to not + Config: testAccOrganizationPolicyConfig_boolean(org, false), + Check: testAccCheckGoogleOrganizationBooleanPolicy("bool", false), + }, + { + Config: " ", + Destroy: true, + }, + { + // Test creation of a not enforced boolean policy + Config: testAccOrganizationPolicyConfig_boolean(org, false), + Check: testAccCheckGoogleOrganizationBooleanPolicy("bool", false), + }, + { + // Test update from not enforced to enforced + Config: testAccOrganizationPolicyConfig_boolean(org, true), + Check: testAccCheckGoogleOrganizationBooleanPolicy("bool", true), + }, + { + ResourceName: "google_organization_policy.bool", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + +} + +func testAccOrganizationPolicy_list_allowAll(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationPolicyConfig_list_allowAll(org), + Check: testAccCheckGoogleOrganizationListPolicyAll("list", "ALLOW"), + }, + { + ResourceName: "google_organization_policy.list", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccOrganizationPolicy_list_allowSome(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + project := getTestProjectFromEnv() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationPolicyConfig_list_allowSome(org, project), + Check: testAccCheckGoogleOrganizationListPolicyAllowedValues("list", []string{"projects/" + project, "projects/debian-cloud"}), + }, + { + ResourceName: "google_organization_policy.list", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccOrganizationPolicy_list_denySome(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationPolicyConfig_list_denySome(org), + Check: testAccCheckGoogleOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + { + ResourceName: "google_organization_policy.list", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccOrganizationPolicy_list_update(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationPolicyConfig_list_allowAll(org), + Check: testAccCheckGoogleOrganizationListPolicyAll("list", "ALLOW"), + }, + { + Config: testAccOrganizationPolicyConfig_list_denySome(org), + Check: testAccCheckGoogleOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + { + ResourceName: "google_organization_policy.list", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccOrganizationPolicy_restore_defaultTrue(t *testing.T) { + org := getTestOrgTargetFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationPolicyConfig_restore_defaultTrue(org), + Check: testAccCheckGoogleOrganizationRestoreDefaultTrue("restore", &cloudresourcemanager.RestoreDefault{}), + }, + { + ResourceName: "google_organization_policy.restore", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGoogleOrganizationPolicyDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_organization_policy" { + continue + } + + org := "organizations/" + rs.Primary.Attributes["org_id"] + constraint := canonicalOrgPolicyConstraint(rs.Primary.Attributes["constraint"]) + policy, err := config.clientResourceManager.Organizations.GetOrgPolicy(org, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: constraint, + }).Do() + + if err != nil { + return err + } + + if policy.ListPolicy != nil || policy.BooleanPolicy != nil { + return fmt.Errorf("Org policy with constraint '%s' hasn't been cleared", constraint) + } + } + return nil +} + +func testAccCheckGoogleOrganizationBooleanPolicy(n string, enforced bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if policy.BooleanPolicy.Enforced != enforced { + return fmt.Errorf("Expected boolean policy enforcement to be '%t', got '%t'", enforced, policy.BooleanPolicy.Enforced) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationListPolicyAll(n, policyType string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if len(policy.ListPolicy.AllowedValues) > 0 || len(policy.ListPolicy.DeniedValues) > 0 { + return fmt.Errorf("The `values` field shouldn't be set") + } + + if policy.ListPolicy.AllValues != policyType { + return fmt.Errorf("Expected the list policy to '%s' all values, got '%s'", policyType, policy.ListPolicy.AllValues) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationListPolicyAllowedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.AllowedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.AllowedValues, values) { + return fmt.Errorf("Expected the list policy to allow '%s', instead allowed '%s'", values, policy.ListPolicy.AllowedValues) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationListPolicyDeniedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.DeniedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.DeniedValues, values) { + return fmt.Errorf("Expected the list policy to deny '%s', instead denied '%s'", values, policy.ListPolicy.DeniedValues) + } + + return nil + } +} + +func testAccCheckGoogleOrganizationRestoreDefaultTrue(n string, policyDefault *cloudresourcemanager.RestoreDefault) resource.TestCheckFunc { + return func(s *terraform.State) error { + + policy, err := getGoogleOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if !reflect.DeepEqual(policy.RestoreDefault, policyDefault) { + return fmt.Errorf("Expected the restore default '%s', instead denied, %s", policyDefault, policy.RestoreDefault) + } + + return nil + } +} + +func getGoogleOrganizationPolicyTestResource(s *terraform.State, n string) (*cloudresourcemanager.OrgPolicy, error) { + rn := "google_organization_policy." + n + rs, ok := s.RootModule().Resources[rn] + if !ok { + return nil, fmt.Errorf("Not found: %s", rn) + } + + if rs.Primary.ID == "" { + return nil, fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + return config.clientResourceManager.Organizations.GetOrgPolicy("organizations/"+rs.Primary.Attributes["org_id"], &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: rs.Primary.Attributes["constraint"], + }).Do() +} + +func testAccOrganizationPolicyConfig_boolean(org string, enforced bool) string { + return fmt.Sprintf(` +resource "google_organization_policy" "bool" { + org_id = "%s" + constraint = "constraints/compute.disableSerialPortAccess" + + boolean_policy { + enforced = %t + } +} +`, org, enforced) +} + +func testAccOrganizationPolicyConfig_list_allowAll(org string) string { + return fmt.Sprintf(` +resource "google_organization_policy" "list" { + org_id = "%s" + constraint = "constraints/serviceuser.services" + + list_policy { + allow { + all = true + } + } +} +`, org) +} + +func testAccOrganizationPolicyConfig_list_allowSome(org, project string) string { + return fmt.Sprintf(` +resource "google_organization_policy" "list" { + org_id = "%s" + constraint = "constraints/compute.trustedImageProjects" + + list_policy { + allow { + values = [ + "projects/%s", + "projects/debian-cloud" + ] + } + } +} +`, org, project) +} + +func testAccOrganizationPolicyConfig_list_denySome(org string) string { + return fmt.Sprintf(` +resource "google_organization_policy" "list" { + org_id = "%s" + constraint = "serviceuser.services" + + list_policy { + deny { + values = [ + "doubleclicksearch.googleapis.com", + "replicapoolupdater.googleapis.com", + ] + } + } +} +`, org) +} + +func testAccOrganizationPolicyConfig_restore_defaultTrue(org string) string { + return fmt.Sprintf(` +resource "google_organization_policy" "restore" { + org_id = "%s" + constraint = "serviceuser.services" + + restore_policy { + default = true + } +} +`, org) +} diff --git a/provider/terraform/tests/resource_google_project_iam_binding_test.go b/provider/terraform/tests/resource_google_project_iam_binding_test.go new file mode 100644 index 000000000000..1bdbf1448306 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_iam_binding_test.go @@ -0,0 +1,257 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func projectIamBindingImportStep(resourceName, pid, role string) resource.TestStep { + return resource.TestStep{ + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s %s", pid, role), + ImportState: true, + ImportStateVerify: true, + } +} + +// Test that an IAM binding can be applied to a project +func TestAccProjectIamBinding_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + role := "roles/compute.instanceAdmin" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateBindingBasic(pid, pname, org, role), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + }, + }) +} + +// Test that multiple IAM bindings can be applied to a project, one at a time +func TestAccProjectIamBinding_multiple(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + role := "roles/compute.instanceAdmin" + role2 := "roles/viewer" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateBindingBasic(pid, pname, org, role), + }, + // Apply another IAM binding + { + Config: testAccProjectAssociateBindingMultiple(pid, pname, org, role, role2), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + projectIamBindingImportStep("google_project_iam_binding.multiple", pid, role2), + }, + }) +} + +// Test that multiple IAM bindings can be applied to a project all at once +func TestAccProjectIamBinding_multipleAtOnce(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + role := "roles/compute.instanceAdmin" + role2 := "roles/viewer" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateBindingMultiple(pid, pname, org, role, role2), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + projectIamBindingImportStep("google_project_iam_binding.multiple", pid, role2), + }, + }) +} + +// Test that an IAM binding can be updated once applied to a project +func TestAccProjectIamBinding_update(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + role := "roles/compute.instanceAdmin" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateBindingBasic(pid, pname, org, role), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + + // Apply an updated IAM binding + { + Config: testAccProjectAssociateBindingUpdated(pid, pname, org, role), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + + // Drop the original member + { + Config: testAccProjectAssociateBindingDropMemberFromBasic(pid, pname, org, role), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + }, + }) +} + +// Test that an IAM binding can be removed from a project +func TestAccProjectIamBinding_remove(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + role := "roles/compute.instanceAdmin" + role2 := "roles/viewer" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply multiple IAM bindings + { + Config: testAccProjectAssociateBindingMultiple(pid, pname, org, role, role2), + }, + projectIamBindingImportStep("google_project_iam_binding.acceptance", pid, role), + projectIamBindingImportStep("google_project_iam_binding.multiple", pid, role2), + + // Remove the bindings + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + }, + }) +} + +func testAccProjectAssociateBindingBasic(pid, name, org, role string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_binding" "acceptance" { + project = "${google_project.acceptance.project_id}" + members = ["user:admin@hashicorptest.com"] + role = "%s" +} +`, pid, name, org, role) +} + +func testAccProjectAssociateBindingMultiple(pid, name, org, role, role2 string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_binding" "acceptance" { + project = "${google_project.acceptance.project_id}" + members = ["user:admin@hashicorptest.com"] + role = "%s" +} + +resource "google_project_iam_binding" "multiple" { + project = "${google_project.acceptance.project_id}" + members = ["user:paddy@hashicorp.com"] + role = "%s" +} +`, pid, name, org, role, role2) +} + +func testAccProjectAssociateBindingUpdated(pid, name, org, role string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_binding" "acceptance" { + project = "${google_project.acceptance.project_id}" + members = ["user:admin@hashicorptest.com", "user:paddy@hashicorp.com"] + role = "%s" +} +`, pid, name, org, role) +} + +func testAccProjectAssociateBindingDropMemberFromBasic(pid, name, org, role string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_binding" "acceptance" { + project = "${google_project.acceptance.project_id}" + members = ["user:paddy@hashicorp.com"] + role = "%s" +} +`, pid, name, org, role) +} diff --git a/provider/terraform/tests/resource_google_project_iam_custom_role_test.go b/provider/terraform/tests/resource_google_project_iam_custom_role_test.go new file mode 100644 index 000000000000..f0e396ca0a30 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_iam_custom_role_test.go @@ -0,0 +1,177 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccProjectIamCustomRole_basic(t *testing.T) { + t.Parallel() + + roleId := "tfIamCustomRole" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleProjectIamCustomRole_basic(roleId), + Check: resource.TestCheckResourceAttr("google_project_iam_custom_role.foo", "stage", "GA"), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccCheckGoogleProjectIamCustomRole_update(roleId), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProjectIamCustomRole_undelete(t *testing.T) { + t.Parallel() + + roleId := "tfIamCustomRole" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleProjectIamCustomRole_basic(roleId), + Check: resource.TestCheckResourceAttr("google_project_iam_custom_role.foo", "deleted", "false"), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + // Soft-delete + { + Config: testAccCheckGoogleProjectIamCustomRole_deleted(roleId), + Check: resource.TestCheckResourceAttr("google_project_iam_custom_role.foo", "deleted", "true"), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + // Undelete + { + Config: testAccCheckGoogleProjectIamCustomRole_basic(roleId), + Check: resource.TestCheckResourceAttr("google_project_iam_custom_role.foo", "deleted", "false"), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProjectIamCustomRole_createAfterDestroy(t *testing.T) { + t.Parallel() + + roleId := "tfIamCustomRole" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectIamCustomRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleProjectIamCustomRole_basic(roleId), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + // Destroy resources + { + Config: " ", + Destroy: true, + }, + // Re-create with no existing state + { + Config: testAccCheckGoogleProjectIamCustomRole_basic(roleId), + }, + { + ResourceName: "google_project_iam_custom_role.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGoogleProjectIamCustomRoleDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_project_iam_custom_role" { + continue + } + + role, err := config.clientIAM.Projects.Roles.Get(rs.Primary.ID).Do() + + if err != nil { + return err + } + + if !role.Deleted { + return fmt.Errorf("Iam custom role still exists") + } + + } + + return nil +} + +func testAccCheckGoogleProjectIamCustomRole_basic(roleId string) string { + return fmt.Sprintf(` +resource "google_project_iam_custom_role" "foo" { + role_id = "%s" + title = "My Custom Role" + description = "foo" + permissions = ["iam.roles.list"] +} +`, roleId) +} + +func testAccCheckGoogleProjectIamCustomRole_deleted(roleId string) string { + return fmt.Sprintf(` +resource "google_project_iam_custom_role" "foo" { + role_id = "%s" + title = "My Custom Role" + description = "foo" + permissions = ["iam.roles.list"] + deleted = true +} +`, roleId) +} + +func testAccCheckGoogleProjectIamCustomRole_update(roleId string) string { + return fmt.Sprintf(` +resource "google_project_iam_custom_role" "foo" { + role_id = "%s" + title = "My Custom Role Updated" + description = "bar" + permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"] + stage = "BETA" +} +`, roleId) +} diff --git a/provider/terraform/tests/resource_google_project_iam_member_test.go b/provider/terraform/tests/resource_google_project_iam_member_test.go new file mode 100644 index 000000000000..df280216ad25 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_iam_member_test.go @@ -0,0 +1,169 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func projectIamMemberImportStep(resourceName, pid, role, member string) resource.TestStep { + return resource.TestStep{ + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s %s %s", pid, role, member), + ImportState: true, + ImportStateVerify: true, + } +} + +// Test that an IAM binding can be applied to a project +func TestAccProjectIamMember_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resourceName := "google_project_iam_member.acceptance" + role := "roles/compute.instanceAdmin" + member := "user:admin@hashicorptest.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateMemberBasic(pid, pname, org, role, member), + }, + projectIamMemberImportStep(resourceName, pid, role, member), + }, + }) +} + +// Test that multiple IAM bindings can be applied to a project +func TestAccProjectIamMember_multiple(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + skipIfEnvNotSet(t, "GOOGLE_ORG") + + pid := "terraform-" + acctest.RandString(10) + resourceName := "google_project_iam_member.acceptance" + resourceName2 := "google_project_iam_member.multiple" + role := "roles/compute.instanceAdmin" + member := "user:admin@hashicorptest.com" + member2 := "user:paddy@hashicorp.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM binding + { + Config: testAccProjectAssociateMemberBasic(pid, pname, org, role, member), + }, + projectIamMemberImportStep(resourceName, pid, role, member), + + // Apply another IAM binding + { + Config: testAccProjectAssociateMemberMultiple(pid, pname, org, role, member, role, member2), + }, + projectIamMemberImportStep(resourceName, pid, role, member), + projectIamMemberImportStep(resourceName2, pid, role, member2), + }, + }) +} + +// Test that an IAM binding can be removed from a project +func TestAccProjectIamMember_remove(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + skipIfEnvNotSet(t, "GOOGLE_ORG") + + pid := "terraform-" + acctest.RandString(10) + resourceName := "google_project_iam_member.acceptance" + role := "roles/compute.instanceAdmin" + member := "user:admin@hashicorptest.com" + member2 := "user:paddy@hashicorp.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + + // Apply multiple IAM bindings + { + Config: testAccProjectAssociateMemberMultiple(pid, pname, org, role, member, role, member2), + }, + projectIamMemberImportStep(resourceName, pid, role, member), + projectIamMemberImportStep(resourceName, pid, role, member2), + + // Remove the bindings + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + }, + }) +} + +func testAccProjectAssociateMemberBasic(pid, name, org, role, member string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_member" "acceptance" { + project = "${google_project.acceptance.project_id}" + role = "%s" + member = "%s" +} +`, pid, name, org, role, member) +} + +func testAccProjectAssociateMemberMultiple(pid, name, org, role, member, role2, member2 string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_member" "acceptance" { + project = "${google_project.acceptance.project_id}" + role = "%s" + member = "%s" +} + +resource "google_project_iam_member" "multiple" { + project = "${google_project.acceptance.project_id}" + role = "%s" + member = "%s" +} +`, pid, name, org, role, member, role2, member2) +} diff --git a/provider/terraform/tests/resource_google_project_iam_policy_test.go b/provider/terraform/tests/resource_google_project_iam_policy_test.go new file mode 100644 index 000000000000..a09460eda0ab --- /dev/null +++ b/provider/terraform/tests/resource_google_project_iam_policy_test.go @@ -0,0 +1,780 @@ +package google + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func TestSubtractIamPolicy(t *testing.T) { + table := []struct { + a *cloudresourcemanager.Policy + b *cloudresourcemanager.Policy + expect cloudresourcemanager.Policy + }{ + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "3", + "4", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{}, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "2", + }, + }, + }, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{}, + }, + }, + } + + for _, test := range table { + c := subtractIamPolicy(test.a, test.b) + sort.Sort(sortableBindings(c.Bindings)) + for i, _ := range c.Bindings { + sort.Strings(c.Bindings[i].Members) + } + + if !reflect.DeepEqual(derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) { + t.Errorf("\ngot %+v\nexpected %+v", derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) + } + } +} + +// Test that an IAM policy can be applied to a project +func TestAccProjectIamPolicy_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM policy from a data source. The application + // merges policies, so we validate the expected state. + resource.TestStep{ + Config: testAccProjectAssociatePolicyBasic(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectIamPolicyIsMerged("google_project_iam_policy.acceptance", "data.google_iam_policy.admin", pid), + ), + }, + resource.TestStep{ + ResourceName: "google_project_iam_policy.acceptance", + ImportState: true, + // Skipping the normal "ImportStateVerify" - Unfortunately, it's not + // really possible to make the imported policy match exactly, since + // the policy depends on the service account being used to create the + // project. + }, + // Finally, remove the custom IAM policy from config and apply, then + // confirm that the project is in its original state. + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + }, + }) +} + +// Test that an IAM policy can be applied to a project when no project is set in the resource +func TestAccProjectIamPolicy_defaultProject(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + resource.TestStep{ + Config: testAccProjectDefaultAssociatePolicyBasic(), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(getTestProjectFromEnv()), + ), + }, + // Apply an IAM policy from a data source. The application + // merges policies, so we validate the expected state. + resource.TestStep{ + Config: testAccProjectDefaultAssociatePolicyBasic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectIamPolicyIsMerged("google_project_iam_policy.acceptance", "data.google_iam_policy.admin", getTestProjectFromEnv()), + ), + }, + }, + }) +} + +// Test that a non-collapsed IAM policy doesn't perpetually diff +func TestAccProjectIamPolicy_expanded(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProjectAssociatePolicyExpanded(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectIamPolicyExists("google_project_iam_policy.acceptance", "data.google_iam_policy.expanded", pid), + ), + }, + }, + }) +} + +func getStatePrimaryResource(s *terraform.State, res, expectedID string) (*terraform.InstanceState, error) { + // Get the project resource + resource, ok := s.RootModule().Resources[res] + if !ok { + return nil, fmt.Errorf("Not found: %s", res) + } + if resource.Primary.Attributes["id"] != expectedID && expectedID != "" { + return nil, fmt.Errorf("Expected project %q to match ID %q in state", resource.Primary.ID, expectedID) + } + return resource.Primary, nil +} + +func getGoogleProjectIamPolicyFromResource(resource *terraform.InstanceState) (cloudresourcemanager.Policy, error) { + var p cloudresourcemanager.Policy + ps, ok := resource.Attributes["policy_data"] + if !ok { + return p, fmt.Errorf("Resource %q did not have a 'policy_data' attribute. Attributes were %#v", resource.ID, resource.Attributes) + } + if err := json.Unmarshal([]byte(ps), &p); err != nil { + return p, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err) + } + return p, nil +} + +func getGoogleProjectIamPolicyFromState(s *terraform.State, res, expectedID string) (cloudresourcemanager.Policy, error) { + project, err := getStatePrimaryResource(s, res, expectedID) + if err != nil { + return cloudresourcemanager.Policy{}, err + } + return getGoogleProjectIamPolicyFromResource(project) +} + +func compareBindings(a, b []*cloudresourcemanager.Binding) bool { + a = mergeBindings(a) + b = mergeBindings(b) + sort.Sort(sortableBindings(a)) + sort.Sort(sortableBindings(b)) + return reflect.DeepEqual(derefBindings(a), derefBindings(b)) +} + +func testAccCheckGoogleProjectIamPolicyExists(projectRes, policyRes, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + projectPolicy, err := getGoogleProjectIamPolicyFromState(s, projectRes, pid) + if err != nil { + return fmt.Errorf("Error retrieving IAM policy for project from state: %s", err) + } + policyPolicy, err := getGoogleProjectIamPolicyFromState(s, policyRes, "") + if err != nil { + return fmt.Errorf("Error retrieving IAM policy for data_policy from state: %s", err) + } + + // The bindings in both policies should be identical + if !compareBindings(projectPolicy.Bindings, policyPolicy.Bindings) { + return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectPolicy.Bindings), derefBindings(policyPolicy.Bindings)) + } + return nil + } +} + +func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + err := testAccCheckGoogleProjectIamPolicyExists(projectRes, policyRes, pid)(s) + if err != nil { + return err + } + + projectPolicy, err := getGoogleProjectIamPolicyFromState(s, projectRes, pid) + if err != nil { + return fmt.Errorf("Error retrieving IAM policy for project from state: %s", err) + } + + // Merge the project policy in Terraform state with the policy the project had before the config was applied + var expected []*cloudresourcemanager.Binding + expected = append(expected, originalPolicy.Bindings...) + expected = append(expected, projectPolicy.Bindings...) + expected = mergeBindings(expected) + + // Retrieve the actual policy from the project + c := testAccProvider.Meta().(*Config) + actual, err := getProjectIamPolicy(pid, c) + if err != nil { + return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err) + } + // The bindings should match, indicating the policy was successfully applied and merged + if !compareBindings(actual.Bindings, expected) { + return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actual.Bindings), derefBindings(expected)) + } + + return nil + } +} + +func TestIamRolesToMembersBinding(t *testing.T) { + table := []struct { + expect []*cloudresourcemanager.Binding + input map[string]map[string]bool + }{ + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{}, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{}, + }, + }, + } + + for _, test := range table { + got := rolesToMembersBinding(test.input) + + sort.Sort(sortableBindings(got)) + for i, _ := range got { + sort.Strings(got[i].Members) + } + + if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) { + t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect)) + } + } +} +func TestIamRolesToMembersMap(t *testing.T) { + table := []struct { + input []*cloudresourcemanager.Binding + expect map[string]map[string]bool + }{ + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-1", + "member-2", + }, + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{}, + }, + }, + } + + for _, test := range table { + got := rolesToMembersMap(test.input) + if !reflect.DeepEqual(got, test.expect) { + t.Errorf("got %+v, expected %+v", got, test.expect) + } + } +} + +func TestIamMergeBindings(t *testing.T) { + table := []struct { + input []*cloudresourcemanager.Binding + expect []cloudresourcemanager.Binding + }{ + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-3", + }, + }, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-3", + }, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-3", + "member-4", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-2", + "member-1", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-1", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-5", + }, + }, + { + Role: "role-3", + Members: []string{ + "member-1", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-2", + }, + }, + {Role: "empty-role", Members: []string{}}, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-3", + "member-4", + "member-5", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-1", + "member-2", + }, + }, + { + Role: "role-3", + Members: []string{ + "member-1", + }, + }, + }, + }, + } + + for _, test := range table { + got := mergeBindings(test.input) + sort.Sort(sortableBindings(got)) + for i, _ := range got { + sort.Strings(got[i].Members) + } + + if !reflect.DeepEqual(derefBindings(got), test.expect) { + t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect) + } + } +} + +func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding { + db := make([]cloudresourcemanager.Binding, len(b)) + + for i, v := range b { + db[i] = *v + sort.Strings(db[i].Members) + } + return db +} + +// Confirm that a project has an IAM policy with at least 1 binding +func testAccProjectExistingPolicy(pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + c := testAccProvider.Meta().(*Config) + var err error + originalPolicy, err = getProjectIamPolicy(pid, c) + if err != nil { + return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err) + } + if len(originalPolicy.Bindings) == 0 { + return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.") + } + return nil + } +} + +func testAccProjectDefaultAssociatePolicyBasic() string { + return fmt.Sprintf(` +resource "google_project_iam_policy" "acceptance" { + policy_data = "${data.google_iam_policy.admin.policy_data}" +} +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + } +} +`) +} + +func testAccProjectAssociatePolicyBasic(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_iam_policy" "acceptance" { + project = "${google_project.acceptance.id}" + policy_data = "${data.google_iam_policy.admin.policy_data}" +} +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + } +} +`, pid, name, org) +} + +func testAccProject_createWithoutOrg(pid, name string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" +}`, pid, name) +} + +func testAccProject_create(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +}`, pid, name, org) +} + +func testAccProject_createBilling(pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +}`, pid, name, org, billing) +} + +func testAccProjectAssociatePolicyExpanded(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_iam_policy" "acceptance" { + project = "${google_project.acceptance.id}" + policy_data = "${data.google_iam_policy.expanded.policy_data}" + authoritative = false +} +data "google_iam_policy" "expanded" { + binding { + role = "roles/viewer" + members = [ + "user:paddy@carvers.co", + ] + } + + binding { + role = "roles/viewer" + members = [ + "user:paddy@hashicorp.com", + ] + } +}`, pid, name, org) +} diff --git a/provider/terraform/tests/resource_google_project_migrate_test.go b/provider/terraform/tests/resource_google_project_migrate_test.go new file mode 100644 index 000000000000..8aeff36404f0 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_migrate_test.go @@ -0,0 +1,70 @@ +package google + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestGoogleProjectMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + Attributes map[string]string + Expected map[string]string + Meta interface{} + }{ + "deprecate policy_data and support creation/deletion": { + StateVersion: 0, + Attributes: map[string]string{}, + Expected: map[string]string{ + "project_id": "test-project", + "skip_delete": "true", + }, + Meta: &Config{}, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: "test-project", + Attributes: tc.Attributes, + } + is, err := resourceGoogleProjectMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.Expected { + if is.Attributes[k] != v { + t.Fatalf( + "bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v", + tn, k, v, k, is.Attributes[k], is.Attributes) + } + } + } +} + +func TestGoogleProjectMigrateState_empty(t *testing.T) { + var is *terraform.InstanceState + var meta *Config + + // should handle nil + is, err := resourceGoogleProjectMigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } + if is != nil { + t.Fatalf("expected nil instancestate, got: %#v", is) + } + + // should handle non-nil but empty + is = &terraform.InstanceState{} + is, err = resourceGoogleProjectMigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } +} diff --git a/provider/terraform/tests/resource_google_project_organization_policy_test.go b/provider/terraform/tests/resource_google_project_organization_policy_test.go new file mode 100644 index 000000000000..12067285c244 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_organization_policy_test.go @@ -0,0 +1,354 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +/* +Tests for `google_project_organization_policy` + +These are *not* run in parallel, as they all use the same project +and I end up with 409 Conflict errors from the API when they are +run in parallel. +*/ + +func TestAccProjectOrganizationPolicy_boolean(t *testing.T) { + projectId := getTestProjectFromEnv() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + // Test creation of an enforced boolean policy + Config: testAccProjectOrganizationPolicy_boolean(projectId, true), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", true), + }, + { + // Test update from enforced to not + Config: testAccProjectOrganizationPolicy_boolean(projectId, false), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", false), + }, + { + Config: " ", + Destroy: true, + }, + { + // Test creation of a not enforced boolean policy + Config: testAccProjectOrganizationPolicy_boolean(projectId, false), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", false), + }, + { + // Test update from not enforced to enforced + Config: testAccProjectOrganizationPolicy_boolean(projectId, true), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", true), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_allowAll(t *testing.T) { + projectId := getTestProjectFromEnv() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowAll(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyAll("list", "ALLOW"), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_allowSome(t *testing.T) { + project := getTestProjectFromEnv() + canonicalProject := canonicalProjectId(project) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowSome(project), + Check: testAccCheckGoogleProjectOrganizationListPolicyAllowedValues("list", []string{canonicalProject}), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_denySome(t *testing.T) { + projectId := getTestProjectFromEnv() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_denySome(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_update(t *testing.T) { + projectId := getTestProjectFromEnv() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowAll(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyAll("list", "ALLOW"), + }, + { + Config: testAccProjectOrganizationPolicy_list_denySome(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_restore_defaultTrue(t *testing.T) { + projectId := getTestProjectFromEnv() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_restore_defaultTrue(projectId), + Check: getGoogleProjectOrganizationRestoreDefaultTrue("restore", &cloudresourcemanager.RestoreDefault{}), + }, + }, + }) +} + +func testAccCheckGoogleProjectOrganizationPolicyDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_project_organization_policy" { + continue + } + + projectId := canonicalProjectId(rs.Primary.Attributes["project"]) + constraint := canonicalOrgPolicyConstraint(rs.Primary.Attributes["constraint"]) + policy, err := config.clientResourceManager.Projects.GetOrgPolicy(projectId, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: constraint, + }).Do() + + if err != nil { + return err + } + + if policy.ListPolicy != nil || policy.BooleanPolicy != nil { + return fmt.Errorf("Org policy with constraint '%s' hasn't been cleared", constraint) + } + } + return nil +} + +func testAccCheckGoogleProjectOrganizationBooleanPolicy(n string, enforced bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if policy.BooleanPolicy.Enforced != enforced { + return fmt.Errorf("Expected boolean policy enforcement to be '%t', got '%t'", enforced, policy.BooleanPolicy.Enforced) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyAll(n, policyType string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if policy.ListPolicy == nil { + return nil + } + + if len(policy.ListPolicy.AllowedValues) > 0 || len(policy.ListPolicy.DeniedValues) > 0 { + return fmt.Errorf("The `values` field shouldn't be set") + } + + if policy.ListPolicy.AllValues != policyType { + return fmt.Errorf("The list policy should %s all values", policyType) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyAllowedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.AllowedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.AllowedValues, values) { + return fmt.Errorf("Expected the list policy to allow '%s', instead allowed '%s'", values, policy.ListPolicy.AllowedValues) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyDeniedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.DeniedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.DeniedValues, values) { + return fmt.Errorf("Expected the list policy to deny '%s', instead denied '%s'", values, policy.ListPolicy.DeniedValues) + } + + return nil + } +} + +func getGoogleProjectOrganizationRestoreDefaultTrue(n string, policyDefault *cloudresourcemanager.RestoreDefault) resource.TestCheckFunc { + return func(s *terraform.State) error { + + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if !reflect.DeepEqual(policy.RestoreDefault, policyDefault) { + return fmt.Errorf("Expected the restore default '%s', instead denied, %s", policyDefault, policy.RestoreDefault) + } + + return nil + } +} + +func getGoogleProjectOrganizationPolicyTestResource(s *terraform.State, n string) (*cloudresourcemanager.OrgPolicy, error) { + rn := "google_project_organization_policy." + n + rs, ok := s.RootModule().Resources[rn] + if !ok { + return nil, fmt.Errorf("Not found: %s", rn) + } + + if rs.Primary.ID == "" { + return nil, fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + projectId := canonicalProjectId(rs.Primary.Attributes["project"]) + + return config.clientResourceManager.Projects.GetOrgPolicy(projectId, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: rs.Primary.Attributes["constraint"], + }).Do() +} + +func testAccProjectOrganizationPolicy_boolean(pid string, enforced bool) string { + return fmt.Sprintf(` +resource "google_project_organization_policy" "bool" { + project = "%s" + constraint = "constraints/compute.disableSerialPortAccess" + + boolean_policy { + enforced = %t + } +} +`, pid, enforced) +} + +func testAccProjectOrganizationPolicy_list_allowAll(pid string) string { + return fmt.Sprintf(` +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/serviceuser.services" + + list_policy { + allow { + all = true + } + } +} +`, pid) +} + +func testAccProjectOrganizationPolicy_list_allowSome(pid string) string { + return fmt.Sprintf(` + +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/compute.trustedImageProjects" + + list_policy { + allow { + values = ["projects/%s"] + } + } +} +`, pid, pid) +} + +func testAccProjectOrganizationPolicy_list_denySome(pid string) string { + return fmt.Sprintf(` + +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/serviceuser.services" + + list_policy { + deny { + values = [ + "doubleclicksearch.googleapis.com", + "replicapoolupdater.googleapis.com", + ] + } + } +} +`, pid) +} + +func testAccProjectOrganizationPolicy_restore_defaultTrue(pid string) string { + return fmt.Sprintf(` +resource "google_project_organization_policy" "restore" { + project = "%s" + constraint = "constraints/serviceuser.services" + + restore_policy { + default = true + } +} +`, pid) +} + +func canonicalProjectId(project string) string { + if strings.HasPrefix(project, "projects/") { + return project + } + return fmt.Sprintf("projects/%s", project) +} diff --git a/provider/terraform/tests/resource_google_project_service_test.go b/provider/terraform/tests/resource_google_project_service_test.go new file mode 100644 index 000000000000..8f4b0e08bf42 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_service_test.go @@ -0,0 +1,189 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// Test that services can be enabled and disabled on a project +func TestAccProjectService_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + services := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"} + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProjectService_basic(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectService(services, pid, true), + ), + }, + resource.TestStep{ + ResourceName: "google_project_service.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"disable_on_destroy"}, + }, + resource.TestStep{ + ResourceName: "google_project_service.test2", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"disable_on_destroy"}, + }, + // Use a separate TestStep rather than a CheckDestroy because we need the project to still exist. + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectService(services, pid, false), + ), + }, + // Create services with disabling turned off. + resource.TestStep{ + Config: testAccProjectService_noDisable(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectService(services, pid, true), + ), + }, + // Check that services are still enabled even after the resources are deleted. + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectService(services, pid, true), + ), + }, + }, + }) +} + +func TestAccProjectService_handleNotFound(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + service := "iam.googleapis.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProjectService_handleNotFound(service, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectService([]string{service}, pid, true), + ), + }, + // Delete the project, implicitly deletes service, expect the plan to want to create the service again + resource.TestStep{ + Config: testAccProjectService_handleNotFoundNoProject(service, pid), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckProjectService(services []string, pid string, expectEnabled bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + apiServices, err := getApiServices(pid, config, map[string]struct{}{}) + if err != nil { + return fmt.Errorf("Error listing services for project %q: %v", pid, err) + } + + for _, expected := range services { + exists := false + for _, actual := range apiServices { + if expected == actual { + exists = true + } + } + if expectEnabled && !exists { + return fmt.Errorf("Expected service %s is not enabled server-side (found %v)", expected, apiServices) + } + if !expectEnabled && exists { + return fmt.Errorf("Expected disabled service %s is enabled server-side", expected) + } + } + + return nil + } +} + +func testAccProjectService_basic(services []string, pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_service" "test" { + project = "${google_project.acceptance.project_id}" + service = "%s" +} + +resource "google_project_service" "test2" { + project = "${google_project.acceptance.project_id}" + service = "%s" +} +`, pid, name, org, services[0], services[1]) +} + +func testAccProjectService_noDisable(services []string, pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_service" "test" { + project = "${google_project.acceptance.project_id}" + service = "%s" + disable_on_destroy = false +} + +resource "google_project_service" "test2" { + project = "${google_project.acceptance.project_id}" + service = "%s" + disable_on_destroy = false +} +`, pid, name, org, services[0], services[1]) +} + +func testAccProjectService_handleNotFound(service, pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +// by passing through locals, we break the dependency chain +// see terraform-provider-google#1292 +locals { + project_id = "${google_project.acceptance.project_id}" +} + +resource "google_project_service" "test" { + project = "${local.project_id}" + service = "%s" +} +`, pid, name, org, service) +} + +func testAccProjectService_handleNotFoundNoProject(service, pid string) string { + return fmt.Sprintf(` +resource "google_project_service" "test" { + project = "%s" + service = "%s" +} +`, pid, service) +} diff --git a/provider/terraform/tests/resource_google_project_services_test.go b/provider/terraform/tests/resource_google_project_services_test.go new file mode 100644 index 000000000000..80983106d03d --- /dev/null +++ b/provider/terraform/tests/resource_google_project_services_test.go @@ -0,0 +1,322 @@ +package google + +import ( + "bytes" + "fmt" + "log" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// Test that services can be enabled and disabled on a project +func TestAccProjectServices_basic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + services1 := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"} + services2 := []string{"cloudresourcemanager.googleapis.com"} + oobService := "iam.googleapis.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with some services + resource.TestStep{ + Config: testAccProjectAssociateServicesBasic(services1, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services1, pid), + ), + }, + // Update services to remove one + resource.TestStep{ + Config: testAccProjectAssociateServicesBasic(services2, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services2, pid), + ), + }, + // Add a service out-of-band and ensure it is removed + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + enableService(oobService, pid, config) + }, + Config: testAccProjectAssociateServicesBasic(services2, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services2, pid), + ), + }, + resource.TestStep{ + ResourceName: "google_project_services.acceptance", + ImportState: true, + ImportStateId: pid, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"disable_on_destroy"}, + }, + }, + }) +} + +// Test that services are authoritative when a project has existing +// sevices not represented in config +func TestAccProjectServices_authoritative(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + services := []string{"cloudresourcemanager.googleapis.com"} + oobService := "iam.googleapis.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with no services + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Add a service out-of-band, then apply a config that creates a service. + // It should remove the out-of-band service. + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + enableService(oobService, pid, config) + }, + Config: testAccProjectAssociateServicesBasic(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +// Test that services are authoritative when a project has existing +// sevices, some which are represented in the config and others +// that are not +func TestAccProjectServices_authoritative2(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + oobServices := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"} + services := []string{"iam.googleapis.com"} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with no services + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Add a service out-of-band, then apply a config that creates a service. + // It should remove the out-of-band service. + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + for _, s := range oobServices { + enableService(s, pid, config) + } + }, + Config: testAccProjectAssociateServicesBasic(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +// Test that services that can't be enabled on their own (such as dataproc-control.googleapis.com) +// don't end up causing diffs when they are enabled as a side-effect of a different service's +// enablement. +func TestAccProjectServices_ignoreUnenablableServices(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + billingId := getTestBillingAccountFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + services := []string{ + "dataproc.googleapis.com", + // The following services are enabled as a side-effect of dataproc's enablement + "storage-component.googleapis.com", + "deploymentmanager.googleapis.com", + "replicapool.googleapis.com", + "replicapoolupdater.googleapis.com", + "resourceviews.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "containerregistry.googleapis.com", + "storage-api.googleapis.com", + "pubsub.googleapis.com", + "oslogin.googleapis.com", + "bigquery-json.googleapis.com", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProjectAssociateServicesBasic_withBilling(services, pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +func TestAccProjectServices_pagination(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + billingId := getTestBillingAccountFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + + // we need at least 50 services (doesn't matter what they are) to exercise the + // pagination handling code. + services := []string{ + "actions.googleapis.com", + "appengine.googleapis.com", + "appengineflex.googleapis.com", + "bigquery-json.googleapis.com", + "bigquerydatatransfer.googleapis.com", + "bigtableadmin.googleapis.com", + "bigtabletableadmin.googleapis.com", + "cloudbuild.googleapis.com", + "clouderrorreporting.googleapis.com", + "cloudfunctions.googleapis.com", + "cloudiot.googleapis.com", + "cloudkms.googleapis.com", + "cloudmonitoring.googleapis.com", + "cloudresourcemanager.googleapis.com", + "cloudtrace.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "containerregistry.googleapis.com", + "dataflow.googleapis.com", + "dataproc.googleapis.com", + "datastore.googleapis.com", + "deploymentmanager.googleapis.com", + "dialogflow.googleapis.com", + "dns.googleapis.com", + "endpoints.googleapis.com", + "firebaserules.googleapis.com", + "firestore.googleapis.com", + "genomics.googleapis.com", + "iam.googleapis.com", + "language.googleapis.com", + "logging.googleapis.com", + "ml.googleapis.com", + "monitoring.googleapis.com", + "oslogin.googleapis.com", + "pubsub.googleapis.com", + "replicapool.googleapis.com", + "replicapoolupdater.googleapis.com", + "resourceviews.googleapis.com", + "runtimeconfig.googleapis.com", + "servicecontrol.googleapis.com", + "servicemanagement.googleapis.com", + "sourcerepo.googleapis.com", + "spanner.googleapis.com", + "speech.googleapis.com", + "sql-component.googleapis.com", + "storage-api.googleapis.com", + "storage-component.googleapis.com", + "storagetransfer.googleapis.com", + "testing.googleapis.com", + "toolresults.googleapis.com", + "translate.googleapis.com", + "videointelligence.googleapis.com", + "vision.googleapis.com", + "zync.googleapis.com", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProjectAssociateServicesBasic_withBilling(services, pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +func testAccProjectAssociateServicesBasic(services []string, pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_services" "acceptance" { + project = "${google_project.acceptance.project_id}" + services = [%s] + disable_on_destroy = true +} +`, pid, name, org, testStringsToString(services)) +} + +func testAccProjectAssociateServicesBasic_withBilling(services []string, pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} +resource "google_project_services" "acceptance" { + project = "${google_project.acceptance.project_id}" + services = [%s] + disable_on_destroy = false +} +`, pid, name, org, billing, testStringsToString(services)) +} + +func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + apiServices, err := getApiServices(pid, config, ignoreProjectServices) + if err != nil { + return fmt.Errorf("Error listing services for project %q: %v", pid, err) + } + + sort.Strings(services) + sort.Strings(apiServices) + if !reflect.DeepEqual(services, apiServices) { + return fmt.Errorf("Services in config (%v) do not exactly match services returned by API (%v)", services, apiServices) + } + + return nil + } +} + +func testStringsToString(s []string) string { + var b bytes.Buffer + for i, v := range s { + b.WriteString(fmt.Sprintf("\"%s\"", v)) + if i < len(s)-1 { + b.WriteString(",") + } + } + r := b.String() + log.Printf("[DEBUG]: Converted list of strings to %s", r) + return b.String() +} diff --git a/provider/terraform/tests/resource_google_project_test.go b/provider/terraform/tests/resource_google_project_test.go new file mode 100644 index 000000000000..dad6333d8d42 --- /dev/null +++ b/provider/terraform/tests/resource_google_project_test.go @@ -0,0 +1,576 @@ +package google + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +var ( + pname = "Terraform Acceptance Tests" + originalPolicy *cloudresourcemanager.Policy +) + +// Test that a Project resource can be created without an organization +func TestAccProject_createWithoutOrg(t *testing.T) { + t.Parallel() + + creds := multiEnvSearch(credsEnvVars) + if strings.Contains(creds, "iam.gserviceaccount.com") { + t.Skip("Service accounts cannot create projects without a parent. Requires user credentials.") + } + + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project + resource.TestStep{ + Config: testAccProject_createWithoutOrg(pid, pname), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + }, + }) +} + +// Test that a Project resource can be created and an IAM policy +// associated +func TestAccProject_create(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + }, + }) +} + +// Test that a Project resource can be created with an associated +// billing account +func TestAccProject_billing(t *testing.T) { + t.Parallel() + org := getTestOrgFromEnv(t) + skipIfEnvNotSet(t, "GOOGLE_BILLING_ACCOUNT_2") + billingId2 := os.Getenv("GOOGLE_BILLING_ACCOUNT_2") + billingId := getTestBillingAccountFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project with a billing account + resource.TestStep{ + Config: testAccProject_createBilling(pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId), + ), + }, + // Make sure import supports billing account + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + // Update to a different billing account + resource.TestStep{ + Config: testAccProject_createBilling(pid, pname, org, billingId2), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId2), + ), + }, + // Unlink the billing account + resource.TestStep{ + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, ""), + ), + }, + }, + }) +} + +// Test that a Project resource can be created with labels +func TestAccProject_labels(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_labels(pid, pname, org, map[string]string{"test": "that"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasLabels("google_project.acceptance", pid, map[string]string{"test": "that"}), + ), + }, + // Make sure import supports labels + { + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + // update project with labels + { + Config: testAccProject_labels(pid, pname, org, map[string]string{"label": "label-value"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasLabels("google_project.acceptance", pid, map[string]string{"label": "label-value"}), + ), + }, + // update project delete labels + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasNoLabels("google_project.acceptance", pid), + ), + }, + }, + }) +} + +func TestAccProject_deleteDefaultNetwork(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + billingId := getTestBillingAccountFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_deleteDefaultNetwork(pid, pname, org, billingId), + }, + }, + }) +} + +func TestAccProject_parentFolder(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "terraform-" + acctest.RandString(10) + folderDisplayName := "tf-test-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_parentFolder(pid, pname, folderDisplayName, org), + }, + }, + }) +} + +func TestAccProject_appEngineBasic(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := acctest.RandomWithPrefix("tf-test") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_appEngineBasic(pid, org), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.name"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.url_dispatch_rule.#"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.code_bucket"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_hostname"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_bucket"), + ), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProject_appEngineBasicWithBilling(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := acctest.RandomWithPrefix("tf-test") + billingId := getTestBillingAccountFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_appEngineBasicWithBilling(pid, org, billingId), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.name"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.url_dispatch_rule.#"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.code_bucket"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_hostname"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_bucket"), + ), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProject_appEngineUpdate(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := acctest.RandomWithPrefix("tf-test") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_appEngineNoApp(pid, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + { + Config: testAccProject_appEngineBasic(pid, org), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.name"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.url_dispatch_rule.#"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.code_bucket"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_hostname"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_bucket"), + ), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProject_appEngineUpdate(pid, org), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProject_appEngineFeatureSettings(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := acctest.RandomWithPrefix("tf-test") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProject_appEngineFeatureSettings(pid, org), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProject_appEngineFeatureSettingsUpdate(pid, org), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + if rs.Primary.ID != pid { + return fmt.Errorf("Expected project %q to match ID %q in state", pid, rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckGoogleProjectHasBillingAccount(r, pid, billingId string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should match expected + if rs.Primary.Attributes["billing_account"] != billingId { + return fmt.Errorf("Billing ID in state (%s) does not match expected value (%s)", rs.Primary.Attributes["billing_account"], billingId) + } + + // Actual value in API should match state and expected + // Read the billing account + config := testAccProvider.Meta().(*Config) + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() + if err != nil { + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) + } + if billingId != strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") { + return fmt.Errorf("Billing ID returned by API (%s) did not match expected value (%s)", ba.BillingAccountName, billingId) + } + return nil + } +} + +func testAccCheckGoogleProjectHasLabels(r, pid string, expected map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should have the same number of labels + if rs.Primary.Attributes["labels.%"] != strconv.Itoa(len(expected)) { + return fmt.Errorf("Expected %d labels, got %s", len(expected), rs.Primary.Attributes["labels.%"]) + } + + // Actual value in API should match state and expected + config := testAccProvider.Meta().(*Config) + + found, err := config.clientResourceManager.Projects.Get(pid).Do() + if err != nil { + return err + } + + actual := found.Labels + if !reflect.DeepEqual(actual, expected) { + // Determine only the different attributes + for k, v := range expected { + if av, ok := actual[k]; ok && v == av { + delete(expected, k) + delete(actual, k) + } + } + + spewConf := spew.NewDefaultConfig() + spewConf.SortKeys = true + return fmt.Errorf( + "Labels not equivalent. Difference is shown below. Top is actual, bottom is expected."+ + "\n\n%s\n\n%s", + spewConf.Sdump(actual), spewConf.Sdump(expected), + ) + } + return nil + } +} + +func testAccCheckGoogleProjectHasNoLabels(r, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should have zero labels + if rs.Primary.Attributes["labels.%"] != "0" { + return fmt.Errorf("Expected 0 labels, got %s", rs.Primary.Attributes["labels.%"]) + } + + // Actual value in API should match state and expected + config := testAccProvider.Meta().(*Config) + + found, err := config.clientResourceManager.Projects.Get(pid).Do() + if err != nil { + return err + } + + spewConf := spew.NewDefaultConfig() + spewConf.SortKeys = true + if found.Labels != nil { + return fmt.Errorf("Labels should be empty. Actual \n%s", spewConf.Sdump(found.Labels)) + } + return nil + } +} + +func testAccProject_labels(pid, name, org string, labels map[string]string) string { + r := fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + labels {`, pid, name, org) + + l := "" + for key, value := range labels { + l += fmt.Sprintf("%q = %q\n", key, value) + } + + l += fmt.Sprintf("}\n}") + return r + l +} + +func testAccProject_deleteDefaultNetwork(pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" # requires billing to enable compute API + auto_create_network = false +}`, pid, name, org, billing) +} + +func testAccProject_parentFolder(pid, projectName, folderName, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + # ensures we can set both org_id and folder_id as long as only one is not empty. + org_id = "" + folder_id = "${google_folder.folder1.id}" +} + +resource "google_folder" "folder1" { + display_name = "%s" + parent = "organizations/%s" +} + +`, pid, projectName, folderName, org) +} + +func testAccProject_appEngineNoApp(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +}`, pid, pid, org) +} + +func testAccProject_appEngineBasic(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + auth_domain = "hashicorptest.com" + location_id = "us-central" + serving_status = "SERVING" + } +}`, pid, pid, org) +} + +func testAccProject_appEngineBasicWithBilling(pid, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + billing_account = "%s" + + app_engine { + auth_domain = "hashicorptest.com" + location_id = "us-central" + serving_status = "SERVING" + } +}`, pid, pid, org, billing) +} + +func testAccProject_appEngineUpdate(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + auth_domain = "tf-test.club" + location_id = "us-central" + serving_status = "USER_DISABLED" + } +}`, pid, pid, org) +} + +func testAccProject_appEngineFeatureSettings(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + location_id = "us-central" + + feature_settings { + "split_health_checks" = true + } + } +}`, pid, pid, org) +} + +func testAccProject_appEngineFeatureSettingsUpdate(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + location_id = "us-central" + + feature_settings { + "split_health_checks" = false + } + } +}`, pid, pid, org) +} + +func skipIfEnvNotSet(t *testing.T, envs ...string) { + for _, k := range envs { + if os.Getenv(k) == "" { + t.Skipf("Environment variable %s is not set", k) + } + } +} diff --git a/provider/terraform/tests/resource_google_service_account_iam_test.go b/provider/terraform/tests/resource_google_service_account_iam_test.go new file mode 100644 index 000000000000..1e183d870059 --- /dev/null +++ b/provider/terraform/tests/resource_google_service_account_iam_test.go @@ -0,0 +1,167 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccServiceAccountIamBinding(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamBinding_basic(account), + Check: testAccCheckGoogleServiceAccountIam(account, "roles/viewer", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_service_account_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s %s", getServiceAccountCanonicalId(account), "roles/viewer"), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccServiceAccountIamMember(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + identity := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamMember_basic(account), + Check: testAccCheckGoogleServiceAccountIam(account, "roles/editor", []string{identity}), + }, + { + ResourceName: "google_service_account_iam_member.foo", + ImportStateId: fmt.Sprintf("%s %s %s", getServiceAccountCanonicalId(account), "roles/editor", identity), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccServiceAccountIamPolicy(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceAccountIamPolicy_basic(account), + Check: testAccCheckGoogleServiceAccountIam(account, "roles/owner", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_service_account_iam_policy.foo", + ImportStateId: getServiceAccountCanonicalId(account), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGoogleServiceAccountIam(account, role string, members []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(getServiceAccountCanonicalId(account)).Do() + if err != nil { + return err + } + + for _, binding := range p.Bindings { + if binding.Role == role { + sort.Strings(members) + sort.Strings(binding.Members) + + if reflect.DeepEqual(members, binding.Members) { + return nil + } + + return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members) + } + } + + return fmt.Errorf("No binding for role %q", role) + } +} + +func getServiceAccountCanonicalId(account string) string { + return fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", getTestProjectFromEnv(), account, getTestProjectFromEnv()) +} + +func testAccServiceAccountIamBinding_basic(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_service_account_iam_binding" "foo" { + service_account_id = "${google_service_account.test_account.id}" + role = "roles/viewer" + members = ["serviceAccount:${google_service_account.test_account.email}"] +} +`, account) +} + +func testAccServiceAccountIamMember_basic(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_service_account_iam_member" "foo" { + service_account_id = "${google_service_account.test_account.id}" + role = "roles/editor" + member = "serviceAccount:${google_service_account.test_account.email}" +} +`, account) +} + +func testAccServiceAccountIamPolicy_basic(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +data "google_iam_policy" "foo" { + binding { + role = "roles/owner" + + members = ["serviceAccount:${google_service_account.test_account.email}"] + } +} + +resource "google_service_account_iam_policy" "foo" { + service_account_id = "${google_service_account.test_account.id}" + policy_data = "${data.google_iam_policy.foo.policy_data}" +} +`, account) +} diff --git a/provider/terraform/tests/resource_google_service_account_key_test.go b/provider/terraform/tests/resource_google_service_account_key_test.go new file mode 100644 index 000000000000..66d1c77b3f93 --- /dev/null +++ b/provider/terraform/tests/resource_google_service_account_key_test.go @@ -0,0 +1,175 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// Test that a service account key can be created and destroyed +func TestAccServiceAccountKey_basic(t *testing.T) { + t.Parallel() + + resourceName := "google_service_account_key.acceptance" + accountID := "a" + acctest.RandString(10) + displayName := "Terraform Test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceAccountKey(accountID, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleServiceAccountKeyExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "public_key"), + resource.TestCheckResourceAttrSet(resourceName, "valid_after"), + resource.TestCheckResourceAttrSet(resourceName, "valid_before"), + resource.TestCheckResourceAttrSet(resourceName, "private_key"), + ), + }, + }, + }) +} + +func TestAccServiceAccountKey_fromEmail(t *testing.T) { + t.Parallel() + + resourceName := "google_service_account_key.acceptance" + accountID := "a" + acctest.RandString(10) + displayName := "Terraform Test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceAccountKey_fromEmail(accountID, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleServiceAccountKeyExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "public_key"), + resource.TestCheckResourceAttrSet(resourceName, "valid_after"), + resource.TestCheckResourceAttrSet(resourceName, "valid_before"), + resource.TestCheckResourceAttrSet(resourceName, "private_key"), + ), + }, + }, + }) +} + +func TestAccServiceAccountKey_pgp(t *testing.T) { + t.Parallel() + resourceName := "google_service_account_key.acceptance" + accountID := "a" + acctest.RandString(10) + displayName := "Terraform Test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceAccountKey_pgp(accountID, displayName, testKeyPairPubKey1), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleServiceAccountKeyExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "public_key"), + resource.TestCheckResourceAttrSet(resourceName, "private_key_encrypted"), + resource.TestCheckResourceAttrSet(resourceName, "private_key_fingerprint"), + ), + }, + }, + }) +} + +func testAccCheckGoogleServiceAccountKeyExists(r string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + config := testAccProvider.Meta().(*Config) + + _, err := config.clientIAM.Projects.ServiceAccounts.Keys.Get(rs.Primary.ID).Do() + if err != nil { + return err + } + + return nil + } +} + +func testAccServiceAccountKey(account, name string) string { + return fmt.Sprintf(` +resource "google_service_account" "acceptance" { + account_id = "%s" + display_name = "%s" +} + +resource "google_service_account_key" "acceptance" { + service_account_id = "${google_service_account.acceptance.name}" + public_key_type = "TYPE_X509_PEM_FILE" +} +`, account, name) +} + +func testAccServiceAccountKey_fromEmail(account, name string) string { + return fmt.Sprintf(` +resource "google_service_account" "acceptance" { + account_id = "%s" + display_name = "%s" +} + +resource "google_service_account_key" "acceptance" { + service_account_id = "${google_service_account.acceptance.email}" + public_key_type = "TYPE_X509_PEM_FILE" +} +`, account, name) +} + +func testAccServiceAccountKey_pgp(account, name string, key string) string { + return fmt.Sprintf(` +resource "google_service_account" "acceptance" { + account_id = "%s" + display_name = "%s" +} + +resource "google_service_account_key" "acceptance" { + service_account_id = "${google_service_account.acceptance.name}" + public_key_type = "TYPE_X509_PEM_FILE" + pgp_key = < <%= compile 'templates/license.erb' -%> -<%= lines(autogen_notice :ruby) -%> \ No newline at end of file +<%= lines(autogen_notice :ruby) -%> + +# A provider to manage <%= @api.name -%> resources. +class <%= object.name -%> < Inspec.resource(1) + + name 'google_<%= product_ns.downcase -%>_<%= object.name.downcase -%>' + desc '<%= object.name -%>' + supports platform: 'gcp-mm' + +<% object.properties.each do |prop| -%> + <%= "attr_reader :#{prop.out_name}" -%> + +<% end -%> + def base + '<%= object.self_link_url[0].join %>' + end + + def url + '<%= url(object) %>' + end + + # TODO + def parse end + + def exists? + !@fetched.nil? + end +end \ No newline at end of file diff --git a/templates/puppet/resource.erb b/templates/puppet/resource.erb index c831e8f8caa9..ecce6b3df1cc 100644 --- a/templates/puppet/resource.erb +++ b/templates/puppet/resource.erb @@ -42,7 +42,7 @@ -%> <%= lines(emit_requires(requires)) -%> -<% Google::LOGGER.info "Generating #{object.name}: #{object.out_name}" -%> +<% Google::LOGGER.debug "Generating #{object.name}: #{object.out_name}" -%> Puppet::Type.type(:<%= object.out_name -%>).provide(:google) do mk_resource_methods diff --git a/templates/puppet/type.erb b/templates/puppet/type.erb index 6562ee68af8f..fe21a01ed614 100644 --- a/templates/puppet/type.erb +++ b/templates/puppet/type.erb @@ -25,7 +25,7 @@ -%> <%= lines(emit_requires(requires)) -%> -<% Google::LOGGER.info "Generating #{object.name}: #{object.out_name}" -%> +<% Google::LOGGER.debug "Generating #{object.name}: #{object.out_name}" -%> Puppet::Type.newtype(:<%= object.out_name -%>) do <%= format_description(object, 2, '@doc =') %> @@ -39,7 +39,7 @@ Puppet::Type.newtype(:<%= object.out_name -%>) do unless object.parameters.nil? object.parameters.each do |param| if param.class <= Api::Type::ResourceRef - Google::LOGGER.info \ + Google::LOGGER.debug \ "Generating autorequire #{object.name}.#{param.name}: #{param.type}" -%> autorequire(:<%= param.out_type -%>) do @@ -103,7 +103,7 @@ Puppet::Type.newtype(:<%= object.out_name -%>) do <% unless object.parameters.nil? -%> <% object.parameters.each do |p| -%> -<% Google::LOGGER.info "Generating param #{object.name}.#{p.name}:#{p.type}" -%> +<% Google::LOGGER.debug "Generating param #{object.name}.#{p.name}:#{p.type}" -%> <%= namevar = identities.include?(p.name) ? 'namevar: true' : nil @@ -132,7 +132,7 @@ Puppet::Type.newtype(:<%= object.out_name -%>) do <% end -%> <% end -%> <% object.properties.each do |p| - Google::LOGGER.info "Generating #{object.name}.#{p.name}: #{p.type}" + Google::LOGGER.debug "Generating #{object.name}.#{p.name}: #{p.type}" -%> <%=