diff --git a/fast/stages/0-bootstrap/data/custom-roles/dns_zone_binder.yaml b/fast/stages/0-bootstrap/data/custom-roles/dns_zone_binder.yaml new file mode 100644 index 000000000..0a8d96857 --- /dev/null +++ b/fast/stages/0-bootstrap/data/custom-roles/dns_zone_binder.yaml @@ -0,0 +1,19 @@ +# Copyright 2025 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. + +# yaml-language-server: $schema=../../schemas/custom-role.schema.json + +name: dnsZoneBinder +includedPermissions: + - dns.networks.bindPrivateDNSZone \ No newline at end of file diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index df91ca23c..edc99d1c7 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -320,21 +320,21 @@ terraform apply |---|---|:---:|:---:|:---:|:---:| | [automation](variables-fast.tf#L19) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables-fast.tf#L43) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | 0-bootstrap | -| [environments](variables-fast.tf#L74) | Environment names. | map(object({…})) | ✓ | | 0-globals | -| [logging](variables-fast.tf#L121) | Logging configuration for tenants. | object({…}) | ✓ | | 1-tenant-factory | -| [organization](variables-fast.tf#L134) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables-fast.tf#L164) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | -| [custom_roles](variables-fast.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [environments](variables-fast.tf#L75) | Environment names. | map(object({…})) | ✓ | | 0-globals | +| [logging](variables-fast.tf#L122) | Logging configuration for tenants. | object({…}) | ✓ | | 1-tenant-factory | +| [organization](variables-fast.tf#L135) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables-fast.tf#L165) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_roles](variables-fast.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | | [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_addon](variables-addons.tf#L17) | FAST addons configurations for stages 2. Keys are used as short names for the add-on resources. | map(object({…})) | | {} | | | [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | map(object({…})) | | {} | | | [fast_stage_3](variables-stages.tf#L114) | FAST stages 3 configurations. | map(object({…})) | | {} | | -| [groups](variables-fast.tf#L92) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | -| [locations](variables-fast.tf#L108) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | -| [org_policy_tags](variables-fast.tf#L152) | Organization policy tags. | object({…}) | | {} | 0-bootstrap | +| [groups](variables-fast.tf#L93) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | +| [locations](variables-fast.tf#L109) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | +| [org_policy_tags](variables-fast.tf#L153) | Organization policy tags. | object({…}) | | {} | 0-bootstrap | | [outputs_location](variables.tf#L31) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | | [resource_names](variables.tf#L37) | Resource names overrides for specific resources. Stage names are interpolated via `$${name}`. Prefix is always set via code, except where noted in the variable type. | object({…}) | | {} | | -| [root_node](variables-fast.tf#L170) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap | +| [root_node](variables-fast.tf#L171) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap | | [tag_names](variables.tf#L57) | Customized names for resource management tags. | object({…}) | | {} | | | [tags](variables.tf#L71) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | | [top_level_folders](variables-toplevel-folders.tf#L17) | Additional top-level folders. Keys are used for service account and bucket names, values implement the folders module interface with the addition of the 'automation' attribute. | map(object({…})) | | {} | | diff --git a/fast/stages/1-resman/data/stage-2/networking.yaml b/fast/stages/1-resman/data/stage-2/networking.yaml index 08c9216ea..2189316c2 100644 --- a/fast/stages/1-resman/data/stage-2/networking.yaml +++ b/fast/stages/1-resman/data/stage-2/networking.yaml @@ -48,7 +48,8 @@ folder_config: expression: | api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([ 'roles/compute.networkUser', 'roles/composer.sharedVpcAgent', - 'roles/container.hostServiceAgentUser', 'roles/vpcaccess.user' + 'roles/container.hostServiceAgentUser', 'roles/vpcaccess.user', + '${custom_roles.dns_zone_binder}' ]) # example conditional grants for stage 3s iam_bindings_additive: {} diff --git a/fast/stages/1-resman/stage-2.tf b/fast/stages/1-resman/stage-2.tf index afcac67f4..9b58c9f4d 100644 --- a/fast/stages/1-resman/stage-2.tf +++ b/fast/stages/1-resman/stage-2.tf @@ -89,6 +89,7 @@ locals { condition = lookup(vv, "condition", null) == null ? null : { title = vv.condition.title expression = templatestring(vv.condition.expression, { + custom_roles = var.custom_roles organization = var.organization tag_names = var.tag_names tag_root = local.tag_root @@ -105,6 +106,7 @@ locals { condition = lookup(vv, "condition", null) == null ? null : { title = vv.condition.title expression = templatestring(vv.condition.expression, { + custom_roles = var.custom_roles organization = var.organization tag_names = var.tag_names tag_root = local.tag_root @@ -126,6 +128,7 @@ locals { condition = lookup(vv, "condition", null) == null ? null : { title = vv.condition.title expression = templatestring(vv.condition.expression, { + custom_roles = var.custom_roles organization = var.organization tag_names = var.tag_names tag_root = local.tag_root diff --git a/fast/stages/1-resman/stage-3.tf b/fast/stages/1-resman/stage-3.tf index f96945301..b70e79b59 100644 --- a/fast/stages/1-resman/stage-3.tf +++ b/fast/stages/1-resman/stage-3.tf @@ -78,6 +78,7 @@ locals { condition = vv.condition == null ? null : { title = vv.condition.title expression = templatestring(vv.condition.expression, { + custom_roles = var.custom_roles organization = var.organization tag_names = var.tag_names tag_root = local.tag_root @@ -94,6 +95,7 @@ locals { condition = vv.condition == null ? null : { title = vv.condition.title expression = templatestring(vv.condition.expression, { + custom_roles = var.custom_roles organization = var.organization tag_names = var.tag_names tag_root = local.tag_root diff --git a/fast/stages/1-resman/variables-fast.tf b/fast/stages/1-resman/variables-fast.tf index 28e0693d2..9b433eb2c 100644 --- a/fast/stages/1-resman/variables-fast.tf +++ b/fast/stages/1-resman/variables-fast.tf @@ -56,6 +56,7 @@ variable "custom_roles" { description = "Custom roles defined at the org level, in key => id format." type = object({ billing_viewer = string + dns_zone_binder = string kms_key_encryption_admin = string kms_key_viewer = string organization_admin_viewer = string diff --git a/fast/stages/2-project-factory/README.md b/fast/stages/2-project-factory/README.md index 7f763ead8..c218548d3 100644 --- a/fast/stages/2-project-factory/README.md +++ b/fast/stages/2-project-factory/README.md @@ -354,19 +354,20 @@ The approach is not shown here but reasonably easy to implement. The main projec |---|---|:---:|:---:|:---:|:---:| | [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables-fast.tf#L26) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables-fast.tf#L101) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | 0-bootstrap | -| [factories_config](variables.tf#L17) | Configuration for YAML-based factories. | object({…}) | | {} | | -| [folder_ids](variables-fast.tf#L39) | Folders created in the resource management stage. | map(string) | | {} | 1-resman | -| [groups](variables-fast.tf#L47) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | map(string) | | {} | 0-bootstrap | -| [host_project_ids](variables-fast.tf#L56) | Host project for the shared VPC. | map(string) | | {} | 2-networking | -| [kms_keys](variables-fast.tf#L64) | KMS key ids. | map(string) | | {} | 2-security | -| [locations](variables-fast.tf#L72) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | -| [org_policy_tags](variables-fast.tf#L90) | Optional organization policy tag values. | object({…}) | | {} | 0-bootstrap | -| [outputs_location](variables.tf#L42) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [perimeters](variables-fast.tf#L82) | Optional VPC-SC perimeter ids. | map(string) | | {} | 1-vpcsc | -| [service_accounts](variables-fast.tf#L111) | Automation service accounts in name => email format. | map(string) | | {} | 1-resman | -| [stage_name](variables.tf#L48) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | | -| [tag_values](variables-fast.tf#L119) | FAST-managed resource manager tag values. | map(string) | | {} | 1-resman | +| [prefix](variables-fast.tf#L109) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | 0-bootstrap | +| [custom_roles](variables-fast.tf#L39) | Custom roles defined at the org level, in key => id format. | map(string) | | {} | 0-bootstrap | +| [factories_config](variables.tf#L17) | Configuration for YAML-based factories. | object({…}) | | {} | | +| [folder_ids](variables-fast.tf#L47) | Folders created in the resource management stage. | map(string) | | {} | 1-resman | +| [groups](variables-fast.tf#L55) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | map(string) | | {} | 0-bootstrap | +| [host_project_ids](variables-fast.tf#L64) | Host project for the shared VPC. | map(string) | | {} | 2-networking | +| [kms_keys](variables-fast.tf#L72) | KMS key ids. | map(string) | | {} | 2-security | +| [locations](variables-fast.tf#L80) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | +| [org_policy_tags](variables-fast.tf#L98) | Optional organization policy tag values. | object({…}) | | {} | 0-bootstrap | +| [outputs_location](variables.tf#L43) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [perimeters](variables-fast.tf#L90) | Optional VPC-SC perimeter ids. | map(string) | | {} | 1-vpcsc | +| [service_accounts](variables-fast.tf#L119) | Automation service accounts in name => email format. | map(string) | | {} | 1-resman | +| [stage_name](variables.tf#L49) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | | +| [tag_values](variables-fast.tf#L127) | FAST-managed resource manager tag values. | map(string) | | {} | 1-resman | ## Outputs diff --git a/fast/stages/2-project-factory/main.tf b/fast/stages/2-project-factory/main.tf index 3054cfada..f6ea11cf6 100644 --- a/fast/stages/2-project-factory/main.tf +++ b/fast/stages/2-project-factory/main.tf @@ -33,6 +33,9 @@ module "projects" { } factories_config = merge(var.factories_config, { context = { + custom_roles = merge( + var.custom_roles, var.factories_config.context.custom_roles + ) folder_ids = merge( { for k, v in var.folder_ids : k => v if v != null }, var.factories_config.context.folder_ids diff --git a/fast/stages/2-project-factory/variables-fast.tf b/fast/stages/2-project-factory/variables-fast.tf index e114b9a5d..f29bd4a53 100644 --- a/fast/stages/2-project-factory/variables-fast.tf +++ b/fast/stages/2-project-factory/variables-fast.tf @@ -36,6 +36,14 @@ variable "billing_account" { } } +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = map(string) + nullable = false + default = {} +} + variable "folder_ids" { # tfdoc:variable:source 1-resman description = "Folders created in the resource management stage." diff --git a/fast/stages/2-project-factory/variables.tf b/fast/stages/2-project-factory/variables.tf index 76105d217..3c2996caf 100644 --- a/fast/stages/2-project-factory/variables.tf +++ b/fast/stages/2-project-factory/variables.tf @@ -25,6 +25,7 @@ variable "factories_config" { notification_channels = optional(map(any), {}) })) context = optional(object({ + custom_roles = optional(map(string), {}) folder_ids = optional(map(string), {}) kms_keys = optional(map(string), {}) iam_principals = optional(map(string), {}) diff --git a/modules/net-vpc-factory/factory-projects-object.tf b/modules/net-vpc-factory/factory-projects-object.tf index 58920b612..15fa9d7d6 100644 --- a/modules/net-vpc-factory/factory-projects-object.tf +++ b/modules/net-vpc-factory/factory-projects-object.tf @@ -54,21 +54,26 @@ locals { ) service_encryption_key_ids = {} services = [] - shared_vpc_service_config = merge({ - host_project = null - network_users = [] - service_agent_iam = {} - service_agent_subnet_iam = {} - service_iam_grants = [] - network_subnet_users = {} - }, try(local._projects_config.data_defaults.shared_vpc_service_config, { + shared_vpc_service_config = merge( + { host_project = null + iam_bindings_additive = {} network_users = [] service_agent_iam = {} service_agent_subnet_iam = {} service_iam_grants = [] network_subnet_users = {} - }) + }, + try(local._projects_config.data_defaults.shared_vpc_service_config, { + host_project = null + iam_bindings_additive = {} + network_users = [] + service_agent_iam = {} + service_agent_subnet_iam = {} + service_iam_grants = [] + network_subnet_users = {} + } + ) ) storage_location = null tag_bindings = {} @@ -245,6 +250,7 @@ locals { ? merge( { host_project = null + iam_bindings_additive = {} network_users = [] service_agent_iam = {} service_agent_subnet_iam = {} diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index 707a15ad7..b19b98453 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -523,11 +523,11 @@ service_accounts: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factories_config](variables.tf#L131) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | -| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L73) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L92) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | -| [factories_data](variables.tf#L158) | Alternate factory data input allowing to use this module as a library. Merged with local YAML data. | object({…}) | | {} | +| [factories_config](variables.tf#L140) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L82) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | +| [data_overrides](variables.tf#L101) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factories_data](variables.tf#L168) | Alternate factory data input allowing to use this module as a library. Merged with local YAML data. | object({…}) | | {} | ## Outputs diff --git a/modules/project-factory/factory-projects-object.tf b/modules/project-factory/factory-projects-object.tf index 58920b612..15fa9d7d6 100644 --- a/modules/project-factory/factory-projects-object.tf +++ b/modules/project-factory/factory-projects-object.tf @@ -54,21 +54,26 @@ locals { ) service_encryption_key_ids = {} services = [] - shared_vpc_service_config = merge({ - host_project = null - network_users = [] - service_agent_iam = {} - service_agent_subnet_iam = {} - service_iam_grants = [] - network_subnet_users = {} - }, try(local._projects_config.data_defaults.shared_vpc_service_config, { + shared_vpc_service_config = merge( + { host_project = null + iam_bindings_additive = {} network_users = [] service_agent_iam = {} service_agent_subnet_iam = {} service_iam_grants = [] network_subnet_users = {} - }) + }, + try(local._projects_config.data_defaults.shared_vpc_service_config, { + host_project = null + iam_bindings_additive = {} + network_users = [] + service_agent_iam = {} + service_agent_subnet_iam = {} + service_iam_grants = [] + network_subnet_users = {} + } + ) ) storage_location = null tag_bindings = {} @@ -245,6 +250,7 @@ locals { ? merge( { host_project = null + iam_bindings_additive = {} network_users = [] service_agent_iam = {} service_agent_subnet_iam = {} diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf index af32a8362..8976bdb2b 100644 --- a/modules/project-factory/main.tf +++ b/modules/project-factory/main.tf @@ -136,7 +136,8 @@ module "projects-iam" { } } iam = { - for k, v in lookup(each.value, "iam", {}) : k => [ + for k, v in lookup(each.value, "iam", {}) : + lookup(var.factories_config.context.custom_roles, k, k) => [ for vv in v : try( # project service accounts (sa) module.service-accounts["${each.key}/${vv}"].iam_email, @@ -184,6 +185,7 @@ module "projects-iam" { ) ) ] + role = lookup(var.factories_config.context.custom_roles, v.role, v.role) }) } iam_bindings_additive = { @@ -208,6 +210,7 @@ module "projects-iam" { : tonumber("[Error] Invalid member: '${v.member}' in project '${each.key}'") ) ) + role = lookup(var.factories_config.context.custom_roles, v.role, v.role) }) } # IAM by principals would trigger dynamic key errors so we don't interpolate @@ -231,7 +234,9 @@ module "projects-iam" { ? k : tonumber("[Error] Invalid member: '${k}' in project '${each.key}'") ) - ) => v + ) => [ + for vv in v : lookup(var.factories_config.context.custom_roles, vv, vv) + ] } # Shared VPC configuration is done at stage 2, to avoid dependency cycle between project service accounts and # IAM grants done for those service accounts @@ -244,6 +249,31 @@ module "projects-iam" { module.projects[each.value.shared_vpc_service_config.host_project].project_id, each.value.shared_vpc_service_config.host_project ) + iam_bindings_additive = { + for k, v in try(each.value.shared_vpc_service_config.iam_bindings_additive, {}) : k => merge(v, { + member = try( + # project service accounts (sa) + module.service-accounts["${each.key}/${v.member}"].iam_email, + # 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 (project/sa) + module.service-accounts[v.member].iam_email, + # other automation service account (project/automation/rw) + local.context.iam_principals[v.member], + # project's service identities + local.service_agents_email[each.key][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}'") + ) + ) + role = lookup(var.factories_config.context.custom_roles, v.role, v.role) + }) + } network_users = [ for vv in try(each.value.shared_vpc_service_config.network_users, []) : try( diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json index 312dc2799..e2c4620c3 100644 --- a/modules/project-factory/schemas/project.schema.json +++ b/modules/project-factory/schemas/project.schema.json @@ -310,6 +310,9 @@ "host_project": { "type": "string" }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, "network_users": { "type": "array", "items": { @@ -557,7 +560,7 @@ }, "role": { "type": "string", - "pattern": "^roles/" + "pattern": "^[a-zA-Z0-9_/]+$" }, "condition": { "type": "object", diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index 36cb67cda..cceabcfb9 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -41,7 +41,16 @@ variable "data_defaults" { service_encryption_key_ids = optional(map(list(string)), {}) services = optional(list(string), []) shared_vpc_service_config = optional(object({ - host_project = string + host_project = string + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) network_users = optional(list(string), []) service_agent_iam = optional(map(list(string)), {}) service_agent_subnet_iam = optional(map(list(string)), {}) @@ -140,6 +149,7 @@ variable "factories_config" { notification_channels = optional(map(any), {}) })) context = optional(object({ + custom_roles = optional(map(string), {}) folder_ids = optional(map(string), {}) iam_principals = optional(map(string), {}) kms_keys = optional(map(string), {}) diff --git a/modules/project/README.md b/modules/project/README.md index d1c942cdb..9dfcddb7a 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -1728,12 +1728,12 @@ alerts: | [service_encryption_key_ids](variables.tf#L210) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | | [services](variables.tf#L217) | Service APIs to enable. | list(string) | | [] | | [shared_vpc_host_config](variables.tf#L223) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L233) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L261) | Deprecated. Use deletion_policy. | bool | | null | +| [shared_vpc_service_config](variables.tf#L233) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L270) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L81) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L88) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [universe](variables.tf#L273) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L282) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L282) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L291) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs diff --git a/modules/project/shared-vpc.tf b/modules/project/shared-vpc.tf index 1ef425138..d2693c53c 100644 --- a/modules/project/shared-vpc.tf +++ b/modules/project/shared-vpc.tf @@ -129,11 +129,25 @@ resource "google_project_iam_member" "shared_vpc_host_robots" { } resource "google_project_iam_member" "shared_vpc_host_iam" { - for_each = toset(var.shared_vpc_service_config.network_users) - project = var.shared_vpc_service_config.host_project - role = "roles/compute.networkUser" - member = each.value - depends_on = [] + for_each = toset(var.shared_vpc_service_config.network_users) + project = var.shared_vpc_service_config.host_project + role = "roles/compute.networkUser" + member = each.value +} + +resource "google_project_iam_member" "shared_vpc_host_iam_additive" { + for_each = try(var.shared_vpc_service_config.iam_bindings_additive, {}) + project = var.shared_vpc_service_config.host_project + role = each.value.role + member = each.value.member + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } } resource "google_compute_subnetwork_iam_member" "shared_vpc_host_robots" { diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 8a4571a44..3fa80e9fb 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -234,7 +234,16 @@ variable "shared_vpc_service_config" { description = "Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config)." # the list of valid service identities is in service-agents.yaml type = object({ - host_project = string + host_project = string + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) network_users = optional(list(string), []) service_agent_iam = optional(map(list(string)), {}) service_agent_subnet_iam = optional(map(list(string)), {}) diff --git a/tests/fast/stages/s0_bootstrap/cicd.yaml b/tests/fast/stages/s0_bootstrap/cicd.yaml index 1f5d3291f..96cd5a65c 100644 --- a/tests/fast/stages/s0_bootstrap/cicd.yaml +++ b/tests/fast/stages/s0_bootstrap/cicd.yaml @@ -2324,7 +2324,7 @@ counts: google_org_policy_custom_constraint: 1 google_org_policy_policy: 38 google_organization_iam_binding: 26 - google_organization_iam_custom_role: 15 + google_organization_iam_custom_role: 16 google_organization_iam_member: 31 google_project: 3 google_project_iam_audit_config: 1 @@ -2343,11 +2343,12 @@ counts: google_tags_tag_value: 2 local_file: 13 modules: 26 - resources: 294 + resources: 295 outputs: custom_roles: billing_viewer: organizations/123456789012/roles/billingViewer + dns_zone_binder: organizations/123456789012/roles/dnsZoneBinder gcve_network_admin: organizations/123456789012/roles/gcveNetworkAdmin gcve_network_viewer: organizations/123456789012/roles/gcveNetworkViewer kms_key_encryption_admin: organizations/123456789012/roles/kmsKeyEncryptionAdmin diff --git a/tests/fast/stages/s0_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml index daaabb257..9418116f7 100644 --- a/tests/fast/stages/s0_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -1551,7 +1551,7 @@ counts: google_org_policy_custom_constraint: 1 google_org_policy_policy: 38 google_organization_iam_binding: 26 - google_organization_iam_custom_role: 15 + google_organization_iam_custom_role: 16 google_organization_iam_member: 31 google_project: 3 google_project_iam_audit_config: 1 @@ -1570,12 +1570,13 @@ counts: google_tags_tag_value: 2 local_file: 8 modules: 20 - resources: 257 + resources: 258 outputs: cicd_repositories: {} custom_roles: billing_viewer: organizations/123456789012/roles/billingViewer + dns_zone_binder: organizations/123456789012/roles/dnsZoneBinder gcve_network_admin: organizations/123456789012/roles/gcveNetworkAdmin gcve_network_viewer: organizations/123456789012/roles/gcveNetworkViewer kms_key_encryption_admin: organizations/123456789012/roles/kmsKeyEncryptionAdmin diff --git a/tests/fast/stages/s1_resman/simple.tfvars b/tests/fast/stages/s1_resman/simple.tfvars index 2878a175f..b318ff675 100644 --- a/tests/fast/stages/s1_resman/simple.tfvars +++ b/tests/fast/stages/s1_resman/simple.tfvars @@ -133,6 +133,7 @@ automation = { custom_roles = { # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", billing_viewer = "organizations/123456789012/roles/billingViewer" + dns_zone_binder = "organizations/123456789012/roles/dnsZoneBinder" gcve_network_admin = "organizations/123456789012/roles/gcveNetworkAdmin" gcve_network_viewer = "organizations/123456789012/roles/gcveNetworkViewer" kms_key_encryption_admin = "organizations/123456789012/roles/kmsKeyEncryptionAdmin"