diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md
index 1a389364..4dec2945 100644
--- a/fast/stages/1-resman/README.md
+++ b/fast/stages/1-resman/README.md
@@ -352,6 +352,7 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [cicd-networking.tf](./cicd-networking.tf) | CI/CD resources for the networking branch. | iam-service-account
· source-repository
| |
| [cicd-project-factory.tf](./cicd-project-factory.tf) | CI/CD resources for the teams branch. | iam-service-account
· source-repository
| |
| [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | iam-service-account
· source-repository
| |
+| [cicd-teams.tf](./cicd-teams.tf) | CI/CD resources for individual teams. | iam-service-account
· source-repository
| |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [organization.tf](./organization.tf) | Organization policies. | organization
| |
| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file
|
@@ -378,22 +379,23 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| [outputs_location](variables.tf#L210) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
| [tag_names](variables.tf#L227) | Customized names for resource management tags. | object({…})
| | {…}
| |
| [tags](variables.tf#L248) | Custome secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | {}
| |
-| [team_folders](variables.tf#L269) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
-| [tenants](variables.tf#L279) | Lightweight tenant definitions. | map(object({…}))
| | {}
| |
-| [tenants_config](variables.tf#L295) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…})
| | {}
| |
+| [team_folders](variables.tf#L269) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
+| [tenants](variables.tf#L285) | Lightweight tenant definitions. | map(object({…}))
| | {}
| |
+| [tenants_config](variables.tf#L301) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…})
| | {}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [cicd_repositories](outputs.tf#L213) | WIF configuration for CI/CD repositories. | | |
-| [dataplatform](outputs.tf#L227) | Data for the Data Platform stage. | | |
-| [gke_multitenant](outputs.tf#L243) | Data for the GKE multitenant stage. | | 03-gke-multitenant
|
-| [networking](outputs.tf#L264) | Data for the networking stage. | | |
-| [project_factories](outputs.tf#L273) | Data for the project factories stage. | | |
-| [providers](outputs.tf#L288) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking
· 02-security
· 03-dataplatform
· xx-sandbox
· xx-teams
|
-| [sandbox](outputs.tf#L295) | Data for the sandbox stage. | | xx-sandbox
|
-| [security](outputs.tf#L309) | Data for the networking stage. | | 02-security
|
-| [teams](outputs.tf#L319) | Data for the teams stage. | | |
-| [tfvars](outputs.tf#L331) | Terraform variable files for the following stages. | ✓ | |
+| [cicd_repositories](outputs.tf#L232) | WIF configuration for CI/CD repositories. | | |
+| [dataplatform](outputs.tf#L246) | Data for the Data Platform stage. | | |
+| [gke_multitenant](outputs.tf#L262) | Data for the GKE multitenant stage. | | 03-gke-multitenant
|
+| [networking](outputs.tf#L283) | Data for the networking stage. | | |
+| [project_factories](outputs.tf#L292) | Data for the project factories stage. | | |
+| [providers](outputs.tf#L307) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking
· 02-security
· 03-dataplatform
· xx-sandbox
· xx-teams
|
+| [sandbox](outputs.tf#L314) | Data for the sandbox stage. | | xx-sandbox
|
+| [security](outputs.tf#L328) | Data for the networking stage. | | 02-security
|
+| [team_cicd_repositories](outputs.tf#L338) | WIF configuration for Team CI/CD repositories. | | |
+| [teams](outputs.tf#L352) | Data for the teams stage. | | |
+| [tfvars](outputs.tf#L364) | Terraform variable files for the following stages. | ✓ | |
diff --git a/fast/stages/1-resman/branch-teams.tf b/fast/stages/1-resman/branch-teams.tf
index 4996f183..9c9f5399 100644
--- a/fast/stages/1-resman/branch-teams.tf
+++ b/fast/stages/1-resman/branch-teams.tf
@@ -90,10 +90,13 @@ module "branch-teams-team-sa" {
display_name = "Terraform team ${each.key} service account."
prefix = var.prefix
iam = {
- "roles/iam.serviceAccountTokenCreator" = (
- each.value.impersonation_groups == null
- ? []
- : [for g in each.value.impersonation_groups : "group:${g}"]
+ "roles/iam.serviceAccountTokenCreator" = concat(
+ compact([try(module.branch-teams-team-sa-cicd[each.key].iam_email, null)]),
+ (
+ each.value.impersonation_groups == null
+ ? []
+ : [for g in each.value.impersonation_groups : "group:${g}"]
+ )
)
}
}
diff --git a/fast/stages/1-resman/cicd-teams.tf b/fast/stages/1-resman/cicd-teams.tf
new file mode 100644
index 00000000..f604a085
--- /dev/null
+++ b/fast/stages/1-resman/cicd-teams.tf
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2022 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.
+ */
+
+# tfdoc:file:description CI/CD resources for individual teams.
+
+# source repository
+
+module "branch-teams-team-cicd-repo" {
+ source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric//modules/source-repository?ref=v24.0.0"
+ for_each = {
+ for k, v in coalesce(local.team_cicd_repositories, {}) : k => v
+ if v.cicd.type == "sourcerepo"
+ }
+ project_id = var.automation.project_id
+ name = each.value.cicd.name
+ iam = {
+ "roles/source.admin" = [module.branch-teams-team-sa[each.key].iam_email]
+ "roles/source.reader" = [module.branch-teams-team-sa-cicd[each.key].iam_email]
+ }
+ triggers = {
+ "fast-03-team-${each.key}" = {
+ filename = ".cloudbuild/workflow.yaml"
+ included_files = ["**/*tf", ".cloudbuild/workflow.yaml"]
+ service_account = module.branch-teams-team-sa-cicd[each.key].id
+ substitutions = {}
+ template = {
+ project_id = null
+ branch_name = each.value.cicd.branch
+ repo_name = each.value.cicd.name
+ tag_name = null
+ }
+ }
+ }
+ depends_on = [module.branch-teams-team-sa-cicd]
+}
+
+# SA used by CI/CD workflows to impersonate automation SAs
+
+module "branch-teams-team-sa-cicd" {
+ source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric//modules/iam-service-account?ref=v24.0.0"
+ for_each = (
+ try(local.team_cicd_repositories, null) != null
+ ? local.team_cicd_repositories
+ : {}
+ )
+ project_id = var.automation.project_id
+ name = "prod-teams-${each.key}-1"
+ display_name = "Terraform CI/CD team ${each.key} service account."
+ prefix = var.prefix
+ iam = (
+ each.value.cicd.type == "sourcerepo"
+ # used directly from the cloud build trigger for source repos
+ ? {
+ "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.cicd.branch == null
+ ? format(
+ local.identity_providers[each.value.cicd.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.cicd.name
+ )
+ : format(
+ local.identity_providers[each.value.cicd.identity_provider].principal_tpl,
+ var.automation.federated_identity_pool,
+ each.value.cicd.name,
+ each.value.cicd.branch
+ )
+ ]
+ }
+ )
+ iam_project_roles = {
+ (var.automation.project_id) = ["roles/logging.logWriter"]
+ }
+ iam_storage_roles = {
+ (var.automation.outputs_bucket) = ["roles/storage.objectViewer"]
+ }
+}
diff --git a/fast/stages/1-resman/main.tf b/fast/stages/1-resman/main.tf
index 95bc1c4f..a30b56fd 100644
--- a/fast/stages/1-resman/main.tf
+++ b/fast/stages/1-resman/main.tf
@@ -47,6 +47,21 @@ locals {
fileexists("${path.module}/templates/workflow-${try(v.type, "")}.yaml")
)
}
+ team_cicd_repositories = {
+ for k, v in coalesce(var.team_folders, {}) : k => v
+ if(
+ v != null &&
+ (
+ try(v.cicd.type, null) == "sourcerepo"
+ ||
+ contains(
+ keys(local.identity_providers),
+ coalesce(try(v.cicd.identity_provider, null), ":")
+ )
+ ) &&
+ fileexists("${path.module}/templates/workflow-${try(v.cicd.type, "")}.yaml")
+ )
+ }
cicd_workflow_var_files = {
stage_2 = [
"0-bootstrap.auto.tfvars.json",
diff --git a/fast/stages/1-resman/outputs-files.tf b/fast/stages/1-resman/outputs-files.tf
index f7f080dd..2f13adfc 100644
--- a/fast/stages/1-resman/outputs-files.tf
+++ b/fast/stages/1-resman/outputs-files.tf
@@ -35,7 +35,7 @@ resource "local_file" "tfvars" {
}
resource "local_file" "workflows" {
- for_each = var.outputs_location == null ? {} : local.cicd_workflows
+ for_each = var.outputs_location == null ? {} : merge(local.cicd_workflows, local.team_cicd_workflows)
file_permission = "0644"
filename = "${local.outputs_location}/workflows/${replace(each.key, "_", "-")}-workflow.yaml"
content = try(each.value, null)
diff --git a/fast/stages/1-resman/outputs-gcs.tf b/fast/stages/1-resman/outputs-gcs.tf
index 5b9f5d85..8e102a41 100644
--- a/fast/stages/1-resman/outputs-gcs.tf
+++ b/fast/stages/1-resman/outputs-gcs.tf
@@ -30,7 +30,7 @@ resource "google_storage_bucket_object" "tfvars" {
}
resource "google_storage_bucket_object" "workflows" {
- for_each = local.cicd_workflows
+ for_each = merge(local.cicd_workflows, local.team_cicd_workflows)
bucket = var.automation.outputs_bucket
name = "workflows/${replace(each.key, "_", "-")}-workflow.yaml"
content = each.value
diff --git a/fast/stages/1-resman/outputs.tf b/fast/stages/1-resman/outputs.tf
index c15706e2..552d42d7 100644
--- a/fast/stages/1-resman/outputs.tf
+++ b/fast/stages/1-resman/outputs.tf
@@ -201,6 +201,25 @@ locals {
for k, v in module.branch-teams-team-sa : "team-${k}" => v.email
},
)
+ team_cicd_workflows = {
+ for k, v in local.team_cicd_repositories : k => templatefile(
+ "${path.module}/templates/workflow-${v.cicd.type}.yaml",
+ merge(local.team_cicd_workflow_attrs[k], {
+ identity_provider = try(
+ local.identity_providers[v.cicd.identity_provider].name, null
+ )
+ outputs_bucket = var.automation.outputs_bucket
+ stage_name = k
+ })
+ )
+ }
+ team_cicd_workflow_attrs = {
+ for k, v in local.team_cicd_repositories : k => {
+ service_account = try(module.branch-teams-team-sa-cicd[k].email, null)
+ tf_providers_file = "3-teams-${k}-providers.tf"
+ tf_var_files = local.cicd_workflow_var_files.stage_3
+ }
+ }
tfvars = {
folder_ids = local.folder_ids
service_accounts = local.service_accounts
@@ -316,6 +335,20 @@ output "security" {
}
}
+output "team_cicd_repositories" {
+ description = "WIF configuration for Team CI/CD repositories."
+ value = {
+ for k, v in local.team_cicd_repositories : k => {
+ branch = v.cicd.branch
+ name = v.cicd.name
+ provider = try(
+ local.identity_providers[v.cicd.identity_provider].name, null
+ )
+ service_account = local.team_cicd_workflow_attrs[k].service_account
+ } if v.cicd != null
+ }
+}
+
output "teams" {
description = "Data for the teams stage."
value = {
diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf
index 35030d2a..a613e5ad 100644
--- a/fast/stages/1-resman/variables.tf
+++ b/fast/stages/1-resman/variables.tf
@@ -272,6 +272,12 @@ variable "team_folders" {
descriptive_name = string
group_iam = map(list(string))
impersonation_groups = list(string)
+ cicd = optional(object({
+ branch = string
+ identity_provider = string
+ name = string
+ type = string
+ }))
}))
default = null
}