Allow custom roles in context, add support for shared VPC IAM to project and project factory (#3163)

* add support for custom roles and hos tproject iam to project modules

* align vpc factory
This commit is contained in:
Ludovico Magnocavallo
2025-06-15 10:01:22 +02:00
committed by GitHub
parent 065d79efbf
commit fe0a8128dc
22 changed files with 184 additions and 64 deletions

View File

@@ -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 = {}

File diff suppressed because one or more lines are too long

View File

@@ -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 = {}

View File

@@ -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(

View File

@@ -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",

View File

@@ -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), {})

View File

@@ -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]}. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [services](variables.tf#L217) | Service APIs to enable. | <code>list&#40;string&#41;</code> | | <code>&#91;&#93;</code> |
| [shared_vpc_host_config](variables.tf#L223) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | <code title="object&#40;&#123;&#10; enabled &#61; bool&#10; service_projects &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [shared_vpc_service_config](variables.tf#L233) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; host_project &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [skip_delete](variables.tf#L261) | Deprecated. Use deletion_policy. | <code>bool</code> | | <code>null</code> |
| [shared_vpc_service_config](variables.tf#L233) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | <code title="object&#40;&#123;&#10; host_project &#61; string&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; host_project &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [skip_delete](variables.tf#L270) | Deprecated. Use deletion_policy. | <code>bool</code> | | <code>null</code> |
| [tag_bindings](variables-tags.tf#L81) | Tag bindings for this project, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [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. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string, &#34;Managed by the Terraform project module.&#34;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; id &#61; optional&#40;string&#41;&#10; values &#61; optional&#40;map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string, &#34;Managed by the Terraform project module.&#34;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; id &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [universe](variables.tf#L273) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | <code title="object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vpc_sc](variables.tf#L282) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | <code title="object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [universe](variables.tf#L282) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | <code title="object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vpc_sc](variables.tf#L291) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | <code title="object&#40;&#123;&#10; perimeter_name &#61; string&#10; perimeter_bridges &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs

View File

@@ -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" {

View File

@@ -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)), {})