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 }