From ad912d795a2c40e7fbc4660090f44a0a17f9185f Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 16 Apr 2026 19:35:17 +0200 Subject: [PATCH] Enable creation of organization- and folder-level service agents (#3877) * Enable creation of organization- and folder-level service agents * formatting * Add folder test * Add org tests * linting * more linting * Fix tests --- .vscode/settings.json | 1 + modules/apigee/recipe-apigee-swp/README.md | 2 +- modules/folder/README.md | 30 +++- modules/folder/outputs.tf | 3 + modules/folder/service-agents.tf | 27 ++-- modules/folder/service-agents.yaml | 30 ---- modules/folder/variables.tf | 10 ++ modules/organization/README.md | 46 ++++-- modules/organization/outputs.tf | 3 + modules/organization/service-agents.tf | 28 ++-- modules/organization/service-agents.yaml | 104 ------------- modules/organization/variables.tf | 10 ++ modules/project/service-agents.yaml | 137 ++++++++++++++++-- tests/modules/folder/examples/agents.yaml | 39 +++++ .../modules/organization/examples/agents.yaml | 36 +++++ tools/build_service_agents.py | 27 +++- 16 files changed, 336 insertions(+), 197 deletions(-) create mode 100644 tests/modules/folder/examples/agents.yaml create mode 100644 tests/modules/organization/examples/agents.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index 0e43c4ee5..3640ee323 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "url": "http://json-schema.org/draft-07/schema#" } ], + "files.insertFinalNewline": true, "yaml.schemas": { "modules/organization/schemas/custom-role.schema.json": [ "data/**/custom-roles/**/*yaml" diff --git a/modules/apigee/recipe-apigee-swp/README.md b/modules/apigee/recipe-apigee-swp/README.md index a16513f75..400b97b1a 100644 --- a/modules/apigee/recipe-apigee-swp/README.md +++ b/modules/apigee/recipe-apigee-swp/README.md @@ -54,4 +54,4 @@ module "recipe_apigee_swp" { subnet_proxy_only_ip_cidr_range = "10.16.2.0/24" } } -# tftest modules=10 resources=43 +# tftest modules=10 resources=44 diff --git a/modules/folder/README.md b/modules/folder/README.md index 4c2dde157..bfc5b02e6 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -9,6 +9,7 @@ This module allows the creation and management of folders, including support for - [Assured Workload Folder](#assured-workload-folder) - [Privileged Access Manager (PAM) Entitlements](#privileged-access-manager-pam-entitlements) - [Privileged Access Manager (PAM) Entitlements Factory](#privileged-access-manager-pam-entitlements-factory) +- [Service Agents](#service-agents) - [Organization policies](#organization-policies) - [Organization Policy Factory](#organization-policy-factory) - [Hierarchical Firewall Policy Attachments](#hierarchical-firewall-policy-attachments) @@ -135,7 +136,7 @@ module "folder" { Note that using PAM entitlements requires specific roles to be granted to the users and groups that will be using them. For more information, see the [official documentation](https://cloud.google.com/iam/docs/pam-permissions-and-setup#before-you-begin). -Additionally, the Privileged Access Manager Service Agent must be created and granted the `roles/privilegedaccessmanager.folderServiceAgent` role. The service agent is not created automatically, and you can find the `gcloud` command to create it in the `service_agents` output of this module. For more information on service agents, see the [official documentation](https://cloud.google.com/iam/docs/service-agents). Refer to the [organization module's documentation](../organization/README.md#privileged-access-manager-pam-entitlements) for an example on how to grant the required role. +Additionally, the Privileged Access Manager Service Agent must be created and granted the `roles/privilegedaccessmanager.folderServiceAgent` role. The service agent can be created automatically by adding `privilegedaccessmanager.googleapis.com` to the `services` list in the `service_agents_config` variable. Refer to the [organization module's documentation](../organization/README.md#privileged-access-manager-pam-entitlements) for an example on how to grant the required role. ```hcl module "folder" { @@ -179,6 +180,26 @@ module "folder" { } ``` +## Service Agents + +The module allows managing service agents at the folder level. Service agent creation is triggered by adding them to the `service_agents_config.services` variable. + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + service_agents_config = { + services = [ + "osconfig.googleapis.com", + "privilegedaccessmanager.googleapis.com", + "progressiverollout.googleapis.com" + ] + } +} +# tftest inventory=agents.yaml +``` + ## Organization policies To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -720,7 +741,7 @@ module "folder" { | [pam.tf](./pam.tf) | None | google_privileged_access_manager_entitlement | | [scc-mute-configs.tf](./scc-mute-configs.tf) | Folder-level SCC mute configurations. | google_scc_v2_folder_mute_config | | [scc-sha-custom-modules.tf](./scc-sha-custom-modules.tf) | Folder-level Custom modules with Security Health Analytics. | google_scc_management_folder_security_health_analytics_custom_module | -| [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | | +| [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | google_folder_service_identity | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-logging.tf](./variables-logging.tf) | None | | @@ -760,7 +781,8 @@ module "folder" { | [parent](variables.tf#L272) | Parent in folders/folder_id or organizations/org_id format. | string | | null | | [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L27) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L286) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [service_agents_config](variables.tf#L286) | Service agents configuration. | object({…}) | | {} | +| [tag_bindings](variables.tf#L296) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs @@ -774,5 +796,5 @@ module "folder" { | [organization_policies_ids](outputs.tf#L54) | Map of ORGANIZATION_POLICIES => ID in the folder. | | | [scc_custom_sha_modules_ids](outputs.tf#L59) | Map of SCC CUSTOM SHA MODULES => ID in the folder. | | | [service_agents](outputs.tf#L64) | Identities of all folder-level service agents. | | -| [sink_writer_identities](outputs.tf#L69) | Writer identities created for each sink. | | +| [sink_writer_identities](outputs.tf#L72) | Writer identities created for each sink. | | diff --git a/modules/folder/outputs.tf b/modules/folder/outputs.tf index b1c14d76d..7a2a8e356 100644 --- a/modules/folder/outputs.tf +++ b/modules/folder/outputs.tf @@ -64,6 +64,9 @@ output "scc_custom_sha_modules_ids" { output "service_agents" { description = "Identities of all folder-level service agents." value = local.service_agents + depends_on = [ + google_folder_service_identity.default + ] } output "sink_writer_identities" { diff --git a/modules/folder/service-agents.tf b/modules/folder/service-agents.tf index 90b4a2c8a..177989573 100644 --- a/modules/folder/service-agents.tf +++ b/modules/folder/service-agents.tf @@ -18,22 +18,21 @@ locals { _sa_raw = yamldecode(file("${path.module}/service-agents.yaml")) - _sa0 = { + service_agents = { for agent in local._sa_raw : agent.name => { - create_command = ( - "gcloud beta services identity create --service=${agent.api} --folder=${local.folder_number}" - ) + name = agent.name + api = agent.api display_name = agent.display_name - email = templatestring(agent.identity, { - folder_number = local.folder_number - }) - } - } - service_agents = { - for k, v in local._sa0 : - k => merge(v, { - iam_email = "serviceAccount:${v.email}" - }) + email = templatestring(agent.identity, { folder_number = local.folder_number }) + iam_email = "serviceAccount:${templatestring(agent.identity, { folder_number = local.folder_number })}" + } if contains(var.service_agents_config.services, agent.api) } } + +resource "google_folder_service_identity" "default" { + provider = google-beta + for_each = var.service_agents_config.create_agents ? local.service_agents : {} + folder = local.folder_number + service = each.value.api +} diff --git a/modules/folder/service-agents.yaml b/modules/folder/service-agents.yaml index e1e2da4aa..401972126 100644 --- a/modules/folder/service-agents.yaml +++ b/modules/folder/service-agents.yaml @@ -16,70 +16,40 @@ display_name: Access Approval Service Agent api: accessapproval.googleapis.com identity: service-f${folder_number}@gcp-sa-accessapproval.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: assuredworkloads display_name: Assured Workloads Service Agent api: assuredworkloads.googleapis.com identity: service-folder-${folder_number}@gcp-sa-assuredworkloads.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: audit-manager display_name: Audit Manager Service Agent api: auditmanager.googleapis.com identity: service-folder-${folder_number}@gcp-sa-audit-manager.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: cloudcontrolspartner display_name: Cloud Controls Partner Service Agent api: cloudcontrolspartner.googleapis.com identity: service-folder-${folder_number}@gcp-sa-cloudcontrolspartner.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: logging display_name: Cloud Logging Service Agent api: logging.googleapis.com identity: service-folder-${folder_number}@gcp-sa-logging.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: observability display_name: Cloud Observability Service Account api: observability.googleapis.com identity: service-folder-${folder_number}@gcp-sa-observability.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: osconfig-rollout display_name: Google Cloud OS Config Rollout Service Agent api: osconfig.googleapis.com identity: service-folder-${folder_number}@gcp-sa-osconfig-rollout.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: osconfig display_name: Google Cloud OS Config Service Agent api: osconfig.googleapis.com identity: service-folder-${folder_number}@gcp-sa-osconfig.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: pam display_name: Privileged Access Manager Service Agent api: privilegedaccessmanager.googleapis.com identity: service-folder-${folder_number}@gcp-sa-pam.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - name: progrollout display_name: Progressive Rollout Service Agent api: progressiverollout.googleapis.com identity: service-folder-${folder_number}@gcp-sa-progrollout.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 0f29d1552..8be11070a 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -283,6 +283,16 @@ variable "parent" { } } +variable "service_agents_config" { + description = "Service agents configuration." + type = object({ + services = optional(list(string), []) + create_agents = optional(bool, true) + }) + default = {} + nullable = false +} + variable "tag_bindings" { description = "Tag bindings for this folder, in key => tag value id format." type = map(string) diff --git a/modules/organization/README.md b/modules/organization/README.md index 72882bf5e..2ba7d4f8d 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -18,6 +18,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [Example](#example) - [IAM](#iam) - [Conditional IAM by Principals](#conditional-iam-by-principals) +- [Service Agents](#service-agents) - [Organization Policies](#organization-policies) - [Organization Policy Factory](#organization-policy-factory) - [Organization Policy Custom Constraints](#organization-policy-custom-constraints) @@ -166,6 +167,25 @@ module "org" { # tftest modules=1 resources=2 inventory=iam-bpc.yaml ``` +## Service Agents + +The module allows managing service agents at the organization level. Service agent creation is triggered by adding them to the `service_agents_config.services` variable. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + service_agents_config = { + services = [ + "osconfig.googleapis.com", + "privilegedaccessmanager.googleapis.com", + "progressiverollout.googleapis.com" + ] + } +} +# tftest inventory=agents.yaml +``` + ## Organization Policies ### Organization Policy Factory @@ -269,19 +289,22 @@ custom.dataprocNoMoreThan10Workers: Note that using PAM entitlements requires specific roles to be granted to the users and groups that will be using them. For more information, see the [official documentation](https://cloud.google.com/iam/docs/pam-permissions-and-setup#before-you-begin). -Additionally, the Privileged Access Manager Service Agent must be created and granted the `roles/privilegedaccessmanager.organizationServiceAgent` role. The service agent is not created automatically, and you can find the `gcloud` command to create it in the `service_agents` output of this module. For more information on service agents, see the [official documentation](https://cloud.google.com/iam/docs/service-agents). +Additionally, the Privileged Access Manager Service Agent must be created and granted the `roles/privilegedaccessmanager.organizationServiceAgent` role. The service agent can be created automatically by adding `privilegedaccessmanager.googleapis.com` to the `services` list in the `service_agents_config` variable. -The following example shows how to grant the required role to the PAM service agent: +The following example shows how to create the service agent and grant the required role: ```hcl module "organization" { source = "./fabric/modules/organization" - organization_id = var.org_id + organization_id = var.organization_id factories_config = { pam_entitlements = "factory/" } + service_agents_config = { + services = ["privilegedaccessmanager.googleapis.com"] + } iam = { - "roles/privilegedaccessmanager.serviceAgent" = [ + "roles/privilegedaccessmanager.organizationServiceAgent" = [ module.organization.service_agents.pam.iam_email ] } @@ -991,7 +1014,7 @@ module "org" { | [pam.tf](./pam.tf) | None | google_privileged_access_manager_entitlement | | [scc-mute-configs.tf](./scc-mute-configs.tf) | Organization-level SCC mute configurations. | google_scc_v2_organization_mute_config | | [scc-sha-custom-modules.tf](./scc-sha-custom-modules.tf) | Organization-level Custom modules with Security Health Analytics. | google_scc_management_organization_security_health_analytics_custom_module | -| [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | | +| [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | google_organization_service_identity | | [tags.tf](./tags.tf) | Manages GCP Secure Tags, keys, values, and IAM. | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-identity-providers.tf](./variables-identity-providers.tf) | None | | @@ -1030,6 +1053,7 @@ module "org" { | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | | [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L28) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [service_agents_config](variables.tf#L182) | Service agents configuration. | object({…}) | | {} | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L96) | 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({…})) | | {} | | [tags_config](variables-tags.tf#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | @@ -1053,10 +1077,10 @@ module "org" { | [scc_custom_sha_modules_ids](outputs.tf#L118) | Map of SCC CUSTOM SHA MODULES => ID in the organization. | | | [scc_mute_configs](outputs.tf#L123) | SCC mute configurations. | | | [service_agents](outputs.tf#L128) | Identities of all organization-level service agents. | | -| [sink_writer_identities](outputs.tf#L133) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L141) | Tag key resources. | | -| [tag_values](outputs.tf#L150) | Tag value resources. | | -| [workforce_identity_pool_ids](outputs.tf#L158) | Workforce identity pool ids. | | -| [workforce_identity_provider_names](outputs.tf#L165) | Workforce Identity provider names. | | -| [workforce_identity_providers](outputs.tf#L172) | Workforce Identity provider attributes. | | +| [sink_writer_identities](outputs.tf#L136) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L144) | Tag key resources. | | +| [tag_values](outputs.tf#L153) | Tag value resources. | | +| [workforce_identity_pool_ids](outputs.tf#L161) | Workforce identity pool ids. | | +| [workforce_identity_provider_names](outputs.tf#L168) | Workforce Identity provider names. | | +| [workforce_identity_providers](outputs.tf#L175) | Workforce Identity provider attributes. | | diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index fba1ec910..b8e46acc4 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -128,6 +128,9 @@ output "scc_mute_configs" { output "service_agents" { description = "Identities of all organization-level service agents." value = local.service_agents + depends_on = [ + google_organization_service_identity.default + ] } output "sink_writer_identities" { diff --git a/modules/organization/service-agents.tf b/modules/organization/service-agents.tf index 24bdc9a48..327e5a3d1 100644 --- a/modules/organization/service-agents.tf +++ b/modules/organization/service-agents.tf @@ -18,27 +18,25 @@ locals { _sa_raw = yamldecode(file("${path.module}/service-agents.yaml")) - _sa0 = { + service_agents = { for agent in local._sa_raw : agent.name => { - create_command = ( - "gcloud beta services identity create --service=${agent.api} --organization=${local.organization_id_numeric}" - ) + name = agent.name + api = agent.api display_name = agent.display_name - email = templatestring(agent.identity, { - organization_number = local.organization_id_numeric - }) - - } - } - service_agents = { - for k, v in local._sa0 : - k => merge(v, { - iam_email = "serviceAccount:${v.email}" - }) + email = templatestring(agent.identity, { organization_number = local.organization_id_numeric }) + iam_email = "serviceAccount:${templatestring(agent.identity, { organization_number = local.organization_id_numeric })}" + } if contains(var.service_agents_config.services, agent.api) } service_agents_ctx = { for k, v in local.service_agents : "$service_agents:${k}" => v.iam_email } } + +resource "google_organization_service_identity" "default" { + provider = google-beta + for_each = var.service_agents_config.create_agents ? local.service_agents : {} + organization = local.organization_id_numeric + service = each.value.api +} diff --git a/modules/organization/service-agents.yaml b/modules/organization/service-agents.yaml index d7e91a0ed..fe3923029 100644 --- a/modules/organization/service-agents.yaml +++ b/modules/organization/service-agents.yaml @@ -16,208 +16,104 @@ display_name: Access Approval Service Agent api: accessapproval.googleapis.com identity: service-o${organization_number}@gcp-sa-accessapproval.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: assuredoss display_name: Assured OSS Service Agent api: assuredoss.googleapis.com identity: service-org-${organization_number}@gcp-sa-assuredoss.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: asm-hpsa display_name: Attack Surface Management Service Agent api: securitycenter.googleapis.com identity: service-org-${organization_number}@gcp-sa-asm-hpsa.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: audit-manager display_name: Audit Manager Service Agent api: auditmanager.googleapis.com identity: service-org-${organization_number}@gcp-sa-audit-manager.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: chronicle-soar display_name: Chronicle Soar Service Agent api: chronicle.googleapis.com identity: service-org-${organization_number}@gcp-sa-chronicle-soar.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: effectivepolicy display_name: Cloud Asset Effective Policy Service Agent api: cloudasset.googleapis.com identity: service-org-${organization_number}@gcp-sa-effectivepolicy.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: othercloudcfg display_name: Cloud Asset Other Cloud Config Service Agent api: cloudasset.googleapis.com identity: service-org-${organization_number}@gcp-sa-othercloudcfg.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: cloudkms display_name: Cloud KMS Organization Service Agent api: cloudkms.googleapis.com identity: service-org-${organization_number}@gcp-sa-cloudkms.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: logging display_name: Cloud Logging Service Agent api: logging.googleapis.com identity: service-org-${organization_number}@gcp-sa-logging.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: nss-hpsa display_name: Cloud Notebook Security Scanner Service Agent api: notebooksecurityscanner.googleapis.com identity: service-org-${organization_number}@gcp-sa-nss-hpsa.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: observability display_name: Cloud Observability Service Account api: observability.googleapis.com identity: service-org-${organization_number}@gcp-sa-observability.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: cloudresourcemanager display_name: Cloud Resource Manager Service Agent api: cloudresourcemanager.googleapis.com identity: service-org-${organization_number}@gcp-sa-cloudresourcemanager.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: riskmanager display_name: Cloud Risk Manager Service Agent api: dlp.googleapis.com identity: organizations-${organization_number}@gcp-sa-riskmanager.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: scc-bulk-export display_name: Cloud Security Command Center Bulk Export Service Account api: securitycenter.googleapis.com identity: service-org-${organization_number}@gcp-sa-scc-bulk-export.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: scc-notification display_name: Cloud Security Command Center Notification Service Account api: securitycenter.googleapis.com identity: service-org-${organization_number}@gcp-sa-scc-notification.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: security-center-api display_name: Cloud Security Command Center Service Agent api: securitycenter.googleapis.com identity: service-org-${organization_number}@security-center-api.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: csc-hpsa display_name: Cloud Security Compliance Service Agent api: cloudsecuritycompliance.googleapis.com identity: service-org-${organization_number}@gcp-sa-csc-hpsa.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: ktd-hpsa display_name: Container Threat Detection Service Agent api: containerthreatdetection.googleapis.com identity: service-org-${organization_number}@gcp-sa-ktd-hpsa.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: dataplex-cmek display_name: Dataplex Cmek Service Agent api: dataplex.googleapis.com identity: service-org-${organization_number}@gcp-sa-dataplex-cmek.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: dataplex display_name: Dataplex Service Agent api: dataplex.googleapis.com identity: service-org-${organization_number}@gcp-sa-dataplex.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: osconfig-rollout display_name: Google Cloud OS Config Rollout Service Agent api: osconfig.googleapis.com identity: service-org-${organization_number}@gcp-sa-osconfig-rollout.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: osconfig display_name: Google Cloud OS Config Service Agent api: osconfig.googleapis.com identity: service-org-${organization_number}@gcp-sa-osconfig.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: v1-remediator display_name: Policy Remediator Service Agent (prod) api: policyremediator.googleapis.com identity: service-org-${organization_number}@gcp-sa-v1-remediator.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: pam display_name: Privileged Access Manager Service Agent api: privilegedaccessmanager.googleapis.com identity: service-org-${organization_number}@gcp-sa-pam.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: progrollout display_name: Progressive Rollout Service Agent api: progressiverollout.googleapis.com identity: service-org-${organization_number}@gcp-sa-progrollout.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: sccspanner display_name: SCC CMEK Spanner Service Agent (PROD) api: securitycenter.googleapis.com identity: service-org-${organization_number}@gcp-sa-sccspanner.iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 478dc4469..27181c4f0 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -178,3 +178,13 @@ variable "organization_id" { error_message = "The organization_id must in the form organizations/nnn." } } + +variable "service_agents_config" { + description = "Service agents configuration." + type = object({ + services = optional(list(string), []) + create_agents = optional(bool, true) + }) + default = {} + nullable = false +} diff --git a/modules/project/service-agents.yaml b/modules/project/service-agents.yaml index 25d011987..e13398fe2 100644 --- a/modules/project/service-agents.yaml +++ b/modules/project/service-agents.yaml @@ -44,6 +44,14 @@ is_primary: false aliases: [] skip_iam: false +- name: aiplatform-pie + display_name: AI Platform Private Instance (PIE) Service Agent + api: aiplatform.googleapis.com + identity: service-${project_number}@gcp-sa-aiplatform-pie.${universe_domain}iam.gserviceaccount.com + role: roles/aiplatform.serviceAgent + is_primary: false + aliases: [] + skip_iam: false - name: vertex-eval display_name: AI Platform Rapid Eval Service Agent api: aiplatform.googleapis.com @@ -132,6 +140,14 @@ is_primary: true aliases: [] skip_iam: false +- name: agentgateway + display_name: Agent Gateway Service Account + api: networkservices.googleapis.com + identity: service-${project_number}@gcp-sa-agentgateway.${universe_domain}iam.gserviceaccount.com + role: roles/agentgateway.serviceAgent + is_primary: false + aliases: [] + skip_iam: false - name: alloydb display_name: AlloyDB Service Account api: alloydb.googleapis.com @@ -277,6 +293,14 @@ is_primary: true aliases: [] skip_iam: false +- name: appoptimize + display_name: App Optimize Service Agent + api: appoptimize.googleapis.com + identity: service-${project_number}@gcp-sa-appoptimize.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: true + aliases: [] + skip_iam: false - name: integrations display_name: Application Integration Service Agent api: integrations.googleapis.com @@ -309,6 +333,14 @@ is_primary: true aliases: [] skip_iam: false +- name: autoannotate + display_name: Auto Annotate Service Account + api: storage.googleapis.com + identity: service-${project_number}@gcp-sa-autoannotate.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: recommendationengine display_name: AutoML Recommendations Service Account api: recommendationengine.googleapis.com @@ -406,6 +438,14 @@ is_primary: true aliases: [] skip_iam: false +- name: bqms + display_name: BigQuery Migration Service Agent + api: bigquerymigration.googleapis.com + identity: service-${project_number}@gcp-sa-bqms.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: true + aliases: [] + skip_iam: false - name: prod-bigqueryomni display_name: BigQuery Omni Service Agent api: bigquery.googleapis.com @@ -446,14 +486,6 @@ is_primary: true aliases: [] skip_iam: false -- name: chronicle-sv - display_name: Chronicle Security Validation Service Account - api: chronicle.googleapis.com - identity: service-${project_number}@gcp-sa-chronicle-sv.${universe_domain}iam.gserviceaccount.com - role: null - is_primary: false - aliases: [] - skip_iam: false - name: chronicle display_name: Chronicle Service Account api: chronicle.googleapis.com @@ -648,14 +680,6 @@ is_primary: false aliases: [] skip_iam: false -- name: lifesciences - display_name: Cloud Life Sciences Service Agent - api: lifesciences.googleapis.com - identity: service-${project_number}@gcp-sa-lifesciences.${universe_domain}iam.gserviceaccount.com - role: roles/lifesciences.serviceAgent - is_primary: true - aliases: [] - skip_iam: false - name: logging display_name: Cloud Logging Service Account api: logging.googleapis.com @@ -864,6 +888,14 @@ is_primary: true aliases: [] skip_iam: false +- name: hypercomputecluster + display_name: Cluster Director Service Agent + api: hypercomputecluster.googleapis.com + identity: service-${project_number}@gcp-sa-hypercomputecluster.${universe_domain}iam.gserviceaccount.com + role: roles/hypercomputecluster.serviceAgent + is_primary: true + aliases: [] + skip_iam: false - name: compute-system display_name: Compute Engine Service Agent api: compute.googleapis.com @@ -985,6 +1017,14 @@ is_primary: true aliases: [] skip_iam: false +- name: ces + display_name: Customer Engagement Suite Service Account + api: ces.googleapis.com + identity: service-${project_number}@gcp-sa-ces.${universe_domain}iam.gserviceaccount.com + role: roles/ces.serviceAgent + is_primary: true + aliases: [] + skip_iam: false - name: dataconnectors display_name: Data Connectors Service Account api: dataconnectors.googleapis.com @@ -1009,6 +1049,14 @@ is_primary: true aliases: [] skip_iam: false +- name: datastudio-cmek + display_name: Data Studio CMEK Service Account + api: datastudio.googleapis.com + identity: service-${project_number}@gcp-sa-datastudio-cmek.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: datastudio display_name: Data Studio Service Account api: datastudio.googleapis.com @@ -1241,6 +1289,14 @@ is_primary: true aliases: [] skip_iam: false +- name: storage-search + display_name: GCS Search Service Account + api: storage.googleapis.com + identity: service-${project_number}@gcp-sa-storage-search.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: gkedataplanev2 display_name: GKE Dataplane V2 Service Account api: gkedataplanev2.googleapis.com @@ -1282,6 +1338,14 @@ is_primary: true aliases: [] skip_iam: false +- name: generativelanguage + display_name: Generative Language Service Agent + api: generativelanguage.googleapis.com + identity: service-${project_number}@gcp-sa-generativelanguage.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: true + aliases: [] + skip_iam: false - name: gkeonprem display_name: Gke On-Prem Service Account api: gkeonprem.googleapis.com @@ -1401,6 +1465,14 @@ aliases: - storage skip_iam: false +- name: diagon + display_name: Hypercompute Diagon Service Account + api: hypercomputecluster.googleapis.com + identity: service-${project_number}@gcp-sa-diagon.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: iap display_name: IAP Service Account api: iap.googleapis.com @@ -1417,6 +1489,14 @@ is_primary: false aliases: [] skip_iam: false +- name: global-spanner + display_name: Infra Spanner Production Service Account + api: spanner.googleapis.com + identity: service-${project_number}@gcp-sa-global-spanner.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: config display_name: Infrastructure Manager Service Account api: config.googleapis.com @@ -1860,6 +1940,14 @@ is_primary: true aliases: [] skip_iam: false +- name: vs-cmek + display_name: Vector Search Cmek Service Account + api: vectorsearch.googleapis.com + identity: service-${project_number}@gcp-sa-vs-cmek.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: vectorsearch display_name: Vector Search Service Account api: vectorsearch.googleapis.com @@ -1868,6 +1956,14 @@ is_primary: true aliases: [] skip_iam: false +- name: vertex-sandbox + display_name: Vertex AI Agent Sandbox Service Agent + api: aiplatform.googleapis.com + identity: service-${project_number}@gcp-sa-vertex-sandbox.${universe_domain}iam.gserviceaccount.com + role: roles/aiplatform.agentSandboxServiceAgent + is_primary: false + aliases: [] + skip_iam: false - name: vertex-shtune display_name: Vertex AI Ancillary Secure Fine Tuning Service Agent api: aiplatform.googleapis.com @@ -1964,6 +2060,14 @@ is_primary: false aliases: [] skip_iam: false +- name: vertex-vtc + display_name: Vertex AI Training Cluster Service Agent + api: aiplatform.googleapis.com + identity: service-${project_number}@gcp-sa-vertex-vtc.${universe_domain}iam.gserviceaccount.com + role: null + is_primary: false + aliases: [] + skip_iam: false - name: vertex-agent display_name: Vertex Agent Service Agent api: aiplatform.googleapis.com @@ -2012,3 +2116,4 @@ is_primary: false aliases: [] skip_iam: false + diff --git a/tests/modules/folder/examples/agents.yaml b/tests/modules/folder/examples/agents.yaml new file mode 100644 index 000000000..bab15407f --- /dev/null +++ b/tests/modules/folder/examples/agents.yaml @@ -0,0 +1,39 @@ +# Copyright 2026 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. + +values: + module.folder.google_folder.folder[0]: + deletion_protection: false + display_name: Folder name + parent: folders/1122334455 + tags: null + timeouts: null + module.folder.google_folder_service_identity.default["osconfig"]: + service: osconfig.googleapis.com + timeouts: null + module.folder.google_folder_service_identity.default["osconfig-rollout"]: + service: osconfig.googleapis.com + timeouts: null + module.folder.google_folder_service_identity.default["pam"]: + service: privilegedaccessmanager.googleapis.com + timeouts: null + module.folder.google_folder_service_identity.default["progrollout"]: + service: progressiverollout.googleapis.com + timeouts: null + +counts: + google_folder: 1 + google_folder_service_identity: 4 + modules: 1 + resources: 5 diff --git a/tests/modules/organization/examples/agents.yaml b/tests/modules/organization/examples/agents.yaml new file mode 100644 index 000000000..0a449e23c --- /dev/null +++ b/tests/modules/organization/examples/agents.yaml @@ -0,0 +1,36 @@ +# Copyright 2026 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. + +values: + module.org.google_organization_service_identity.default["osconfig"]: + organization: '1122334455' + service: osconfig.googleapis.com + timeouts: null + module.org.google_organization_service_identity.default["osconfig-rollout"]: + organization: '1122334455' + service: osconfig.googleapis.com + timeouts: null + module.org.google_organization_service_identity.default["pam"]: + organization: '1122334455' + service: privilegedaccessmanager.googleapis.com + timeouts: null + module.org.google_organization_service_identity.default["progrollout"]: + organization: '1122334455' + service: progressiverollout.googleapis.com + timeouts: null + +counts: + google_organization_service_identity: 4 + modules: 1 + resources: 4 diff --git a/tools/build_service_agents.py b/tools/build_service_agents.py index a032ca98e..4c432e434 100755 --- a/tools/build_service_agents.py +++ b/tools/build_service_agents.py @@ -24,6 +24,7 @@ # ] # /// +from collections import Counter from dataclasses import asdict, dataclass from itertools import chain @@ -133,6 +134,17 @@ class Agent: is_primary: bool aliases: list[str] skip_iam: bool + node_type: str + + def to_dict(self): + d = asdict(self) + d.pop('node_type', None) + if self.node_type in ['organization', 'folder']: + d.pop('is_primary', None) + d.pop('role', None) + d.pop('skip_iam', None) + d.pop('aliases', None) + return d @click.command() @@ -180,6 +192,12 @@ def main(mode, e2e=False): if identity in IGNORED_AGENTS or '-IDENTIFIER' in identity: continue + role = col2.code.get_text() if 'roles/' in agent_text else None + + # Ignore Apigee Core service agent as it shares email with primary agent + if identity == 'service-PROJECT_NUMBER@gcp-sa-apigee.iam.gserviceaccount.com' and role == 'roles/apigee.coreServiceAgent': + continue + if identity in AGENT_NAME_OVERRIDE: name = AGENT_NAME_OVERRIDE[identity] else: @@ -221,6 +239,7 @@ def main(mode, e2e=False): is_primary=PRIMARY_OVERRIDE.get(name, is_primary), aliases=ALIASES.get(name, []), skip_iam=skip_iam, + node_type=mode, ) if mode == 'project' and agent.name == 'cloudservices': @@ -231,7 +250,11 @@ def main(mode, e2e=False): # make sure all names and aliases are different: names = set(agent.name for agent in agents) - assert len(names) == len(agents) + duplicate_names = [ + name for name, count in Counter(agent.name for agent in agents).items() + if count > 1 + ] + assert len(names) == len(agents), f"duplicate names found: {duplicate_names}" aliases = set(chain.from_iterable(agent.aliases for agent in agents)) assert aliases.isdisjoint(names) @@ -244,7 +267,7 @@ def main(mode, e2e=False): header = open(__file__).readlines()[2:15] print("".join(header)) # and print all the agents - print(yaml.safe_dump([asdict(a) for a in agents], sort_keys=False)) + print(yaml.safe_dump([a.to_dict() for a in agents], sort_keys=False)) else: jit_services = {} result = {"locals": {"jit_services": jit_services}}