From 13ed799a8b8353ae64a675ab1c5079b649f1c742 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 20 Oct 2020 22:36:03 +0200 Subject: [PATCH 1/2] Update service account module to Terraform 0.13 --- modules/iam-service-account/README.md | 54 +++++++ modules/iam-service-account/main.tf | 125 ++++++++++++++++ modules/iam-service-account/outputs.tf | 36 +++++ .../variables.tf | 45 +++--- .../versions.tf | 0 modules/iam-service-accounts/README.md | 59 -------- modules/iam-service-accounts/main.tf | 136 ------------------ modules/iam-service-accounts/outputs.tf | 75 ---------- 8 files changed, 240 insertions(+), 290 deletions(-) create mode 100644 modules/iam-service-account/README.md create mode 100644 modules/iam-service-account/main.tf create mode 100644 modules/iam-service-account/outputs.tf rename modules/{iam-service-accounts => iam-service-account}/variables.tf (52%) rename modules/{iam-service-accounts => iam-service-account}/versions.tf (100%) delete mode 100644 modules/iam-service-accounts/README.md delete mode 100644 modules/iam-service-accounts/main.tf delete mode 100644 modules/iam-service-accounts/outputs.tf diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md new file mode 100644 index 00000000..f55c0b51 --- /dev/null +++ b/modules/iam-service-account/README.md @@ -0,0 +1,54 @@ +# Google Service Account Module + +This module allows simplified creation and management of one a service account and its IAM bindings. A key can optionally be generated and will be stored in Terraform state. To use it create a sensitive output in your root modules referencing the `key` output, then extract the private key from the JSON formatted outputs. + +## Example + +```hcl +module "myproject-default-service-accounts" { + source = "./modules/iam-service-accounts" + project_id = "myproject" + name = "vm-default" + generate_key = true + # authoritative roles granted *on* the service accounts to other identities + iam_roles = ["roles/iam.serviceAccountUser"] + iam_members = { + "roles/iam.serviceAccountUser" = ["user:foo@example.com"] + } + # non-authoritative roles granted *to* the service accounts on other resources + iam_project_roles = { + "myproject" = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + ] + } +} +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---: |:---:|:---:| +| name | Name of the service account to create. | string | ✓ | | +| project_id | Project id where service account will be created. | string | ✓ | | +| *display_name* | Display name of the service account to create. | string | | Terraform-managed. | +| *generate_key* | Generate a key for service account. | bool | | false | +| *iam_billing_roles* | Project roles granted to the service account, by billing account id. | map(set(string)) | | {} | +| *iam_folder_roles* | Project roles granted to the service account, by folder id. | map(set(string)) | | {} | +| *iam_members* | Map of members which are granted authoritative roles on the service account, keyed by role. | map(set(string)) | | {} | +| *iam_organization_roles* | Project roles granted to the service account, by organization id. | map(set(string)) | | {} | +| *iam_project_roles* | Project roles granted to the service account, by project id. | map(set(string)) | | {} | +| *iam_roles* | Authoritative roles granted on the service account. | set(string) | | [] | +| *iam_storage_roles* | Storage roles granted to the service account, by bucket name. | map(set(string)) | | {} | +| *prefix* | Prefix applied to service account names. | string | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| email | Service account email. | | +| iam_email | IAM-format service account email. | | +| key | Service account key. | ✓ | +| service_account | Service account resource. | | + diff --git a/modules/iam-service-account/main.tf b/modules/iam-service-account/main.tf new file mode 100644 index 00000000..b749e172 --- /dev/null +++ b/modules/iam-service-account/main.tf @@ -0,0 +1,125 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + iam_billing_pairs = flatten([ + for entity, roles in var.iam_billing_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_folder_pairs = flatten([ + for entity, roles in var.iam_folder_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_organization_pairs = flatten([ + for entity, roles in var.iam_organization_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_project_pairs = flatten([ + for entity, roles in var.iam_project_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + iam_storage_pairs = flatten([ + for entity, roles in var.iam_storage_roles : [ + for role in roles : [ + { entity = entity, role = role } + ] + ] + ]) + key = var.generate_key ? google_service_account_key.key["1"] : {} + prefix = var.prefix != null ? "${var.prefix}-" : "" + resource_iam_email = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_service_account" "service_account" { + project = var.project_id + account_id = "${local.prefix}${var.name}" + display_name = var.display_name +} + +resource "google_service_account_key" "key" { + for_each = var.generate_key ? { 1 = 1 } : {} + service_account_id = google_service_account.service_account.email +} + +resource "google_service_account_iam_binding" "roles" { + for_each = var.iam_roles + #for_each = toset(keys(var.iam_members)) + service_account_id = google_service_account.service_account.name + role = each.key + members = lookup(var.iam_members, each.key, []) +} + +resource "google_billing_account_iam_member" "billing-roles" { + for_each = { + for pair in local.iam_billing_pairs : + "${pair.entity}-${pair.role}" => pair + } + billing_account_id = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_folder_iam_member" "folder-roles" { + for_each = { + for pair in local.iam_folder_pairs : + "${pair.entity}-${pair.role}" => pair + } + folder = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_organization_iam_member" "organization-roles" { + for_each = { + for pair in local.iam_organization_pairs : + "${pair.entity}-${pair.role}" => pair + } + org_id = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_project_iam_member" "project-roles" { + for_each = { + for pair in local.iam_project_pairs : + "${pair.entity}-${pair.role}" => pair + } + project = each.value.entity + role = each.value.role + member = local.resource_iam_email +} + +resource "google_storage_bucket_iam_member" "bucket-roles" { + for_each = { + for pair in local.iam_storage_pairs : + "${pair.entity}-${pair.role}" => pair + } + bucket = each.value.entity + role = each.value.role + member = local.resource_iam_email +} diff --git a/modules/iam-service-account/outputs.tf b/modules/iam-service-account/outputs.tf new file mode 100644 index 00000000..86de600b --- /dev/null +++ b/modules/iam-service-account/outputs.tf @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "service_account" { + description = "Service account resource." + value = google_service_account.service_account +} + +output "email" { + description = "Service account email." + value = google_service_account.service_account.email +} + +output "iam_email" { + description = "IAM-format service account email." + value = local.resource_iam_email +} + +output "key" { + description = "Service account key." + sensitive = true + value = local.key +} diff --git a/modules/iam-service-accounts/variables.tf b/modules/iam-service-account/variables.tf similarity index 52% rename from modules/iam-service-accounts/variables.tf rename to modules/iam-service-account/variables.tf index 1ffedff8..6d29cb80 100644 --- a/modules/iam-service-accounts/variables.tf +++ b/modules/iam-service-account/variables.tf @@ -14,58 +14,63 @@ * limitations under the License. */ -variable "generate_keys" { - description = "Generate keys for service accounts." +variable "generate_key" { + description = "Generate a key for service account." type = bool default = false } variable "iam_members" { - description = "Map of member lists which are granted authoritative roles on the service accounts, keyed by role." - type = map(list(string)) + description = "Map of members which are granted authoritative roles on the service account, keyed by role." + type = map(set(string)) default = {} } variable "iam_roles" { - description = "List of authoritative roles granted on the service accounts." - type = list(string) + description = "Authoritative roles granted on the service account." + type = set(string) default = [] } variable "iam_billing_roles" { - description = "Project roles granted to all service accounts, by billing account id." - type = map(list(string)) + description = "Project roles granted to the service account, by billing account id." + type = map(set(string)) default = {} } variable "iam_folder_roles" { - description = "Project roles granted to all service accounts, by folder id." - type = map(list(string)) + description = "Project roles granted to the service account, by folder id." + type = map(set(string)) default = {} } variable "iam_organization_roles" { - description = "Project roles granted to all service accounts, by organization id." - type = map(list(string)) + description = "Project roles granted to the service account, by organization id." + type = map(set(string)) default = {} } variable "iam_project_roles" { - description = "Project roles granted to all service accounts, by project id." - type = map(list(string)) + description = "Project roles granted to the service account, by project id." + type = map(set(string)) default = {} } variable "iam_storage_roles" { - description = "Storage roles granted to all service accounts, by bucket name." - type = map(list(string)) + description = "Storage roles granted to the service account, by bucket name." + type = map(set(string)) default = {} } -variable "names" { - description = "Names of the service accounts to create." - type = list(string) - default = [] +variable "name" { + description = "Name of the service account to create." + type = string +} + +variable "display_name" { + description = "Display name of the service account to create." + type = string + default = "Terraform-managed." } variable "prefix" { diff --git a/modules/iam-service-accounts/versions.tf b/modules/iam-service-account/versions.tf similarity index 100% rename from modules/iam-service-accounts/versions.tf rename to modules/iam-service-account/versions.tf diff --git a/modules/iam-service-accounts/README.md b/modules/iam-service-accounts/README.md deleted file mode 100644 index d348db72..00000000 --- a/modules/iam-service-accounts/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Google Service Accounts Module - -This module allows simplified creation and management of one or more service accounts and their IAM bindings. Keys can optionally be generated and will be stored in Terraform state. To use them create a sensitive output in your root modules referencing the `keys` or `key` outputs, then extract the private key from the JSON formatted outputs. - -## Example - -```hcl -module "myproject-default-service-accounts" { - source = "./modules/iam-service-accounts" - project_id = "myproject" - names = ["vm-default", "gke-node-default"] - generate_keys = true - # authoritative roles granted *on* the service accounts to other identities - iam_roles = ["roles/iam.serviceAccountUser"] - iam_members = { - "roles/iam.serviceAccountUser" = ["user:foo@example.com"] - } - # non-authoritative roles granted *to* the service accounts on other resources - iam_project_roles = { - "myproject" = [ - "roles/logging.logWriter", - "roles/monitoring.metricWriter", - ] - } -} -``` - - -## Variables - -| name | description | type | required | default | -|---|---|:---: |:---:|:---:| -| project_id | Project id where service account will be created. | string | ✓ | | -| *generate_keys* | Generate keys for service accounts. | bool | | false | -| *iam_billing_roles* | Project roles granted to all service accounts, by billing account id. | map(list(string)) | | {} | -| *iam_folder_roles* | Project roles granted to all service accounts, by folder id. | map(list(string)) | | {} | -| *iam_members* | Map of member lists which are granted authoritative roles on the service accounts, keyed by role. | map(list(string)) | | {} | -| *iam_organization_roles* | Project roles granted to all service accounts, by organization id. | map(list(string)) | | {} | -| *iam_project_roles* | Project roles granted to all service accounts, by project id. | map(list(string)) | | {} | -| *iam_roles* | List of authoritative roles granted on the service accounts. | list(string) | | [] | -| *iam_storage_roles* | Storage roles granted to all service accounts, by bucket name. | map(list(string)) | | {} | -| *names* | Names of the service accounts to create. | list(string) | | [] | -| *prefix* | Prefix applied to service account names. | string | | null | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| email | Service account email (for single use). | | -| emails | Service account emails. | | -| emails_list | Service account emails. | | -| iam_email | IAM-format service account email (for single use). | | -| iam_emails | IAM-format service account emails. | | -| iam_emails_list | IAM-format service account emails. | | -| key | Service account key (for single use). | | -| keys | Map of service account keys. | ✓ | -| service_account | Service account resource (for single use). | | -| service_accounts | Service account resources. | | - diff --git a/modules/iam-service-accounts/main.tf b/modules/iam-service-accounts/main.tf deleted file mode 100644 index 67ff8095..00000000 --- a/modules/iam-service-accounts/main.tf +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -locals { - iam_pairs = { - for pair in setproduct(var.names, var.iam_roles) : - "${pair.0}-${pair.1}" => { name = pair.0, role = pair.1 } - } - iam_billing_pairs = flatten([ - for entity, roles in var.iam_billing_roles : [ - for role in roles : [ - for name in var.names : { entity = entity, role = role, name = name } - ] - ] - ]) - iam_folder_pairs = flatten([ - for entity, roles in var.iam_folder_roles : [ - for role in roles : [ - for name in var.names : { entity = entity, role = role, name = name } - ] - ] - ]) - iam_organization_pairs = flatten([ - for entity, roles in var.iam_organization_roles : [ - for role in roles : [ - for name in var.names : { entity = entity, role = role, name = name } - ] - ] - ]) - iam_project_pairs = flatten([ - for entity, roles in var.iam_project_roles : [ - for role in roles : [ - for name in var.names : { entity = entity, role = role, name = name } - ] - ] - ]) - iam_storage_pairs = flatten([ - for entity, roles in var.iam_storage_roles : [ - for role in roles : [ - for name in var.names : { entity = entity, role = role, name = name } - ] - ] - ]) - keys = var.generate_keys ? google_service_account_key.keys : {} - prefix = var.prefix != null ? "${var.prefix}-" : "" - resource = try(google_service_account.service_accounts[var.names[0]], null) - resource_iam_emails = { - for name, resource in google_service_account.service_accounts : - name => "serviceAccount:${resource.email}" - } -} - -resource "google_service_account" "service_accounts" { - for_each = toset(var.names) - project = var.project_id - account_id = "${local.prefix}${lower(each.value)}" - display_name = "Terraform-managed." -} - -resource "google_service_account_key" "keys" { - for_each = var.generate_keys ? toset(var.names) : toset([]) - service_account_id = google_service_account.service_accounts[each.value].email -} - -resource "google_service_account_iam_binding" "sa-roles" { - for_each = local.iam_pairs - service_account_id = google_service_account.service_accounts[each.value.name].name - role = each.value.role - members = lookup(var.iam_members, each.value.role, []) -} - -resource "google_billing_account_iam_member" "roles" { - for_each = { - for pair in local.iam_billing_pairs : - "${pair.name}-${pair.entity}-${pair.role}" => pair - } - billing_account_id = each.value.entity - role = each.value.role - member = local.resource_iam_emails[each.value.name] -} - -resource "google_folder_iam_member" "roles" { - for_each = { - for pair in local.iam_folder_pairs : - "${pair.name}-${pair.entity}-${pair.role}" => pair - } - folder = each.value.entity - role = each.value.role - member = local.resource_iam_emails[each.value.name] -} - -resource "google_organization_iam_member" "roles" { - for_each = { - for pair in local.iam_organization_pairs : - "${pair.name}-${pair.entity}-${pair.role}" => pair - } - org_id = each.value.entity - role = each.value.role - member = local.resource_iam_emails[each.value.name] -} - -resource "google_project_iam_member" "project-roles" { - for_each = { - for pair in local.iam_project_pairs : - "${pair.name}-${pair.entity}-${pair.role}" => pair - } - project = each.value.entity - role = each.value.role - member = local.resource_iam_emails[each.value.name] -} - -resource "google_storage_bucket_iam_member" "bucket-roles" { - for_each = { - for pair in local.iam_storage_pairs : - "${pair.name}-${pair.entity}-${pair.role}" => pair - } - bucket = each.value.entity - role = each.value.role - member = local.resource_iam_emails[each.value.name] -} - -# TODO(ludoo): link from README -# ref: https://cloud.google.com/vpc/docs/shared-vpc diff --git a/modules/iam-service-accounts/outputs.tf b/modules/iam-service-accounts/outputs.tf deleted file mode 100644 index 3673a71f..00000000 --- a/modules/iam-service-accounts/outputs.tf +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -output "service_account" { - description = "Service account resource (for single use)." - value = local.resource -} - -output "service_accounts" { - description = "Service account resources." - value = google_service_account.service_accounts -} - -output "email" { - description = "Service account email (for single use)." - value = try(local.resource.email, null) -} - -output "iam_email" { - description = "IAM-format service account email (for single use)." - value = try("serviceAccount:${local.resource.email}", null) -} - -output "key" { - description = "Service account key (for single use)." - value = try(local.keys[var.names[0]], null) -} - -output "emails" { - description = "Service account emails." - value = { - for name, resource in google_service_account.service_accounts : - name => resource.email - } -} - -output "iam_emails" { - description = "IAM-format service account emails." - value = local.resource_iam_emails -} - -output "emails_list" { - description = "Service account emails." - value = [ - for name, resource in google_service_account.service_accounts : - resource.email - ] -} - -output "iam_emails_list" { - description = "IAM-format service account emails." - value = [ - for name, resource in google_service_account.service_accounts : - "serviceAccount:${resource.email}" - ] -} - -output "keys" { - description = "Map of service account keys." - sensitive = true - value = local.keys -} From d6242957028e76cf1d07652edc7645532a9d4337 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 20 Oct 2020 23:25:50 +0200 Subject: [PATCH 2/2] Fix service account tests --- .../asset-inventory-feed-remediation/main.tf | 4 +- .../main.tf | 4 +- .../gcs-to-bq-with-dataflow/main.tf | 14 ++--- foundations/environments/main.tf | 11 ++-- foundations/environments/outputs.tf | 4 +- networking/hub-and-spoke-peering/main.tf | 8 +-- networking/ilb-next-hop/main.tf | 4 +- networking/ilb-next-hop/vms.tf | 4 +- networking/onprem-google-access-dns/main.tf | 8 +-- networking/shared-vpc-gke/main.tf | 4 +- tests/foundations/environments/test_plan.py | 26 +++++---- .../__init__.py | 0 .../fixture/main.tf | 6 +- .../fixture/variables.tf | 2 +- .../modules/iam_service_account/test_plan.py | 56 +++++++++++++++++++ .../modules/iam_service_accounts/test_plan.py | 51 ----------------- 16 files changed, 108 insertions(+), 98 deletions(-) rename tests/modules/{iam_service_accounts => iam_service_account}/__init__.py (100%) rename tests/modules/{iam_service_accounts => iam_service_account}/fixture/main.tf (90%) rename tests/modules/{iam_service_accounts => iam_service_account}/fixture/variables.tf (97%) create mode 100644 tests/modules/iam_service_account/test_plan.py delete mode 100644 tests/modules/iam_service_accounts/test_plan.py diff --git a/cloud-operations/asset-inventory-feed-remediation/main.tf b/cloud-operations/asset-inventory-feed-remediation/main.tf index 0d6b29ef..da256576 100644 --- a/cloud-operations/asset-inventory-feed-remediation/main.tf +++ b/cloud-operations/asset-inventory-feed-remediation/main.tf @@ -75,9 +75,9 @@ module "pubsub" { } module "service-account" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project.project_id - names = ["${var.name}-cf"] + name = "${var.name}-cf" # iam_project_roles = { (module.project.project_id) = [local.role_id] } } diff --git a/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/cloud-operations/scheduled-asset-inventory-export-bq/main.tf index f1f07aea..479afe71 100644 --- a/cloud-operations/scheduled-asset-inventory-export-bq/main.tf +++ b/cloud-operations/scheduled-asset-inventory-export-bq/main.tf @@ -35,9 +35,9 @@ module "project" { } module "service-account" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project.project_id - names = ["${var.name}-cf"] + name = "${var.name}-cf" iam_project_roles = { (var.project_id) = ["roles/cloudasset.viewer"] } diff --git a/data-solutions/gcs-to-bq-with-dataflow/main.tf b/data-solutions/gcs-to-bq-with-dataflow/main.tf index 39a9ffd3..57ec8e2a 100644 --- a/data-solutions/gcs-to-bq-with-dataflow/main.tf +++ b/data-solutions/gcs-to-bq-with-dataflow/main.tf @@ -57,9 +57,9 @@ module "project-kms" { ############################################################################### module "service-account-bq" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project-service.project_id - names = ["bq-test"] + name = "bq-test" iam_project_roles = { (var.project_service_name) = [ "roles/logging.logWriter", @@ -70,9 +70,9 @@ module "service-account-bq" { } module "service-account-gce" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project-service.project_id - names = ["gce-test"] + name = "gce-test" iam_project_roles = { (var.project_service_name) = [ "roles/logging.logWriter", @@ -86,9 +86,9 @@ module "service-account-gce" { } module "service-account-df" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project-service.project_id - names = ["df-test"] + name = "df-test" iam_project_roles = { (var.project_service_name) = [ "roles/dataflow.worker", @@ -301,7 +301,7 @@ module "bigquery-dataset" { owner = { role = "OWNER", type = "user_by_email" } } access_identities = { - owner = module.service-account-bq.email + owner = module.service-account-bq.email } encryption_key = module.kms.keys.key-bq.self_link tables = { diff --git a/foundations/environments/main.tf b/foundations/environments/main.tf index 34ad8d08..3f839a3c 100644 --- a/foundations/environments/main.tf +++ b/foundations/environments/main.tf @@ -33,9 +33,10 @@ module "tf-project" { # per-environment service accounts module "tf-service-accounts" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.tf-project.project_id - names = var.environments + for_each = var.environments + name = each.value prefix = var.prefix iam_billing_roles = { (var.billing_account_id) = ( @@ -49,7 +50,7 @@ module "tf-service-accounts" { var.iam_xpn_config.grant ? local.sa_xpn_org_roles : [] ) } - generate_keys = var.service_account_keys + generate_key = var.service_account_keys } # bootstrap Terraform state GCS bucket @@ -75,7 +76,7 @@ module "tf-gcs-environments" { } iam_members = { for name in var.environments : (name) => { - "roles/storage.objectAdmin" = [module.tf-service-accounts.iam_emails[name]] + "roles/storage.objectAdmin" = [module.tf-service-accounts[name].iam_email] } } } @@ -92,7 +93,7 @@ module "environment-folders" { iam_roles = local.folder_roles iam_members = { for role in local.folder_roles : - (role) => [module.tf-service-accounts.iam_emails[each.value]] + (role) => [module.tf-service-accounts[each.value].iam_email] } } diff --git a/foundations/environments/outputs.tf b/foundations/environments/outputs.tf index fee9506f..c5315d50 100644 --- a/foundations/environments/outputs.tf +++ b/foundations/environments/outputs.tf @@ -35,12 +35,12 @@ output "environment_tf_gcs_buckets" { output "environment_service_account_keys" { description = "Service account keys used to run each environment Terraform modules." sensitive = true - value = module.tf-service-accounts.keys + value = { for env, sa in module.tf-service-accounts : env => sa.key } } output "environment_service_accounts" { description = "Service accounts used to run each environment Terraform modules." - value = module.tf-service-accounts.emails + value = { for env, sa in module.tf-service-accounts : env => sa.email } } output "audit_logs_bq_dataset" { diff --git a/networking/hub-and-spoke-peering/main.tf b/networking/hub-and-spoke-peering/main.tf index f77d49f6..6b39929a 100644 --- a/networking/hub-and-spoke-peering/main.tf +++ b/networking/hub-and-spoke-peering/main.tf @@ -180,9 +180,9 @@ module "vm-spoke-2" { } module "service-account-gce" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = var.project_id - names = ["gce-test"] + name = "gce-test" iam_project_roles = { (var.project_id) = [ "roles/container.developer", @@ -232,9 +232,9 @@ module "cluster-1-nodepool-1" { # project level, with no risk of conflicts with pre-existing roles module "service-account-gke-node" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = var.project_id - names = ["gke-node"] + name = "gke-node" iam_project_roles = { (var.project_id) = [ "roles/logging.logWriter", "roles/monitoring.metricWriter", diff --git a/networking/ilb-next-hop/main.tf b/networking/ilb-next-hop/main.tf index b5ca75d2..a630ec97 100644 --- a/networking/ilb-next-hop/main.tf +++ b/networking/ilb-next-hop/main.tf @@ -37,9 +37,9 @@ module "project" { } module "service-accounts" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project.project_id - names = ["${local.prefix}gce-vm"] + name = "${local.prefix}gce-vm" iam_project_roles = { (var.project_id) = [ "roles/logging.logWriter", diff --git a/networking/ilb-next-hop/vms.tf b/networking/ilb-next-hop/vms.tf index d3fbc0f8..c80226aa 100644 --- a/networking/ilb-next-hop/vms.tf +++ b/networking/ilb-next-hop/vms.tf @@ -42,7 +42,7 @@ module "vm-left" { startup-script = local.vm_startup_script } service_account = try( - module.service-accounts.emails["${local.prefix}gce-vm"], null + module.service-accounts.email, null ) service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] instance_count = 2 @@ -68,7 +68,7 @@ module "vm-right" { startup-script = local.vm_startup_script } service_account = try( - module.service-accounts.emails["${local.prefix}gce-vm"], null + module.service-accounts.email, null ) service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] instance_count = 2 diff --git a/networking/onprem-google-access-dns/main.tf b/networking/onprem-google-access-dns/main.tf index 2d803673..2c24d72e 100644 --- a/networking/onprem-google-access-dns/main.tf +++ b/networking/onprem-google-access-dns/main.tf @@ -170,9 +170,9 @@ resource "google_dns_policy" "inbound" { ################################################################################ module "service-account-gce" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = var.project_id - names = ["gce-test"] + name = "gce-test" iam_project_roles = { (var.project_id) = [ "roles/logging.logWriter", @@ -222,9 +222,9 @@ module "config-onprem" { } module "service-account-onprem" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = var.project_id - names = ["gce-onprem"] + name = "gce-onprem" iam_project_roles = { (var.project_id) = [ "roles/compute.viewer", diff --git a/networking/shared-vpc-gke/main.tf b/networking/shared-vpc-gke/main.tf index 2e0296e2..ce46aba7 100644 --- a/networking/shared-vpc-gke/main.tf +++ b/networking/shared-vpc-gke/main.tf @@ -240,7 +240,7 @@ module "cluster-1-nodepool-1" { # project level, with no risk of conflicts with pre-existing roles module "service-account-gke-node" { - source = "../../modules/iam-service-accounts" + source = "../../modules/iam-service-account" project_id = module.project-svc-gke.project_id - names = ["gke-node"] + name = "gke-node" } diff --git a/tests/foundations/environments/test_plan.py b/tests/foundations/environments/test_plan.py index 5bdc18b1..a2c7c6a3 100644 --- a/tests/foundations/environments/test_plan.py +++ b/tests/foundations/environments/test_plan.py @@ -23,7 +23,7 @@ FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') def test_folder_roles(plan_runner): "Test folder roles." _, modules = plan_runner(FIXTURES_DIR, is_module=False) - for env in ["test", "prod"]: + for env in ['test', 'prod']: resources = modules[f'module.test.module.environment-folders["{env}"]'] folders = [r for r in resources if r['type'] == 'google_folder'] assert len(folders) == 1 @@ -42,13 +42,17 @@ def test_org_roles(plan_runner): 'iam_xpn_config': '{grant = true, target_org = true}' } _, modules = plan_runner(FIXTURES_DIR, is_module=False, **vars) - resources = (modules['module.test.module.environment-folders["test"]'] + - modules['module.test.module.environment-folders["prod"]']) - folder_bindings = [r['index'] - for r in resources if r['type'] == 'google_folder_iam_binding'] - assert len(folder_bindings) == 8 - resources = modules['module.test.module.tf-service-accounts'] - org_bindings = [r['index'].split('-') - for r in resources if r['type'] == 'google_organization_iam_member'] - assert len(org_bindings) == 4 - assert {b[0] for b in org_bindings} == {'prod', 'test'} + for env in ['test', 'prod']: + resources = modules[f'module.test.module.environment-folders["{env}"]'] + folder_bindings = [r['index'] + for r in resources if r['type'] == 'google_folder_iam_binding'] + assert len(folder_bindings) == 4 + + resources = modules[f'module.test.module.tf-service-accounts["{env}"]'] + org_bindings = [r for r in resources + if r['type'] == 'google_organization_iam_member'] + assert len(org_bindings) == 2 + assert {b['values']['role'] for b in org_bindings} == { + 'roles/resourcemanager.organizationViewer', + 'roles/compute.xpnAdmin' + } diff --git a/tests/modules/iam_service_accounts/__init__.py b/tests/modules/iam_service_account/__init__.py similarity index 100% rename from tests/modules/iam_service_accounts/__init__.py rename to tests/modules/iam_service_account/__init__.py diff --git a/tests/modules/iam_service_accounts/fixture/main.tf b/tests/modules/iam_service_account/fixture/main.tf similarity index 90% rename from tests/modules/iam_service_accounts/fixture/main.tf rename to tests/modules/iam_service_account/fixture/main.tf index 69188086..5f70b175 100644 --- a/tests/modules/iam_service_accounts/fixture/main.tf +++ b/tests/modules/iam_service_account/fixture/main.tf @@ -15,11 +15,11 @@ */ module "test" { - source = "../../../../modules/iam-service-accounts" + source = "../../../../modules/iam-service-account" project_id = var.project_id - names = ["sa-one", "sa-two", "sa-three"] + name = "sa-one" prefix = var.prefix - generate_keys = var.generate_keys + generate_key = var.generate_key iam_members = var.iam_members iam_roles = var.iam_roles iam_billing_roles = var.iam_billing_roles diff --git a/tests/modules/iam_service_accounts/fixture/variables.tf b/tests/modules/iam_service_account/fixture/variables.tf similarity index 97% rename from tests/modules/iam_service_accounts/fixture/variables.tf rename to tests/modules/iam_service_account/fixture/variables.tf index 0a784a94..0d5eba5b 100644 --- a/tests/modules/iam_service_accounts/fixture/variables.tf +++ b/tests/modules/iam_service_account/fixture/variables.tf @@ -14,7 +14,7 @@ * limitations under the License. */ -variable "generate_keys" { +variable "generate_key" { type = bool default = false } diff --git a/tests/modules/iam_service_account/test_plan.py b/tests/modules/iam_service_account/test_plan.py new file mode 100644 index 00000000..719bc574 --- /dev/null +++ b/tests/modules/iam_service_account/test_plan.py @@ -0,0 +1,56 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import pytest + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + + +def test_resources(plan_runner): + "Test service account resource." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + resource = resources[0] + assert resource['type'] == 'google_service_account' + assert resource['values']['account_id'] == 'sa-one' + + _, resources = plan_runner(FIXTURES_DIR, prefix='foo') + assert len(resources) == 1 + resource = resources[0] + assert resource['values']['account_id'] == 'foo-sa-one' + + +def test_iam_roles(plan_runner): + "Test iam roles with one member." + variables = dict( + iam_roles='["roles/iam.serviceAccountUser"]', + iam_members=( + '{' + '"roles/iam.serviceAccountUser" = ["user:a@b.com"] ' + '}') + ) + _, resources = plan_runner(FIXTURES_DIR, **variables) + assert len(resources) == 2 + iam_resources = [r for r in resources + if r['type'] != 'google_service_account'] + assert len(iam_resources) == 1 + + iam_resource = iam_resources[0] + assert iam_resource['type'] == 'google_service_account_iam_binding' + assert iam_resource['index'] == 'roles/iam.serviceAccountUser' + assert iam_resource['values']['role'] == 'roles/iam.serviceAccountUser' + assert iam_resource['values']['members'] == ["user:a@b.com"] diff --git a/tests/modules/iam_service_accounts/test_plan.py b/tests/modules/iam_service_accounts/test_plan.py deleted file mode 100644 index dbcb2bef..00000000 --- a/tests/modules/iam_service_accounts/test_plan.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import os -import pytest - - -FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') - - -def test_resources(plan_runner): - "Test service account resource." - _, resources = plan_runner(FIXTURES_DIR) - assert len(resources) == 3 - assert set(r['type'] for r in resources) == set(['google_service_account']) - assert set(r['values']['account_id'] for r in resources) == set([ - 'sa-one', 'sa-two', 'sa-three' - ]) - _, resources = plan_runner(FIXTURES_DIR, prefix='foo') - assert set(r['values']['account_id'] for r in resources) == set([ - 'foo-sa-one', 'foo-sa-two', 'foo-sa-three' - ]) - - -def test_iam_roles(plan_runner): - "Test iam roles with no memmbers." - _, resources = plan_runner(FIXTURES_DIR, - iam_roles='["roles/iam.serviceAccountUser"]') - assert len(resources) == 6 - iam_resources = [r for r in resources if r['type'] - != 'google_service_account'] - assert len(iam_resources) == 3 - assert set(r['type'] for r in iam_resources) == set( - ['google_service_account_iam_binding']) - assert [r['index'] for r in iam_resources] == [ - 'sa-one-roles/iam.serviceAccountUser', - 'sa-three-roles/iam.serviceAccountUser', - 'sa-two-roles/iam.serviceAccountUser', - ]