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"