diff --git a/google/bootstrap_utils_test.go b/google/bootstrap_utils_test.go index f6ccc0b1bd3..f4f869acf62 100644 --- a/google/bootstrap_utils_test.go +++ b/google/bootstrap_utils_test.go @@ -7,7 +7,6 @@ import ( "testing" "google.golang.org/api/cloudkms/v1" - "google.golang.org/api/iam/v1" ) var SharedKeyRing = "tftest-shared-keyring-1" @@ -104,96 +103,3 @@ func BootstrapKMSKey(t *testing.T) bootstrappedKMS { cryptoKey, } } - -var serviceAccountEmail = "tf-bootstrap-service-account" -var serviceAccountDisplay = "Bootstrapped Service Account for Terraform tests" - -// Some tests need a second service account, other than the test runner, to assert functionality on. -// This provides a well-known service account that can be used when dynamically creating a service -// account isn't an option. -func getOrCreateServiceAccount(config Config, project string) (*iam.ServiceAccount, error) { - name := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, serviceAccountEmail, project) - log.Printf("[DEBUG] Verifying %s as bootstrapped service account.\n", name) - - sa, err := config.clientIAM.Projects.ServiceAccounts.Get(name).Do() - if err != nil && !isGoogleApiErrorWithCode(err, 404) { - return nil, err - } - - if sa == nil { - log.Printf("[DEBUG] Account missing. Creating %s as bootstrapped service account.\n", name) - sa = &iam.ServiceAccount{ - DisplayName: serviceAccountDisplay, - } - - r := &iam.CreateServiceAccountRequest{ - AccountId: serviceAccountEmail, - ServiceAccount: sa, - } - sa, err = config.clientIAM.Projects.ServiceAccounts.Create("projects/"+project, r).Do() - if err != nil { - return nil, err - } - } - - return sa, nil -} - -// In order to test impersonation we need to grant the testRunner's account the ability to grant tokens -// on a different service account. Granting permissions takes time and there is no operation to wait on -// so instead this creates a single service account once per test-suite with the correct permissions. -// The first time this test is run it will fail, but subsequent runs will succeed. -func impersonationServiceAccountPermissions(config Config, sa *iam.ServiceAccount, testRunner string) error { - log.Printf("[DEBUG] Setting service account permissions.\n") - policy := iam.Policy{ - Bindings: []*iam.Binding{}, - } - - binding := &iam.Binding{ - Role: "roles/iam.serviceAccountTokenCreator", - Members: []string{"serviceAccount:" + sa.Email, "serviceAccount:" + testRunner}, - } - policy.Bindings = append(policy.Bindings, binding) - - // Overwrite the roles each time on this service account. This is because this account is - // only created for the test suite and will stop snowflaking of permissions to get tests - // to run. Overwriting permissions on 1 service account shouldn't affect others. - _, err := config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(sa.Name, &iam.SetIamPolicyRequest{ - Policy: &policy, - }).Do() - if err != nil { - return err - } - - return nil -} - -func BootstrapServiceAccount(t *testing.T, project, testRunner string) string { - if v := os.Getenv("TF_ACC"); v == "" { - log.Println("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") - return "" - } - - config := Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - if err := config.LoadAndValidate(); err != nil { - t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) - } - - sa, err := getOrCreateServiceAccount(config, project) - if err != nil { - t.Fatalf("Bootstrapping failed. Cannot retrieve service account, %s", err) - } - - err = impersonationServiceAccountPermissions(config, sa, testRunner) - if err != nil { - t.Fatalf("Bootstrapping failed. Cannot set service account permissions, %s", err) - } - - return sa.Email -} diff --git a/google/data_source_google_service_account_access_token_test.go b/google/data_source_google_service_account_access_token_test.go index 53fbc71fda1..27e2ba45995 100644 --- a/google/data_source_google_service_account_access_token_test.go +++ b/google/data_source_google_service_account_access_token_test.go @@ -30,8 +30,8 @@ func TestAccDataSourceGoogleServiceAccountAccessToken_basic(t *testing.T) { t.Parallel() resourceName := "data.google_service_account_access_token.default" - serviceAccount := getTestServiceAccountFromEnv(t) - targetServiceAccountEmail := BootstrapServiceAccount(t, getTestProjectFromEnv(), serviceAccount) + + targetServiceAccountEmail := getTestServiceAccountFromEnv(t) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, diff --git a/google/iam_compute_subnetwork.go b/google/iam_compute_subnetwork.go index 93cfad7a2a0..5508b446951 100644 --- a/google/iam_compute_subnetwork.go +++ b/google/iam_compute_subnetwork.go @@ -1,3 +1,164 @@ package google -// Magic Modules doesn't let us remove files - blank out beta-only common-compile files for now. +import ( + "fmt" + "strings" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/compute/v1" +) + +var IamComputeSubnetworkSchema = map[string]*schema.Schema{ + "subnetwork": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, +} + +type ComputeSubnetworkIamUpdater struct { + project string + region string + resourceId string + Config *Config +} + +func NewComputeSubnetworkIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) { + project, err := getProject(d, config) + if err != nil { + return nil, err + } + + region, err := getRegion(d, config) + if err != nil { + return nil, err + } + + return &ComputeSubnetworkIamUpdater{ + project: project, + region: region, + resourceId: d.Get("subnetwork").(string), + Config: config, + }, nil +} + +func ComputeSubnetworkIdParseFunc(d *schema.ResourceData, config *Config) error { + parts := strings.Split(d.Id(), "/") + var fv *RegionalFieldValue + if len(parts) == 3 { + // {project}/{region}/{name} syntax + fv = &RegionalFieldValue{ + Project: parts[0], + Region: parts[1], + Name: parts[2], + resourceType: "subnetworks", + } + } else if len(parts) == 2 { + // /{region}/{name} syntax + project, err := getProject(d, config) + if err != nil { + return err + } + fv = &RegionalFieldValue{ + Project: project, + Region: parts[0], + Name: parts[1], + resourceType: "subnetworks", + } + } else { + // We either have a name or a full self link, so use the field helper + var err error + fv, err = ParseSubnetworkFieldValue(d.Id(), d, config) + if err != nil { + return err + } + } + d.Set("subnetwork", fv.Name) + d.Set("project", fv.Project) + d.Set("region", fv.Region) + + // Explicitly set the id so imported resources have the same ID format as non-imported ones. + d.SetId(fv.RelativeLink()) + return nil +} + +func (u *ComputeSubnetworkIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + p, err := u.Config.clientCompute.Subnetworks.GetIamPolicy(u.project, u.region, u.resourceId).Do() + + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + cloudResourcePolicy, err := computeToResourceManagerPolicy(p) + + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + return cloudResourcePolicy, nil +} + +func (u *ComputeSubnetworkIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + computePolicy, err := resourceManagerToComputePolicy(policy) + + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + req := &compute.RegionSetPolicyRequest{ + Policy: computePolicy, + } + _, err = u.Config.clientCompute.Subnetworks.SetIamPolicy(u.project, u.region, u.resourceId, req).Do() + + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + return nil +} + +func (u *ComputeSubnetworkIamUpdater) GetResourceId() string { + return fmt.Sprintf("projects/%s/regions/%s/subnetworks/%s", u.project, u.region, u.resourceId) +} + +func (u *ComputeSubnetworkIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-compute-subnetwork-%s-%s-%s", u.project, u.region, u.resourceId) +} + +func (u *ComputeSubnetworkIamUpdater) DescribeResource() string { + return fmt.Sprintf("Compute Subnetwork %s/%s/%s", u.project, u.region, u.resourceId) +} + +func resourceManagerToComputePolicy(p *cloudresourcemanager.Policy) (*compute.Policy, error) { + out := &compute.Policy{} + err := Convert(p, out) + if err != nil { + return nil, errwrap.Wrapf("Cannot convert a resourcemanager policy to a compute policy: {{err}}", err) + } + return out, nil +} + +func computeToResourceManagerPolicy(p *compute.Policy) (*cloudresourcemanager.Policy, error) { + out := &cloudresourcemanager.Policy{} + err := Convert(p, out) + if err != nil { + return nil, errwrap.Wrapf("Cannot convert a compute policy to a resourcemanager policy: {{err}}", err) + } + return out, nil +} diff --git a/google/provider.go b/google/provider.go index e849ca0258c..1a4a7b36dad 100644 --- a/google/provider.go +++ b/google/provider.go @@ -183,6 +183,9 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_compute_security_policy": resourceComputeSecurityPolicy(), "google_compute_shared_vpc_host_project": resourceComputeSharedVpcHostProject(), "google_compute_shared_vpc_service_project": resourceComputeSharedVpcServiceProject(), + "google_compute_subnetwork_iam_binding": ResourceIamBindingWithImport(IamComputeSubnetworkSchema, NewComputeSubnetworkIamUpdater, ComputeSubnetworkIdParseFunc), + "google_compute_subnetwork_iam_member": ResourceIamMemberWithImport(IamComputeSubnetworkSchema, NewComputeSubnetworkIamUpdater, ComputeSubnetworkIdParseFunc), + "google_compute_subnetwork_iam_policy": ResourceIamPolicyWithImport(IamComputeSubnetworkSchema, NewComputeSubnetworkIamUpdater, ComputeSubnetworkIdParseFunc), "google_compute_target_pool": resourceComputeTargetPool(), "google_container_cluster": resourceContainerCluster(), "google_container_node_pool": resourceContainerNodePool(), diff --git a/google/resource_compute_subnetwork_iam_test.go b/google/resource_compute_subnetwork_iam_test.go index 93cfad7a2a0..fb25002a367 100644 --- a/google/resource_compute_subnetwork_iam_test.go +++ b/google/resource_compute_subnetwork_iam_test.go @@ -1,3 +1,247 @@ package google -// Magic Modules doesn't let us remove files - blank out beta-only common-compile files for now. +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccComputeSubnetworkIamBinding(t *testing.T) { + t.Parallel() + + account := acctest.RandomWithPrefix("tf-test") + role := "roles/compute.networkUser" + region := getTestRegionFromEnv() + subnetwork := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccComputeSubnetworkIamBinding_basic(account, region, subnetwork, role), + }, + { + ResourceName: "google_compute_subnetwork_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s/%s %s", region, subnetwork, role), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccComputeSubnetworkIamBinding_update(account, region, subnetwork, role), + }, + { + ResourceName: "google_compute_subnetwork_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s/%s %s", region, subnetwork, role), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccComputeSubnetworkIamMember(t *testing.T) { + t.Parallel() + + project := getTestProjectFromEnv() + account := acctest.RandomWithPrefix("tf-test") + role := "roles/compute.networkUser" + region := getTestRegionFromEnv() + subnetwork := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccComputeSubnetworkIamMember_basic(account, region, subnetwork, role), + }, + { + ResourceName: "google_compute_subnetwork_iam_member.foo", + ImportStateId: fmt.Sprintf("%s/%s %s serviceAccount:%s@%s.iam.gserviceaccount.com", region, subnetwork, role, account, project), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccComputeSubnetworkIamPolicy(t *testing.T) { + t.Parallel() + + project := getTestProjectFromEnv() + account := acctest.RandomWithPrefix("tf-test") + role := "roles/compute.networkUser" + region := getTestRegionFromEnv() + subnetwork := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccComputeSubnetworkIamPolicy_basic(account, region, subnetwork, role), + }, + // Test a few import formats + { + ResourceName: "google_compute_subnetwork_iam_policy.foo", + ImportStateId: fmt.Sprintf("projects/%s/regions/%s/subnetworks/%s", project, region, subnetwork), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_compute_subnetwork_iam_policy.foo", + ImportStateId: fmt.Sprintf("%s/%s/%s", project, region, subnetwork), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_compute_subnetwork_iam_policy.foo", + ImportStateId: fmt.Sprintf("%s/%s", region, subnetwork), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_compute_subnetwork_iam_policy.foo", + ImportStateId: fmt.Sprintf("%s", subnetwork), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccComputeSubnetworkIamBinding_basic(account, region, subnetworkName, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_compute_network" "network" { + name = "%s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnetwork" { + name = "%s" + region = "%s" + ip_cidr_range = "10.1.0.0/16" + network = "${google_compute_network.network.name}" +} + +resource "google_compute_subnetwork_iam_binding" "foo" { + project = "${google_compute_subnetwork.subnetwork.project}" + region = "${google_compute_subnetwork.subnetwork.region}" + subnetwork = "${google_compute_subnetwork.subnetwork.name}" + role = "%s" + members = ["serviceAccount:${google_service_account.test_account.email}"] +} +`, account, subnetworkName, subnetworkName, region, roleId) +} + +func testAccComputeSubnetworkIamBinding_update(account, region, subnetworkName, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_service_account" "test_account_2" { + account_id = "%s-2" + display_name = "Iam Testing Account" +} + +resource "google_compute_network" "network" { + name = "%s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnetwork" { + name = "%s" + region = "%s" + ip_cidr_range = "10.1.0.0/16" + network = "${google_compute_network.network.name}" +} + +resource "google_compute_subnetwork_iam_binding" "foo" { + project = "${google_compute_subnetwork.subnetwork.project}" + region = "${google_compute_subnetwork.subnetwork.region}" + subnetwork = "${google_compute_subnetwork.subnetwork.name}" + role = "%s" + members = [ + "serviceAccount:${google_service_account.test_account.email}", + "serviceAccount:${google_service_account.test_account_2.email}" + ] +} +`, account, account, subnetworkName, subnetworkName, region, roleId) +} + +func testAccComputeSubnetworkIamMember_basic(account, region, subnetworkName, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_compute_network" "network" { + name = "%s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnetwork" { + name = "%s" + region = "%s" + ip_cidr_range = "10.1.0.0/16" + network = "${google_compute_network.network.name}" +} + +resource "google_compute_subnetwork_iam_member" "foo" { + project = "${google_compute_subnetwork.subnetwork.project}" + region = "${google_compute_subnetwork.subnetwork.region}" + subnetwork = "${google_compute_subnetwork.subnetwork.name}" + role = "%s" + member = "serviceAccount:${google_service_account.test_account.email}" +} +`, account, subnetworkName, subnetworkName, region, roleId) +} + +func testAccComputeSubnetworkIamPolicy_basic(account, region, subnetworkName, roleId string) string { + return fmt.Sprintf(` +resource "google_service_account" "test_account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_compute_network" "network" { + name = "%s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnetwork" { + name = "%s" + region = "%s" + ip_cidr_range = "10.1.0.0/16" + network = "${google_compute_network.network.name}" +} + +data "google_iam_policy" "foo" { + binding { + role = "%s" + + members = ["serviceAccount:${google_service_account.test_account.email}"] + } +} + +resource "google_compute_subnetwork_iam_policy" "foo" { + project = "${google_compute_subnetwork.subnetwork.project}" + region = "${google_compute_subnetwork.subnetwork.region}" + subnetwork = "${google_compute_subnetwork.subnetwork.name}" + policy_data = "${data.google_iam_policy.foo.policy_data}" +} +`, account, subnetworkName, subnetworkName, region, roleId) +}