diff --git a/fast/stages/fast-links.sh b/fast/stages/fast-links.sh index 0d971e463..991621011 100755 --- a/fast/stages/fast-links.sh +++ b/fast/stages/fast-links.sh @@ -1,5 +1,5 @@ -#!/bin/bash -# Copyright 2024 Google LLC +#!/bin/env bash +# 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. diff --git a/modules/net-vlan-attachment/README.md b/modules/net-vlan-attachment/README.md index ed0f580c4..0ba2ed816 100644 --- a/modules/net-vlan-attachment/README.md +++ b/modules/net-vlan-attachment/README.md @@ -613,6 +613,7 @@ module "example-va-a" { description = "example-va-a vlan attachment" peer_asn = "65001" router_config = { + asn = 16550 create = true } partner_interconnect_config = { @@ -630,6 +631,7 @@ module "example-va-b" { description = "example-va-b vlan attachment" peer_asn = "65001" router_config = { + asn = 16550 create = true } partner_interconnect_config = { diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index c5be4d194..591cf7dd9 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -291,7 +291,10 @@ Assuming keys of the form `my_folder`, `my_project`, `my_sa`, etc. this is an ex - `$service_account_ids:my_project/my_sa` - `$service_account_ids:my_project/automation/my_sa` - `$service_agents:compute` -- `$tag_values:my_value` +- `$tag_keys:my_key` *static context* +- `$tag_keys:my_project/my_key` *project-level tag keys* +- `$tag_values:my_key/my_value` *static context* +- `$tag_values:my_project/my_key/my_value` *project-level tag values* - `$vpc_host_projects:my_project` - `$vpc_sc_perimeters:my_perimeter` @@ -855,11 +858,11 @@ compute.disableSerialPortAccess: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factories_config](variables.tf#L164) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [data_defaults](variables.tf#L41) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L106) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L125) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factories_config](variables.tf#L165) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [data_defaults](variables.tf#L42) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L107) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | +| [data_overrides](variables.tf#L126) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | | [folders](variables-folders.tf#L17) | Folders data merged with factory data. | map(object({…})) | | {} | | [notification_channels](variables-billing.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} | | [projects](variables-projects.tf#L17) | Projects data merged with factory data. | map(object({…})) | | {} | @@ -882,7 +885,6 @@ compute.disableSerialPortAccess: | [service_accounts](outputs.tf#L158) | Service account emails. | | | [storage_buckets](outputs.tf#L163) | Bucket names. | | - ## Tests These tests validate fixes to the project factory. @@ -896,6 +898,15 @@ module "project-factory" { id = 1234567890 } } + iam_principals = { + tag-test = "user:user1@example.com" + } + tag_keys = { + "context" = "tagKeys/1234567890" + } + tag_values = { + "context/project-factory" = "tagValues/1234567890" + } } data_defaults = { billing_account = "012345-67890A-ABCDEF" @@ -916,7 +927,7 @@ module "project-factory" { projects = "data/projects" } } -# tftest modules=5 resources=29 files=test-0,test-1,test-2 inventory=test-1.yaml +# tftest modules=7 resources=31 files=test-0,test-1,test-2 inventory=test-1.yaml ``` ```yaml @@ -934,14 +945,15 @@ iam_bindings_additive: title: Test context expression: resource.matchTag('${organization.id}/context', 'project-factory') tags: - allow-key-creation: - description: Allow key creation for automation service account + context: + description: Test org-level tag value shadowing. values: - allow: - description: Allow key creation + project-factory: + description: Test value. iam: roles/resourcemanager.tagUser: - - $iam_principals:service_accounts/tags-iam-test/automation/rw + - $iam_principals:tag-test + - $iam_principals:service_accounts/test-1/tag-test # tftest-file id=test-0 path=data/projects/test-0.yaml ``` @@ -953,8 +965,11 @@ prefix: null services: - iam.googleapis.com - contactcenteraiplatform.googleapis.com +service_accounts: + tag-test: {} tag_bindings: - test: $tag_values/ + org-level: $tag_values:context/project-factory + project-level: $tag_values:test-0/context/project-factory # tftest-file id=test-1 path=data/projects/test-1.yaml ``` diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf index 55ac766d1..8cf1b5999 100644 --- a/modules/project-factory/main.tf +++ b/modules/project-factory/main.tf @@ -39,7 +39,7 @@ resource "terraform_data" "defaults_preconditions" { } # precondition { # condition = local.projects_input == null - # error_message = yamlencode(var.context.condition_vars) + # error_message = jsonencode(local.ctx_tag_values) # } } } diff --git a/modules/project-factory/projects.tf b/modules/project-factory/projects.tf index 3d17037d9..d7dcbc2b8 100644 --- a/modules/project-factory/projects.tf +++ b/modules/project-factory/projects.tf @@ -49,6 +49,23 @@ locals { } ctx_project_ids = merge(local.ctx.project_ids, local.project_ids) ctx_project_numbers = merge(local.ctx.project_numbers, local.project_numbers) + # cross-project tag contexts, keyed on project name + ctx_tag_keys = merge(local.ctx.tag_keys, { + for k, v in merge([ + for pk, pv in local.projects_input : { + for tk, tv in module.projects[pk].tag_keys : + "${pv.name}/${tk}" => tv.id + } + ]...) : k => v + }) + ctx_tag_values = merge(local.ctx.tag_values, { + for k, v in merge([ + for pk, pv in local.projects_input : { + for tk, tv in module.projects[pk].tag_values : + "${pv.name}/${tk}" => tv.id + } + ]...) : k => v + }) project_ids = { for k, v in module.projects : k => v.project_id } @@ -130,10 +147,10 @@ module "projects" { each.value.services, var.data_merges.services )) - tag_bindings = merge( - each.value.tag_bindings, var.data_merges.tag_bindings - ) - tags = each.value.tags + tags = each.value.tags + tags_config = { + ignore_iam = true + } universe = each.value.universe vpc_sc = each.value.vpc_sc workload_identity_pools = each.value.workload_identity_pools @@ -142,7 +159,8 @@ module "projects" { module "projects-iam" { source = "../project" for_each = local.projects_input - name = module.projects[each.key].project_id + name = each.value.name + prefix = each.value.prefix project_reuse = { use_data_source = false attributes = { @@ -163,6 +181,8 @@ module "projects-iam" { local.ctx.project_ids, { for k, v in module.projects : k => v.project_id } ) + tag_keys = local.ctx_tag_keys + tag_values = local.ctx_tag_values }) factories_config = { # we do anything that can refer to IAM and custom roles in this call @@ -186,5 +206,16 @@ module "projects-iam" { ) shared_vpc_host_config = each.value.shared_vpc_host_config shared_vpc_service_config = each.value.shared_vpc_service_config - universe = each.value.universe + tag_bindings = merge( + each.value.tag_bindings, var.data_merges.tag_bindings + ) + tags = each.value.tags + tags_config = { + force_context_ids = true + } + universe = each.value.universe + # we use explicit depends_on as this allows us passing name and prefix + depends_on = [ + module.projects + ] } diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index b387ff686..c27c8ee25 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -30,6 +30,7 @@ variable "context" { project_numbers = optional(map(string), {}) pubsub_topics = optional(map(string), {}) storage_buckets = optional(map(string), {}) + tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) vpc_host_projects = optional(map(string), {}) vpc_sc_perimeters = optional(map(string), {}) diff --git a/modules/project/tags.tf b/modules/project/tags.tf index 32910a81e..b6ed4b768 100644 --- a/modules/project/tags.tf +++ b/modules/project/tags.tf @@ -46,7 +46,7 @@ locals { tags = { for k, v in local._tags_merged : k => { id = v.id != null ? v.id : ( - var.tags_config.force_context_ids == true ? "$tag_keys:${k}" : null + var.tags_config.force_context_ids == true ? "$tag_keys:${var.name}/${k}" : null ) description = v.description iam = var.tags_config.ignore_iam == true ? {} : { @@ -64,7 +64,7 @@ locals { values = { for vk, vv in v.values : vk => { id = vv.id != null ? vv.id : ( - var.tags_config.force_context_ids == true ? "$tag_values:${k}/${vk}" : null + var.tags_config.force_context_ids == true ? "$tag_values:${var.name}/${k}/${vk}" : null ) description = vv.description iam = var.tags_config.ignore_iam == true ? {} : { diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml index c4d1d307f..b38126e39 100644 --- a/tests/modules/project_factory/examples/example.yaml +++ b/tests/modules/project_factory/examples/example.yaml @@ -357,6 +357,14 @@ values: : condition: [] project: $project_ids:dev-spoke-0 role: roles/container.hostServiceAgentUser + module.project-factory.module.projects-iam["dev-ta-app0-be"].google_tags_tag_binding.binding["context"]: + tag_value: tagValues/654321 + timeouts: null + ? module.project-factory.module.projects-iam["dev-ta-app0-be"].google_tags_tag_value_iam_binding.default["my-tag-key-1/my-value-2:roles/resourcemanager.tagUser"] + : condition: [] + members: + - user:user@example.com + role: roles/resourcemanager.tagUser module.project-factory.module.projects-iam["dev-tb-app0-0"].google_compute_shared_vpc_host_project.shared_vpc_host[0]: project: test-pf-dev-tb-app0-0 timeouts: null @@ -478,10 +486,8 @@ values: project: test-pf-dev-ta-app0-be service: pubsub.googleapis.com timeouts: null - module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_binding.binding["context"]: - tag_value: tagValues/654321 - timeouts: null module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_key.default["my-tag-key-1"]: + allowed_values_regex: null description: Managed by the Terraform project-factory module. parent: projects/test-pf-dev-ta-app0-be purpose: null @@ -496,11 +502,6 @@ values: description: My value 3 short_name: my-value-2 timeouts: null - ? module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_value_iam_binding.default["my-tag-key-1/my-value-2:roles/resourcemanager.tagUser"] - : condition: [] - members: - - user:user@example.com - role: roles/resourcemanager.tagUser module.project-factory.module.projects["dev-tb-app0-0"].data.google_storage_project_service_account.gcs_sa[0]: project: test-pf-dev-tb-app0-0 user_project: null diff --git a/tests/modules/project_factory/examples/test-1.yaml b/tests/modules/project_factory/examples/test-1.yaml index ed4ae9fe1..fb0f441fe 100644 --- a/tests/modules/project_factory/examples/test-1.yaml +++ b/tests/modules/project_factory/examples/test-1.yaml @@ -21,6 +21,19 @@ values: member: user:user1@example.com project: foo-test-0 role: roles/viewer + ? module.project-factory.module.projects-iam["test-0"].google_tags_tag_value_iam_binding.default["context/project-factory:roles/resourcemanager.tagUser"] + : condition: [] + members: + - serviceAccount:tag-test@test-1.iam.gserviceaccount.com + - user:user1@example.com + role: roles/resourcemanager.tagUser + module.project-factory.module.projects-iam["test-1"].google_tags_tag_binding.binding["org-level"]: + tag_value: tagValues/1234567890 + timeouts: null + module.project-factory.module.projects-iam["test-1"].google_tags_tag_binding.binding["project-level"]: + # tag_value is undefined at plan time as it depends on the tag + # tag_value: $tag_values:test-0/context/project-factory + timeouts: null module.project-factory.module.projects["test-0"].google_project.project[0]: auto_create_network: false billing_account: 012345-67890A-ABCDEF @@ -83,23 +96,18 @@ values: project: foo-test-0 service: container.googleapis.com timeouts: null - module.project-factory.module.projects["test-0"].google_tags_tag_key.default["allow-key-creation"]: + module.project-factory.module.projects["test-0"].google_tags_tag_key.default["context"]: allowed_values_regex: null - description: Allow key creation for automation service account + description: Test org-level tag value shadowing. parent: projects/foo-test-0 purpose: null purpose_data: null - short_name: allow-key-creation + short_name: context timeouts: null - module.project-factory.module.projects["test-0"].google_tags_tag_value.default["allow-key-creation/allow"]: - description: Allow key creation - short_name: allow + module.project-factory.module.projects["test-0"].google_tags_tag_value.default["context/project-factory"]: + description: Test value. + short_name: project-factory timeouts: null - ? module.project-factory.module.projects["test-0"].google_tags_tag_value_iam_binding.default["allow-key-creation/allow:roles/resourcemanager.tagUser"] - : condition: [] - members: - - $iam_principals:service_accounts/tags-iam-test/automation/rw - role: roles/resourcemanager.tagUser module.project-factory.module.projects["test-1"].google_project.project[0]: auto_create_network: false billing_account: 012345-67890A-ABCDEF @@ -144,9 +152,6 @@ values: : project: test-1 service: contactcenteraiplatform.googleapis.com timeouts: null - module.project-factory.module.projects["test-1"].google_tags_tag_binding.binding["test"]: - tag_value: $tag_values/ - timeouts: null module.project-factory.module.projects["test-2"].data.google_storage_project_service_account.gcs_sa[0]: project: bar-test-2 user_project: null @@ -190,6 +195,16 @@ values: project: bar-test-2 service: storage.googleapis.com timeouts: null + module.project-factory.module.service-accounts["test-1/tag-test"].google_service_account.service_account[0]: + account_id: tag-test + create_ignore_already_exists: null + description: null + disabled: false + display_name: Terraform-managed. + email: tag-test@test-1.iam.gserviceaccount.com + member: serviceAccount:tag-test@test-1.iam.gserviceaccount.com + project: test-1 + timeouts: null module.project-factory.terraform_data.defaults_preconditions: input: null output: null @@ -204,11 +219,12 @@ counts: google_project_iam_member: 6 google_project_service: 10 google_project_service_identity: 3 + google_service_account: 1 google_storage_project_service_account: 1 - google_tags_tag_binding: 1 + google_tags_tag_binding: 2 google_tags_tag_key: 1 google_tags_tag_value: 1 google_tags_tag_value_iam_binding: 1 - modules: 5 - resources: 29 + modules: 7 + resources: 31 terraform_data: 2