diff --git a/google/bootstrap_utils_test.go b/google/bootstrap_utils_test.go index f4f869acf62..f6ccc0b1bd3 100644 --- a/google/bootstrap_utils_test.go +++ b/google/bootstrap_utils_test.go @@ -7,6 +7,7 @@ import ( "testing" "google.golang.org/api/cloudkms/v1" + "google.golang.org/api/iam/v1" ) var SharedKeyRing = "tftest-shared-keyring-1" @@ -103,3 +104,96 @@ 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 27e2ba45995..53fbc71fda1 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" - - targetServiceAccountEmail := getTestServiceAccountFromEnv(t) + serviceAccount := getTestServiceAccountFromEnv(t) + targetServiceAccountEmail := BootstrapServiceAccount(t, getTestProjectFromEnv(), serviceAccount) 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 fa4c159b499..8895d9f20ad 100644 --- a/google/provider.go +++ b/google/provider.go @@ -184,6 +184,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_attached_disk_test.go b/google/resource_compute_attached_disk_test.go index 922c489c8de..e2d04007c6d 100644 --- a/google/resource_compute_attached_disk_test.go +++ b/google/resource_compute_attached_disk_test.go @@ -189,7 +189,6 @@ resource "google_compute_attached_disk" "test" { resource "google_compute_region_disk" "region" { name = "%s" region = "us-central1" - size = 10 replica_zones = ["us-central1-b", "us-central1-a"] } 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) +}