#!/usr/bin/env python3 # 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 # # https://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. # # /// script # requires-python = ">=3.11" # dependencies = [ # "BeautifulSoup4", # "click", # "requests", # "pyyaml", # ] # /// from collections import Counter from dataclasses import asdict, dataclass from itertools import chain import click import json import requests import yaml from bs4 import BeautifulSoup # BASEDIR = pathlib.Path(__file__).resolve().parents[1] SERVICE_AGENTS_URL = "https://cloud.google.com/iam/docs/service-agents" # old names used by Fabric ALIASES = { 'bigquery-encryption': ['bq'], 'cloudservices': ['cloudsvc'], 'compute-system': ['compute'], 'cloudcomposer-accounts': ['composer'], 'container-engine-robot': ['container', 'container-engine'], 'dataflow-service-producer-prod': ['dataflow'], 'dataproc-accounts': ['dataproc'], 'gae-api-prod': ['gae-flex'], 'gcf-admin-robot': ['cloudfunctions', 'gcf'], 'gkehub': ['fleet'], 'gs-project-accounts': ['storage'], 'monitoring-notification': ['monitoring'], 'serverless-robot-prod': ['cloudrun', 'run'], } IGNORED_AGENTS = [] # SKIP_IAM_AGENTS defines the GLOBAL/STATIC skip list. # These service agents are known to be created lazily by GCP and will ALWAYS # fail on API enablement if Fabric tries to grant default roles automatically. # Running this script marks them with `skip_iam: true` in `service-agents.yaml`. SKIP_IAM_AGENTS = [ 'service-PROJECT_NUMBER@gcp-sa-apigateway-mgmt.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-apigateway.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-bigqueryspark.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-bigquerytardis.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-connectedsheets.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-firebase.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-krmapihosting-dataplane.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-krmapihosting.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-logging.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-networkactions.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-prod-bigqueryomni.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-scc-notification.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-securitycenter.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-ns-authz.iam.gserviceaccount.com', 'service-PROJECT_NUMBER@gcp-sa-agentgateway.iam.gserviceaccount.com', ] AGENT_NAME_OVERRIDE = { # special case for Cloud Build that has two service agents: # - %s@cloudbuild.gserviceaccount.com # - service-%s@gcp-sa-cloudbuild.iam.gserviceaccount.com 'PROJECT_NUMBER@cloudbuild.gserviceaccount.com': 'cloudbuild-sa', } E2E_SERVICES = [ "alloydb.googleapis.com", "analyticshub.googleapis.com", "apigee.googleapis.com", "artifactregistry.googleapis.com", "assuredworkloads.googleapis.com", "bigquery.googleapis.com", "cloudbuild.googleapis.com", "cloudfunctions.googleapis.com", "cloudkms.googleapis.com", "cloudresourcemanager.googleapis.com", "compute.googleapis.com", "container.googleapis.com", "dataform.googleapis.com", "dataplex.googleapis.com", "dataproc.googleapis.com", "dns.googleapis.com", "eventarc.googleapis.com", "iam.googleapis.com", "iap.googleapis.com", "logging.googleapis.com", "looker.googleapis.com", "monitoring.googleapis.com", "networkconnectivity.googleapis.com", "pubsub.googleapis.com", "run.googleapis.com", "secretmanager.googleapis.com", "servicenetworking.googleapis.com", "serviceusage.googleapis.com", "sqladmin.googleapis.com", "stackdriver.googleapis.com", "storage-component.googleapis.com", "storage.googleapis.com", "vpcaccess.googleapis.com", ] PRIMARY_OVERRIDE = { 'storage-transfer-service': True, } @dataclass class Agent: name: str display_name: str api: str identity: str role: str 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() @click.option('--e2e', is_flag=True, default=False) @click.option('--organization', 'mode', flag_value='organization', default=False, help='Extract organization-level service agents') @click.option('--folder', 'mode', flag_value='folder', default=False, help='Extract folder-level service agents') @click.option('--project', 'mode', flag_value='project', default=False, help='Extract project-level service agents') def main(mode, e2e=False): page = requests.get(SERVICE_AGENTS_URL).content soup = BeautifulSoup(page, 'html.parser') agents = [] for content in soup.find(id='service-agents').select('tbody tr'): agent_text = content.get_text() col1, col2 = content.find_all('td') # Extract all identities from col1 (could be in a single
or multiple in a