diff --git a/mmv1/third_party/terraform/utils/provider_test.go.erb b/mmv1/third_party/terraform/utils/provider_test.go.erb index 37b8e4eee875..40301f4a2bc7 100644 --- a/mmv1/third_party/terraform/utils/provider_test.go.erb +++ b/mmv1/third_party/terraform/utils/provider_test.go.erb @@ -27,6 +27,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "google.golang.org/api/cloudbilling/v1" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/iamcredentials/v1" + "google.golang.org/api/serviceusage/v1" ) var testAccProviders map[string]*schema.Provider @@ -542,26 +546,25 @@ func TestAccProviderUserProjectOverride(t *testing.T) { org := getTestOrgFromEnv(t) billing := getTestBillingAccountFromEnv(t) pid := "tf-test-" + randString(t, 10) - sa := "tf-test-" + randString(t, 10) topicName := "tf-test-topic-" + randString(t, 10) + config := BootstrapConfig(t) + accessToken, err := setupProjectsAndGetAccessToken(org, billing, pid, "pubsub", config) + if err != nil { + t.Error(err) + } + vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, // No TestDestroy since that's not really the point of this test Steps: []resource.TestStep{ { - Config: testAccProviderUserProjectOverride(pid, pname, org, billing, sa), - // The token creator IAM API call returns success long before the policy is - // actually usable. Wait a solid 2 minutes to ensure we can use it. - Check: sleepInSecondsForTest(2 * 60), - }, - { - Config: testAccProviderUserProjectOverride_step2(pid, pname, org, billing, sa, false, topicName), + Config: testAccProviderUserProjectOverride_step2(accessToken, pid, false, topicName), ExpectError: regexp.MustCompile("Cloud Pub/Sub API has not been used"), }, { - Config: testAccProviderUserProjectOverride_step2(pid, pname, org, billing, sa, true, topicName), + Config: testAccProviderUserProjectOverride_step2(accessToken, pid, true, topicName), }, { ResourceName: "google_pubsub_topic.project-2-topic", @@ -569,7 +572,7 @@ func TestAccProviderUserProjectOverride(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccProviderUserProjectOverride_step3(pid, pname, org, billing, sa, true), + Config: testAccProviderUserProjectOverride_step3(accessToken, true), }, }, }) @@ -585,7 +588,12 @@ func TestAccProviderIndirectUserProjectOverride(t *testing.T) { org := getTestOrgFromEnv(t) billing := getTestBillingAccountFromEnv(t) pid := "tf-test-" + randString(t, 10) - sa := "tf-test-" + randString(t, 10) + + config := BootstrapConfig(t) + accessToken, err := setupProjectsAndGetAccessToken(org, billing, pid, "cloudkms", config) + if err != nil { + t.Error(err) + } vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -593,20 +601,11 @@ func TestAccProviderIndirectUserProjectOverride(t *testing.T) { // No TestDestroy since that's not really the point of this test Steps: []resource.TestStep{ { - Config: testAccProviderIndirectUserProjectOverride(pid, pname, org, billing, sa), - Check: func(s *terraform.State) error { - // The token creator IAM API call returns success long before the policy is - // actually usable. Wait a solid 2 minutes to ensure we can use it. - time.Sleep(2 * time.Minute) - return nil - }, - }, - { - Config: testAccProviderIndirectUserProjectOverride_step2(pid, pname, org, billing, sa, false), + Config: testAccProviderIndirectUserProjectOverride_step2(pid, accessToken, false), ExpectError: regexp.MustCompile(`Cloud Key Management Service \(KMS\) API has not been used`), }, { - Config: testAccProviderIndirectUserProjectOverride_step2(pid, pname, org, billing, sa, true), + Config: testAccProviderIndirectUserProjectOverride_step2(pid, accessToken, true), }, { ResourceName: "google_kms_crypto_key.project-2-key", @@ -614,7 +613,7 @@ func TestAccProviderIndirectUserProjectOverride(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccProviderIndirectUserProjectOverride_step3(pid, pname, org, billing, sa, true), + Config: testAccProviderIndirectUserProjectOverride_step3(accessToken, true), }, }, }) @@ -647,58 +646,8 @@ resource "google_compute_address" "default" { // Set up two projects. Project 1 has a service account that is used to create a // pubsub topic in project 2. The pubsub API is only enabled in project 2, // which causes the create to fail unless user_project_override is set to true. -func testAccProviderUserProjectOverride(pid, name, org, billing, sa string) string { - return fmt.Sprintf(` -resource "google_project" "project-1" { - project_id = "%s" - name = "%s" - org_id = "%s" - billing_account = "%s" -} - -resource "google_service_account" "project-1" { - project = google_project.project-1.project_id - account_id = "%s" -} - -resource "google_project" "project-2" { - project_id = "%s-2" - name = "%s-2" - org_id = "%s" - billing_account = "%s" -} - -resource "google_project_service" "project-2-pubsub-service" { - project = google_project.project-2.project_id - service = "pubsub.googleapis.com" -} - -// Permission needed for user_project_override -resource "google_project_iam_member" "project-2-serviceusage" { - project = google_project.project-2.project_id - role = "roles/serviceusage.serviceUsageConsumer" - member = "serviceAccount:${google_service_account.project-1.email}" -} -resource "google_project_iam_member" "project-2-pubsub-member" { - project = google_project.project-2.project_id - role = "roles/pubsub.admin" - member = "serviceAccount:${google_service_account.project-1.email}" -} - -data "google_client_openid_userinfo" "me" {} - -// Enable the test runner to get an access token on behalf of -// the project 1 service account -resource "google_service_account_iam_member" "token-creator-iam" { - service_account_id = google_service_account.project-1.name - role = "roles/iam.serviceAccountTokenCreator" - member = "serviceAccount:${data.google_client_openid_userinfo.me.email}" -} -`, pid, name, org, billing, sa, pid, name, org, billing) -} - -func testAccProviderUserProjectOverride_step2(pid, name, org, billing, sa string, override bool, topicName string) string { +func testAccProviderUserProjectOverride_step2(accessToken, pid string, override bool, topicName string) string { return fmt.Sprintf(` // See step 3 below, which is really step 2 minus the pubsub topic. // Step 3 exists because provider configurations can't be removed while objects @@ -708,101 +657,27 @@ func testAccProviderUserProjectOverride_step2(pid, name, org, billing, sa string resource "google_pubsub_topic" "project-2-topic" { provider = google.project-1-token - project = google_project.project-2.project_id + project = "%s-2" name = "%s" labels = { foo = "bar" } } -`, testAccProviderUserProjectOverride_step3(pid, name, org, billing, sa, override), topicName) +`, testAccProviderUserProjectOverride_step3(accessToken, override), pid, topicName) } -func testAccProviderUserProjectOverride_step3(pid, name, org, billing, sa string, override bool) string { +func testAccProviderUserProjectOverride_step3(accessToken string, override bool) string { return fmt.Sprintf(` -%s - -data "google_service_account_access_token" "project-1-token" { - // This data source would have a depends_on t - // google_service_account_iam_binding.token-creator-iam, but depends_on - // in data sources makes them always have a diff in apply: - // https://www.terraform.io/docs/configuration/data-sources.html#data-resource-dependencies - // Instead, rely on the other test step completing before this one. - - target_service_account = google_service_account.project-1.email - scopes = ["userinfo-email", "https://www.googleapis.com/auth/cloud-platform"] - lifetime = "300s" -} - provider "google" { alias = "project-1-token" - access_token = data.google_service_account_access_token.project-1-token.access_token + access_token = "%s" user_project_override = %v } -`, testAccProviderUserProjectOverride(pid, name, org, billing, sa), override) -} - -// Set up two projects. Project 1 has a service account that is used to create a -// kms crypto key in project 2. The kms API is only enabled in project 2, -// which causes the create to fail unless user_project_override is set to true. -func testAccProviderIndirectUserProjectOverride(pid, name, org, billing, sa string) string { - return fmt.Sprintf(` -resource "google_project" "project-1" { - project_id = "%s" - name = "%s" - org_id = "%s" - billing_account = "%s" -} - -resource "google_service_account" "project-1" { - project = google_project.project-1.project_id - account_id = "%s" -} - -resource "google_project" "project-2" { - project_id = "%s-2" - name = "%s-2" - org_id = "%s" - billing_account = "%s" -} - -resource "google_project_service" "project-2-kms" { - project = google_project.project-2.project_id - service = "cloudkms.googleapis.com" -} - -// Permission needed for user_project_override -resource "google_project_iam_member" "project-2-serviceusage" { - project = google_project.project-2.project_id - role = "roles/serviceusage.serviceUsageConsumer" - member = "serviceAccount:${google_service_account.project-1.email}" -} - -resource "google_project_iam_member" "project-2-kms" { - project = google_project.project-2.project_id - role = "roles/cloudkms.admin" - member = "serviceAccount:${google_service_account.project-1.email}" -} - -resource "google_project_iam_member" "project-2-kms-encrypt" { - project = google_project.project-2.project_id - role = "roles/cloudkms.cryptoKeyEncrypter" - member = "serviceAccount:${google_service_account.project-1.email}" -} - -data "google_client_openid_userinfo" "me" {} - -// Enable the test runner to get an access token on behalf of -// the project 1 service account -resource "google_service_account_iam_member" "token-creator-iam" { - service_account_id = google_service_account.project-1.name - role = "roles/iam.serviceAccountTokenCreator" - member = "serviceAccount:${data.google_client_openid_userinfo.me.email}" -} -`, pid, name, org, billing, sa, pid, name, org, billing) +`, accessToken, override) } -func testAccProviderIndirectUserProjectOverride_step2(pid, name, org, billing, sa string, override bool) string { +func testAccProviderIndirectUserProjectOverride_step2(pid, accessToken string, override bool) string { return fmt.Sprintf(` // See step 3 below, which is really step 2 minus the kms resources. // Step 3 exists because provider configurations can't be removed while objects @@ -812,7 +687,7 @@ func testAccProviderIndirectUserProjectOverride_step2(pid, name, org, billing, s resource "google_kms_key_ring" "project-2-keyring" { provider = google.project-1-token - project = google_project.project-2.project_id + project = "%s-2" name = "%s" location = "us-central1" @@ -829,32 +704,18 @@ data "google_kms_secret_ciphertext" "project-2-ciphertext" { crypto_key = google_kms_crypto_key.project-2-key.id plaintext = "my-secret" } -`, testAccProviderIndirectUserProjectOverride_step3(pid, name, org, billing, sa, override), pid, pid) +`, testAccProviderIndirectUserProjectOverride_step3(accessToken, override), pid, pid, pid) } -func testAccProviderIndirectUserProjectOverride_step3(pid, name, org, billing, sa string, override bool) string { +func testAccProviderIndirectUserProjectOverride_step3(accessToken string, override bool) string { return fmt.Sprintf(` -%s - -data "google_service_account_access_token" "project-1-token" { - // This data source would have a depends_on to - // google_service_account_iam_binding.token-creator-iam, but depends_on - // in data sources makes them always have a diff in apply: - // https://www.terraform.io/docs/configuration/data-sources.html#data-resource-dependencies - // Instead, rely on the other test step completing before this one. - - target_service_account = google_service_account.project-1.email - scopes = ["userinfo-email", "https://www.googleapis.com/auth/cloud-platform"] - lifetime = "300s" -} - provider "google" { alias = "project-1-token" - access_token = data.google_service_account_access_token.project-1-token.access_token + access_token = "%s" user_project_override = %v } -`, testAccProviderIndirectUserProjectOverride(pid, name, org, billing, sa), override) +`, accessToken, override) } // getTestRegion has the same logic as the provider's getRegion, to be used in tests. @@ -970,6 +831,197 @@ func sleepInSecondsForTest(t int) resource.TestCheckFunc { } } +func setupProjectsAndGetAccessToken(org, billing, pid, service string, config *Config) (string, error) { + // Create project-1 and project-2 + rmService := config.NewResourceManagerClient(config.userAgent) + + project := &cloudresourcemanager.Project{ + ProjectId: pid, + Name: pname, + Parent: &cloudresourcemanager.ResourceId{ + Id: org, + Type: "organization", + }, + } + + var op *cloudresourcemanager.Operation + err := retryTimeDuration(func() (reqErr error) { + op, reqErr = rmService.Projects.Create(project).Do() + return reqErr + }, 5*time.Minute) + if err != nil { + return "", err + } + + // Wait for the operation to complete + opAsMap, err := ConvertToMap(op) + if err != nil { + return "", err + } + + waitErr := resourceManagerOperationWaitTime(config, opAsMap, "creating project", config.userAgent, 5*time.Minute) + if waitErr != nil { + return "", waitErr + } + + ba := &cloudbilling.ProjectBillingInfo{ + BillingAccountName: fmt.Sprintf("billingAccounts/%s", billing), + } + _, err = config.NewBillingClient(config.userAgent).Projects.UpdateBillingInfo(prefixedProject(pid), ba).Do() + if err != nil { + return "", err + } + + p2 := fmt.Sprintf("%s-2", pid) + project.ProjectId = p2 + project.Name = fmt.Sprintf("%s-2", pname) + + err = retryTimeDuration(func() (reqErr error) { + op, reqErr = rmService.Projects.Create(project).Do() + return reqErr + }, 5*time.Minute) + if err != nil { + return "", err + } + + // Wait for the operation to complete + opAsMap, err = ConvertToMap(op) + if err != nil { + return "", err + } + + waitErr = resourceManagerOperationWaitTime(config, opAsMap, "creating project", config.userAgent, 5*time.Minute) + if waitErr != nil { + return "", waitErr + } + + _, err = config.NewBillingClient(config.userAgent).Projects.UpdateBillingInfo(prefixedProject(p2), ba).Do() + if err != nil { + return "", err + } + + // Enable the appropriate service in project-2 only + suService := config.NewServiceUsageClient(config.userAgent) + + serviceReq := &serviceusage.BatchEnableServicesRequest{ + ServiceIds: []string{fmt.Sprintf("%s.googleapis.com", service)}, + } + + _, err = suService.Services.BatchEnable(fmt.Sprintf("projects/%s", p2), serviceReq).Do() + if err != nil { + return "", err + } + + // Enable the test runner to create service accounts and get an access token on behalf of + // the project 1 service account + curEmail, err := GetCurrentUserEmail(config, config.userAgent) + if err != nil { + return "", err + } + + proj1SATokenCreator := &cloudresourcemanager.Binding{ + Members: []string{fmt.Sprintf("serviceAccount:%s", curEmail)}, + Role: "roles/iam.serviceAccountTokenCreator", + } + + proj1SACreator := &cloudresourcemanager.Binding{ + Members: []string{fmt.Sprintf("serviceAccount:%s", curEmail)}, + Role: "roles/iam.serviceAccountCreator", + } + + bindings := mergeBindings([]*cloudresourcemanager.Binding{proj1SATokenCreator, proj1SACreator}) + + p, err := rmService.Projects.GetIamPolicy(pid, + &cloudresourcemanager.GetIamPolicyRequest{ + Options: &cloudresourcemanager.GetPolicyOptions{ + RequestedPolicyVersion: iamPolicyVersion, + }, + }).Do() + if err != nil { + return "", err + } + + p.Bindings = mergeBindings(append(p.Bindings, bindings...)) + _, err = config.NewResourceManagerClient(config.userAgent).Projects.SetIamPolicy(pid, + &cloudresourcemanager.SetIamPolicyRequest{ + Policy: p, + UpdateMask: "bindings,etag,auditConfigs", + }).Do() + if err != nil { + return "", err + } + + // Create a service account for project-1 + sa1, err := getOrCreateServiceAccount(config, pid) + if err != nil { + return "", err + } + + // Add permissions to service accounts + + // Permission needed for user_project_override + proj2ServiceUsageBinding := &cloudresourcemanager.Binding{ + Members: []string{fmt.Sprintf("serviceAccount:%s", sa1.Email)}, + Role: "roles/serviceusage.serviceUsageConsumer", + } + + // Admin permission for service + proj2ServiceAdminBinding := &cloudresourcemanager.Binding{ + Members: []string{fmt.Sprintf("serviceAccount:%s", sa1.Email)}, + Role: fmt.Sprintf("roles/%s.admin", service), + } + + bindings = mergeBindings([]*cloudresourcemanager.Binding{proj2ServiceUsageBinding, proj2ServiceAdminBinding}) + + // For KMS test only + if service == "cloudkms" { + proj2CryptoKeyBinding := &cloudresourcemanager.Binding{ + Members: []string{fmt.Sprintf("serviceAccount:%s", sa1.Email)}, + Role: "roles/cloudkms.cryptoKeyEncrypter", + } + + bindings = mergeBindings(append(bindings, proj2CryptoKeyBinding)) + } + + p, err = rmService.Projects.GetIamPolicy(p2, + &cloudresourcemanager.GetIamPolicyRequest{ + Options: &cloudresourcemanager.GetPolicyOptions{ + RequestedPolicyVersion: iamPolicyVersion, + }, + }).Do() + if err != nil { + return "", err + } + + p.Bindings = mergeBindings(append(p.Bindings, bindings...)) + _, err = config.NewResourceManagerClient(config.userAgent).Projects.SetIamPolicy(p2, + &cloudresourcemanager.SetIamPolicyRequest{ + Policy: p, + UpdateMask: "bindings,etag,auditConfigs", + }).Do() + if err != nil { + return "", err + } + + // The token creator IAM API call returns success long before the policy is + // actually usable. Wait a solid 2 minutes to ensure we can use it. + time.Sleep(2 * time.Minute) + + iamCredsService := config.NewIamCredentialsClient(config.userAgent) + tokenRequest := &iamcredentials.GenerateAccessTokenRequest{ + Lifetime: "300s", + Scope: []string{"https://www.googleapis.com/auth/cloud-platform"}, + } + atResp, err := iamCredsService.Projects.ServiceAccounts.GenerateAccessToken(fmt.Sprintf("projects/-/serviceAccounts/%s", sa1.Email), tokenRequest).Do() + if err != nil { + return "", err + } + + accessToken := atResp.AccessToken + + return accessToken, nil +} + func isReleaseDiffEnabled() bool { releaseDiff := os.Getenv("RELEASE_DIFF") return releaseDiff != ""