diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index c4247b328..fea4c820a 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -196,8 +196,8 @@ Interpolations leverage contexts from two separate sources: an internal set for The following table lists the available context interpolations. External contexts are passed in via the `factories_config.contexts` variable. IAM principals are interpolated in all IAM attributes except `iam_by_principal`. First two columns show for which attribute of which resource context is interpolated. `external contexts` column show in which map passed as `var.factories_config.context` key will be looked up. -* Internally created folders creates keys under `${folder_name_1}[/${folder_name_2}/${folder_name_3}]` -* IAM principals are resolved within context of managed project or use `${project}/${service_account}` to refer service account from other projects managed by the same project factory instance. +- Internally created folders creates keys under `${folder_name_1}[/${folder_name_2}/${folder_name_3}]` +- IAM principals are resolved within context of managed project or use `${project}/${service_account}` to refer service account from other projects managed by the same project factory instance. | resource | attribute | external contexts | internal contexts | |---------------------|-----------------|---------------------|------------------------------------| @@ -400,9 +400,9 @@ services: - storage.googleapis.com iam: "roles/owner": - - rw + - automation/rw "roles/viewer": - - ro + - automation/ro shared_vpc_host_config: enabled: true automation: @@ -416,12 +416,12 @@ automation: description: Team B app 0 Terraform state bucket. iam: roles/storage.objectCreator: - - rw + - automation/rw roles/storage.objectViewer: - gcp-devops - group:team-b-admins@example.org - - rw - - ro + - automation/rw + - automation/ro # tftest-file id=7 path=data/projects/dev-tb-app0-0.yaml schema=project.schema.json ``` diff --git a/modules/project-factory/automation.tf b/modules/project-factory/automation.tf index 8ddbe2a07..666859e54 100644 --- a/modules/project-factory/automation.tf +++ b/modules/project-factory/automation.tf @@ -35,7 +35,7 @@ locals { ] ]) } -output "foo" { value = local.automation_buckets } + module "automation-bucket" { source = "../gcs" for_each = local.automation_buckets @@ -58,9 +58,20 @@ module "automation-bucket" { for k, v in lookup(each.value, "iam_bindings", {}) : k => merge(v, { members = [ for vv in v.members : try( + # rw (infer local project and automation prefix) + module.automation-service-accounts["${each.key}/automation/${vv}"].iam_email, + # automation/rw or sa (infer local project) module.automation-service-accounts["${each.key}/${vv}"].iam_email, + # project/automation/rw project/sa var.factories_config.context.iam_principals[vv], - vv + # fully specified principal + vv, + # passthrough + error handling using tonumber until Terraform gets fail/raise function + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in automation bucket '${each.key}'") + ) ) ] }) @@ -94,7 +105,7 @@ module "automation-bucket" { module "automation-service-accounts" { source = "../iam-service-account" for_each = { - for k in local.automation_sa : "${k.project}/${k.name}" => k + for k in local.automation_sa : "${k.project}/automation/${k.name}" => k } # we cannot use interpolation here as we would get a cycle # from the IAM dependency in the outputs of the main project @@ -107,6 +118,7 @@ module "automation-service-accounts" { "display_name", "Service account ${each.value.name} for ${each.value.project}." ) + # TODO: also support short form for service accounts in this project iam = { for k, v in lookup(each.value, "iam", {}) : k => [ for vv in v : lookup( diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf index fd338b303..70d09b2aa 100644 --- a/modules/project-factory/main.tf +++ b/modules/project-factory/main.tf @@ -106,16 +106,22 @@ module "projects-iam" { iam = { for k, v in lookup(each.value, "iam", {}) : k => [ for vv in v : try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.key}/${vv}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.key}/automation/${vv}"], + # automation service account (automation/rw) local.context.iam_principals["${each.key}/${vv}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[vv].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[vv], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(vv, ":") ? vv : tonumber("[Error] Invalid member: '${vv}' in project '${each.key}'") + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in project '${each.key}'") + ) ) ] } @@ -123,16 +129,22 @@ module "projects-iam" { for k, v in lookup(each.value, "iam_bindings", {}) : k => merge(v, { members = [ for vv in v.members : try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.key}/${vv}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.key}/automation/${vv}"], + # automation service account (automation/rw) local.context.iam_principals["${each.key}/${vv}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[vv].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[vv], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(vv, ":") ? vv : tonumber("[Error] Invalid member: '${vv}' in project '${each.key}'") + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in project '${each.key}'") + ) ) ] }) @@ -140,16 +152,22 @@ module "projects-iam" { iam_bindings_additive = { for k, v in lookup(each.value, "iam_bindings_additive", {}) : k => merge(v, { member = try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.key}/${v.member}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.key}/automation/${v.member}"], + # automation service account (automation/rw) local.context.iam_principals["${each.key}/${v.member}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[v.member].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[v.member], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(v.member, ":") ? v.member : tonumber("[Error] Invalid member: '${v.member}' in project '${each.key}'") + ( + strcontains(v.member, ":") + ? v.member + : tonumber("[Error] Invalid member: '${v.member}' in project '${each.key}'") + ) ) }) } @@ -164,32 +182,38 @@ module "projects-iam" { try(each.value.shared_vpc_service_config.host_project, null) == null ? null : merge(each.value.shared_vpc_service_config, { - host_project = lookup( - var.factories_config.context.vpc_host_projects, - each.value.shared_vpc_service_config.host_project, + host_project = try( + var.factories_config.context.vpc_host_projects[each.value.shared_vpc_service_config.host_project], + module.projects[each.value.shared_vpc_service_config.host_project].project_id, each.value.shared_vpc_service_config.host_project ) network_users = [ - for v in try(each.value.shared_vpc_service_config.network_users, []) : + for vv in try(each.value.shared_vpc_service_config.network_users, []) : try( - # project service accounts - module.service-accounts["${each.key}/${v}"].iam_email, - # automation service account - local.context.iam_principals["${each.key}/${v}"], - # other projects service accounts - module.service-accounts[v].iam_email, - # other automation service account - local.context.iam_principals[v], + # project service accounts (sa) + module.service-accounts["${each.key}/${vv}"].iam_email, + # automation service account (rw) + local.context.iam_principals["${each.key}/automation/${vv}"], + # automation service account (automation/rw) + local.context.iam_principals["${each.key}/${vv}"], + # other projects service accounts (project/sa) + module.service-accounts[vv].iam_email, + # other automation service account (project/automation/rw) + local.context.iam_principals[vv], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(v, ":") ? v : tonumber("[Error] Invalid member: '${v}' in project '${each.key}'") + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in project '${each.key}'") + ) ) ] - # TODO: network subnet users }) ) # add service agents config, so Service Agents can be referred in the IAM grants service_agents_config = { - grant_default_roles = false # Default roles are granted in module.project + # default roles are granted in module.project + grant_default_roles = false } } @@ -205,16 +229,22 @@ module "buckets" { iam = { for k, v in each.value.iam : k => [ for vv in v : try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.value.project}/${vv}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.value.project}/automation/${vv}"], + # automation service account (automation/rw) local.context.iam_principals["${each.value.project}/${vv}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[vv].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[vv], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(vv, ":") ? vv : tonumber("[Error] Invalid member: '${vv}' for bucket '${each.key}' in project '${each.value.project}'") + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in project '${each.value.project}'") + ) ) ] } @@ -222,16 +252,22 @@ module "buckets" { for k, v in each.value.iam_bindings : k => merge(v, { members = [ for vv in v.members : try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.value.project}/${vv}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.value.project}/automation/${vv}"], + # automation service account (automation/rw) local.context.iam_principals["${each.value.project}/${vv}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[vv].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[vv], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(vv, ":") ? vv : tonumber("[Error] Invalid member: '${vv}' for bucket '${each.key}' in project '${each.value.project}'") + ( + strcontains(vv, ":") + ? vv + : tonumber("[Error] Invalid member: '${vv}' in project '${each.value.project}'") + ) ) ] }) @@ -239,16 +275,22 @@ module "buckets" { iam_bindings_additive = { for k, v in each.value.iam_bindings_additive : k => merge(v, { member = try( - # project service accounts + # project service accounts (sa) module.service-accounts["${each.value.project}/${v.member}"].iam_email, - # automation service account + # automation service account (rw) + local.context.iam_principals["${each.value.project}/automation/${v.member}"], + # automation service account (automation/rw) local.context.iam_principals["${each.value.project}/${v.member}"], - # other projects service accounts + # other projects service accounts (project/sa) module.service-accounts[v.member].iam_email, - # other automation service account + # other automation service account (project/automation/rw) local.context.iam_principals[v.member], # passthrough + error handling using tonumber until Terraform gets fail/raise function - strcontains(v.member, ":") ? v.member : tonumber("[Error] Invalid member: '${v.member}' for bucket '${each.key}' in project '${each.value.project}'") + ( + strcontains(v.member, ":") + ? v.member + : tonumber("[Error] Invalid member: '${v.member}' in project '${each.value.project}'") + ) ) }) } diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml index b1dc655f0..8152997e0 100644 --- a/tests/modules/project_factory/examples/example.yaml +++ b/tests/modules/project_factory/examples/example.yaml @@ -54,8 +54,8 @@ values: - serviceAccount:test-pf-dev-tb-app0-0-ro@test-pf-teams-iac-0.iam.gserviceaccount.com - serviceAccount:test-pf-dev-tb-app0-0-rw@test-pf-teams-iac-0.iam.gserviceaccount.com role: roles/storage.objectViewer - module.project-factory.module.automation-service-accounts["dev-tb-app0-0/ro"].google_service_account.service_account[0]: - account_id: test-pf-dev-tb-app0-0-ro + ? module.project-factory.module.automation-service-accounts["dev-tb-app0-0/automation/ro"].google_service_account.service_account[0] + : account_id: test-pf-dev-tb-app0-0-ro create_ignore_already_exists: null description: Team B app 0 read-only automation sa. disabled: false @@ -64,8 +64,8 @@ values: member: serviceAccount:test-pf-dev-tb-app0-0-ro@test-pf-teams-iac-0.iam.gserviceaccount.com project: test-pf-teams-iac-0 timeouts: null - module.project-factory.module.automation-service-accounts["dev-tb-app0-0/rw"].google_service_account.service_account[0]: - account_id: test-pf-dev-tb-app0-0-rw + ? module.project-factory.module.automation-service-accounts["dev-tb-app0-0/automation/rw"].google_service_account.service_account[0] + : account_id: test-pf-dev-tb-app0-0-rw create_ignore_already_exists: null description: Team B app 0 read/write automation sa. disabled: false @@ -257,6 +257,26 @@ values: : project: test-pf-dev-ta-app0-be service: container.googleapis.com timeouts: null + module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_key.default["my-tag-key-1"]: + description: Managed by the Terraform project-factory module. + parent: projects/test-pf-dev-ta-app0-be + purpose: null + purpose_data: null + short_name: my-tag-key-1 + timeouts: null + module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_value.default["my-tag-key-1/my-value-1"]: + description: My value 1 + short_name: my-value-1 + timeouts: null + module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_value.default["my-tag-key-1/my-value-2"]: + description: My value 3 + short_name: my-value-2 + timeouts: null + ? module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_value_iam_binding.default["my-tag-key-1/my-value-2:roles/resourcemanager.tagUser"] + : condition: [] + members: + - user:user@example.com + role: roles/resourcemanager.tagUser module.project-factory.module.projects["dev-tb-app0-0"].data.google_storage_project_service_account.gcs_sa[0]: project: test-pf-dev-tb-app0-0 user_project: null diff --git a/tests/modules/project_factory/shared_vpc_network_user.yaml b/tests/modules/project_factory/shared_vpc_network_user.yaml index 974b5263d..35418a119 100644 --- a/tests/modules/project_factory/shared_vpc_network_user.yaml +++ b/tests/modules/project_factory/shared_vpc_network_user.yaml @@ -13,7 +13,7 @@ # limitations under the License. values: - module.automation-service-accounts["service1/ro"].google_service_account.service_account[0]: + module.automation-service-accounts["service1/automation/ro"].google_service_account.service_account[0]: account_id: my-prefix-service1-ro create_ignore_already_exists: null description: Service read-only automation sa. @@ -23,7 +23,7 @@ values: member: serviceAccount:my-prefix-service1-ro@service-iac.iam.gserviceaccount.com project: service-iac timeouts: null - module.automation-service-accounts["service1/rw"].google_service_account.service_account[0]: + module.automation-service-accounts["service1/automation/rw"].google_service_account.service_account[0]: account_id: my-prefix-service1-rw create_ignore_already_exists: null description: Service read/write automation sa.