From 1879c8f5c6837479965e6c7e329a57078349f7af Mon Sep 17 00:00:00 2001 From: kyson Date: Mon, 24 Aug 2026 19:12:14 +0800 Subject: [PATCH] fix: allow main manual infrastructure plans --- docs/architecture/overview.md | 2 +- docs/design/environments-and-delivery.md | 2 +- docs/design/infrastructure-and-identity.md | 2 +- infra/modules/github_identity/main.tf | 7 +++++++ infra/modules/github_identity/outputs.tf | 4 ++++ infra/stacks/bootstrap/tests/contracts.tftest.hcl | 7 ++++++- 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index c685cf0..aa2d922 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -12,7 +12,7 @@ Proxy Builder 在 GCP Compute Engine 上运行 sing-box,为每个用户提供 系统定义 `development` 与 `production` 两个环境。每个环境使用独立的 GCP Project、OpenTofu state、WIF Provider、Service Accounts、运行资源、代理密钥和用户集合。当前只有 `development` 在线。 -GitHub Repository Variables 保存 pull-request plan 身份;同名 GitHub Environment 保存 apply/deploy 身份与五项应用 Secrets。本地 `.secrets//` 是权限受限的灾难恢复副本,不进入 Git。 +GitHub Repository Variables 保存 PR 与 main 手动 plan 身份;同名 GitHub Environment 保存 apply/deploy 身份与五项应用 Secrets。本地 `.secrets//` 是权限受限的灾难恢复副本,不进入 Git。 ## 组件与数据流 diff --git a/docs/design/environments-and-delivery.md b/docs/design/environments-and-delivery.md index ba47bf6..2addea7 100644 --- a/docs/design/environments-and-delivery.md +++ b/docs/design/environments-and-delivery.md @@ -44,7 +44,7 @@ Git 分支只标识版本,不隐式选择环境。所有可写工作流必须 ## GitHub 配置 -Repository Variables 为 PR plan 提供非秘密只读接入标识: +Repository Variables 为 PR 与 main 手动 plan 提供非秘密只读接入标识: ```text DEV_GCP_WIF_PROVIDER diff --git a/docs/design/infrastructure-and-identity.md b/docs/design/infrastructure-and-identity.md index 72fc7a6..af2af45 100644 --- a/docs/design/infrastructure-and-identity.md +++ b/docs/design/infrastructure-and-identity.md @@ -140,7 +140,7 @@ startup script 不接收 OpenTofu secret 输入,也不得把应用秘密写入 | `proxy-runtime` | 运行 VM;无 Project 级管理角色,无 Secret Manager 读取权限 | | `subscription-runtime` | 仅读取指定环境的 `proxy-users` 与 `obfs-password` secret | -WIF attribute mapping 必须包含 `repository_id`、`repository_owner_id`、`event_name`、`ref` 和 `sub`。每个环境只有一个 GitHub provider,其 admission condition 使用不可变 repository ID。`github-plan` 只绑定精确的 `repo:/:pull_request` subject;`github-apply` 与 `github-deploy` 只绑定精确的 `repo:/:environment:` subject。fork PR 不获得 WIF,GitHub Environment 审批仍是可写身份的必要条件。 +WIF attribute mapping 必须包含 `repository_id`、`repository_owner_id`、`event_name`、`ref` 和 `sub`。每个环境只有一个 GitHub provider,其 admission condition 使用不可变 repository ID。`github-plan` 绑定精确的 `repo:/:pull_request` 与 `repo:/:ref:refs/heads/main` subject,分别用于 PR 和 main 的手动只读 plan;`github-apply` 与 `github-deploy` 只绑定精确的 `repo:/:environment:` subject。fork PR 不获得 WIF,GitHub Environment 审批仍是可写身份的必要条件。 subscription token 为客户端兼容性保留在 query parameter。platform 必须按当前环境的 subscription service 名创建 `google_logging_project_exclusion`,仅排除 `run.googleapis.com/requests` 的 Cloud Run 自动 request log,避免其 `requestUrl` 持久化 token;应用自身的脱敏结构化日志继续保留。bootstrap 因此启用 Cloud Logging API,并仅向 `github-apply` 授予 `roles/logging.configWriter`。 diff --git a/infra/modules/github_identity/main.tf b/infra/modules/github_identity/main.tf index 44d0684..ad675fa 100644 --- a/infra/modules/github_identity/main.tf +++ b/infra/modules/github_identity/main.tf @@ -2,6 +2,7 @@ locals { name_prefix = var.resource_prefix environment_sub = "repo:${var.github_repository}:environment:${var.environment}" pull_request_sub = "repo:${var.github_repository}:pull_request" + main_ref_sub = "repo:${var.github_repository}:ref:refs/heads/main" required_services = toset([ "artifactregistry.googleapis.com", @@ -165,6 +166,12 @@ resource "google_service_account_iam_member" "plan_wif" { member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/subject/${local.pull_request_sub}" } +resource "google_service_account_iam_member" "plan_main_wif" { + service_account_id = google_service_account.github["plan"].name + role = "roles/iam.workloadIdentityUser" + member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/subject/${local.main_ref_sub}" +} + resource "google_service_account_iam_member" "environment_wif" { for_each = toset(["apply", "deploy"]) diff --git a/infra/modules/github_identity/outputs.tf b/infra/modules/github_identity/outputs.tf index addaca5..648713d 100644 --- a/infra/modules/github_identity/outputs.tf +++ b/infra/modules/github_identity/outputs.tf @@ -28,6 +28,10 @@ output "plan_wif_member" { value = google_service_account_iam_member.plan_wif.member } +output "plan_main_wif_member" { + value = google_service_account_iam_member.plan_main_wif.member +} + output "required_services" { value = local.required_services } diff --git a/infra/stacks/bootstrap/tests/contracts.tftest.hcl b/infra/stacks/bootstrap/tests/contracts.tftest.hcl index 7ab032d..5fc7d53 100644 --- a/infra/stacks/bootstrap/tests/contracts.tftest.hcl +++ b/infra/stacks/bootstrap/tests/contracts.tftest.hcl @@ -55,7 +55,12 @@ run "identity_contract" { assert { condition = module.github_identity.plan_wif_member == "principal://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/mock-pool/subject/repo:${var.github_repository}:pull_request" - error_message = "plan 身份必须只允许当前仓库的 pull_request subject。" + error_message = "plan 身份必须允许当前仓库的 pull_request subject。" + } + + assert { + condition = module.github_identity.plan_main_wif_member == "principal://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/mock-pool/subject/repo:${var.github_repository}:ref:refs/heads/main" + error_message = "plan 身份必须允许当前仓库 main 的手动 plan subject。" } assert {