diff --git a/GEMINI.md b/GEMINI.md index 11d22e62f..9ae14a1a9 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -119,6 +119,21 @@ pytest -s 'tests/examples/test_plan.py::test_example[terraform:modules//tftest.yaml` define test scenarios (e.g., context resolution, IAM variants) using `tfvars` + YAML inventory pairs. Run them individually: + +```bash +# Run a specific test from a module's tftest.yaml +pytest 'tests/modules//tftest.yaml::' --tb=short -s +``` + +For example: +```bash +pytest 'tests/modules/organization/tftest.yaml::context' --tb=short -s +pytest 'tests/modules/project/tftest.yaml::context' --tb=short -s +``` + #### 3. Contributing * **Branching:** Use `username/feature-name`. diff --git a/adrs/20260210-dataset-base-path.md b/adrs/20260210-dataset-base-path.md index 9ee413e4e..8ff703e5c 100644 --- a/adrs/20260210-dataset-base-path.md +++ b/adrs/20260210-dataset-base-path.md @@ -5,7 +5,7 @@ ## Status -Draft +Implemented ## Context diff --git a/adrs/20260424-templatestring-vars-convention.md b/adrs/20260424-templatestring-vars-convention.md new file mode 100644 index 000000000..c7f756566 --- /dev/null +++ b/adrs/20260424-templatestring-vars-convention.md @@ -0,0 +1,52 @@ +# Convention for context variables passed to templatestring + +**authors:** [Julio Castillo](https://github.com/juliocc), Antigravity (AI Assistant) +**date:** Apr 24, 2026 + +## Status + +Approved + +## Context + +In Cloud Foundation Fabric, we use a single `context` variable to pass shared state between modules (as documented in `20251013-context-locals.md`). Most of these context variables are flat maps of strings used for simple lookups and full replacements (e.g., `project_ids`, `networks`). + +However, some use cases require partial interpolation within strings using Terraform's `templatestring()` function (for example, resolving dynamic tags in bindings, or conditions in IAM bindings). The `templatestring()` function requires its second argument (the variables map) to be a direct reference to a data structure. + +When we attempt to flatten the entire `context` variable into a single map for lookups (like `local.ctx`), complex objects like those needed for `templatestring` cause type mismatch errors because they are not flat maps of strings. + +## Decision + +1. Context variables intended to be passed as the variables map (second argument) to `templatestring()` MUST be named with a `_vars` suffix (e.g., `tag_vars`, `condition_vars`). +2. These variables MUST be excluded from the `local.ctx` flattening loop in modules to avoid type mismatch errors. +3. Other keys in `context` not with `_vars` suffix continue to be flat maps used for full replacement/lookup. + +Example of exclusion in `locals`: + +```hcl + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } if !endswith(k, "_vars") + } +``` + +Example of usage: + +```hcl + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) +``` + +## Consequences + +* Maintains the single `context` variable pattern while supporting complex template interpolations. +* Ensures type safety during context flattening. +* Automatically excludes any new `_vars` variable in the module's local context construction using `endswith`. + +## Reasoning + +This convention provides a clear visual and structural distinction between simple lookup maps and complex variable structures used for templating, preventing runtime errors in Terraform. + +## Implementation + +This pattern has been implemented for `tag_vars` and `condition_vars` in the `project`, `folder`, `gcs`, `bigquery-dataset`, and `kms` modules, as well as in the Project Factory and relevant FAST stages. diff --git a/fast/stages/0-org-setup/output-files.tf b/fast/stages/0-org-setup/output-files.tf index fba1a3204..27133174d 100644 --- a/fast/stages/0-org-setup/output-files.tf +++ b/fast/stages/0-org-setup/output-files.tf @@ -40,6 +40,7 @@ locals { module.factory.project_ids ) storage_buckets = module.factory.storage_buckets + tag_keys = module.organization[0].tag_keys tag_values = merge( local.ctx.tag_values, local.org_tag_values @@ -125,6 +126,7 @@ locals { subnet_self_links = { for k, v in module.vpcs.vpcs : k => v.subnet_ids } + tag_keys = local.of_ctx.tag_keys tag_values = local.of_ctx.tag_values vpc_self_links = { for k, v in module.vpcs.vpcs : k => v.id diff --git a/fast/stages/0-org-setup/schemas/defaults.schema.json b/fast/stages/0-org-setup/schemas/defaults.schema.json index fd6792058..98c8406db 100644 --- a/fast/stages/0-org-setup/schemas/defaults.schema.json +++ b/fast/stages/0-org-setup/schemas/defaults.schema.json @@ -748,6 +748,24 @@ "type": "string" } }, + "tag_vars": { + "type": "object", + "additionalProperties": false, + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "organization": { + "type": "string" + } + } + }, "vpc_host_projects": { "type": "object", "additionalProperties": { diff --git a/fast/stages/0-org-setup/schemas/defaults.schema.md b/fast/stages/0-org-setup/schemas/defaults.schema.md index 77570efd6..bdf1b50b2 100644 --- a/fast/stages/0-org-setup/schemas/defaults.schema.md +++ b/fast/stages/0-org-setup/schemas/defaults.schema.md @@ -220,6 +220,11 @@
*additional properties: string* - **tag_values**: *object*
*additional properties: string* + - **tag_vars**: *object* +
*additional properties: false* + - **projects**: *object* +
*additional properties: object* + - **organization**: *string* - **vpc_host_projects**: *object*
*additional properties: string* - **vpc_sc_perimeters**: *object* diff --git a/fast/stages/0-org-setup/schemas/project.schema.json b/fast/stages/0-org-setup/schemas/project.schema.json index 4f3b15610..32e1bd81f 100644 --- a/fast/stages/0-org-setup/schemas/project.schema.json +++ b/fast/stages/0-org-setup/schemas/project.schema.json @@ -834,6 +834,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "description": { "type": "string" }, @@ -873,7 +876,23 @@ } } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ] } }, "tag_bindings": { diff --git a/fast/stages/0-org-setup/schemas/tags.schema.json b/fast/stages/0-org-setup/schemas/tags.schema.json index 64e2827b0..22b5357b4 100644 --- a/fast/stages/0-org-setup/schemas/tags.schema.json +++ b/fast/stages/0-org-setup/schemas/tags.schema.json @@ -4,6 +4,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "name": { "type": "string" }, @@ -56,6 +59,22 @@ } } }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ], "$defs": { "iam": { "type": "object", diff --git a/fast/stages/0-org-setup/schemas/tags.schema.md b/fast/stages/0-org-setup/schemas/tags.schema.md index a7b5bc865..592e01ada 100644 --- a/fast/stages/0-org-setup/schemas/tags.schema.md +++ b/fast/stages/0-org-setup/schemas/tags.schema.md @@ -6,6 +6,7 @@ *additional properties: false* +- **allowed_values_regex**: *string* - **name**: *string* - **description**: *string* - **id**: *string* diff --git a/fast/stages/2-networking/README.md b/fast/stages/2-networking/README.md index 7e9d98b79..5d8e6bd24 100644 --- a/fast/stages/2-networking/README.md +++ b/fast/stages/2-networking/README.md @@ -392,9 +392,9 @@ Internally created resources are mapped to context namespaces, and use specific | [project_ids](variables-fast.tf#L85) | Projects created in the bootstrap stage. | map(string) | | {} | | [service_accounts](variables-fast.tf#L93) | Service accounts created in the bootstrap stage. | map(string) | | {} | | [storage_buckets](variables-fast.tf#L101) | Storage buckets created in the bootstrap stage. | map(string) | | {} | -| [tag_keys](variables-fast.tf#L109) | FAST-managed resource manager tag keys. | map(string) | | {} | -| [tag_values](variables-fast.tf#L117) | FAST-managed resource manager tag values. | map(string) | | {} | -| [universe](variables-fast.tf#L125) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | +| [tag_keys](variables-fast.tf#L109) | FAST-managed resource manager tag keys. | map(object({…})) | | {} | +| [tag_values](variables-fast.tf#L120) | FAST-managed resource manager tag values. | map(string) | | {} | +| [universe](variables-fast.tf#L128) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | ## Outputs diff --git a/fast/stages/2-networking/main.tf b/fast/stages/2-networking/main.tf index 3bcff6b90..507edcae9 100644 --- a/fast/stages/2-networking/main.tf +++ b/fast/stages/2-networking/main.tf @@ -42,11 +42,18 @@ locals { }, local._ctx.iam_principals ) - kms_keys = merge(var.kms_keys, local._ctx.kms_keys) - project_ids = merge(var.project_ids, local._ctx.project_ids) - storage_buckets = merge(var.storage_buckets, local._ctx.storage_buckets) - tag_keys = merge(var.tag_keys, local._ctx.tag_keys) - tag_values = merge(var.tag_values, local._ctx.tag_values) + kms_keys = merge(var.kms_keys, local._ctx.kms_keys) + project_ids = merge(var.project_ids, local._ctx.project_ids) + storage_buckets = merge(var.storage_buckets, local._ctx.storage_buckets) + tag_keys = merge(var.tag_keys, local._ctx.tag_keys) + tag_values = merge(var.tag_values, local._ctx.tag_values) + tag_vars = { + projects = try(local._ctx.tag_vars.projects, {}) + organization = merge({ + for k, v in var.tag_keys : k => v.namespaced_name + if v.allowed_values_regex != null + }, try(local._ctx.tag_vars.organization, {})) + } vpc_sc_perimeters = merge(var.perimeters, local._ctx.vpc_sc_perimeters) }) # normalize defaults diff --git a/fast/stages/2-networking/schemas/defaults.schema.json b/fast/stages/2-networking/schemas/defaults.schema.json index f31c07f90..7b0237031 100644 --- a/fast/stages/2-networking/schemas/defaults.schema.json +++ b/fast/stages/2-networking/schemas/defaults.schema.json @@ -710,6 +710,24 @@ "type": "string" } }, + "tag_vars": { + "type": "object", + "additionalProperties": false, + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "organization": { + "type": "string" + } + } + }, "vpc_sc_perimeters": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-networking/schemas/defaults.schema.md b/fast/stages/2-networking/schemas/defaults.schema.md index 37a838d52..96bf3ebb4 100644 --- a/fast/stages/2-networking/schemas/defaults.schema.md +++ b/fast/stages/2-networking/schemas/defaults.schema.md @@ -212,6 +212,11 @@
*additional properties: string* - **tag_values**: *object*
*additional properties: string* + - **tag_vars**: *object* +
*additional properties: false* + - **projects**: *object* +
*additional properties: object* + - **organization**: *string* - **vpc_sc_perimeters**: *object*
*additional properties: string* - **output_files**: *object* diff --git a/fast/stages/2-networking/schemas/project.schema.json b/fast/stages/2-networking/schemas/project.schema.json index 4f3b15610..32e1bd81f 100644 --- a/fast/stages/2-networking/schemas/project.schema.json +++ b/fast/stages/2-networking/schemas/project.schema.json @@ -834,6 +834,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "description": { "type": "string" }, @@ -873,7 +876,23 @@ } } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ] } }, "tag_bindings": { diff --git a/fast/stages/2-networking/variables-fast.tf b/fast/stages/2-networking/variables-fast.tf index 32595cefd..6a65b0cc6 100644 --- a/fast/stages/2-networking/variables-fast.tf +++ b/fast/stages/2-networking/variables-fast.tf @@ -109,9 +109,12 @@ variable "storage_buckets" { variable "tag_keys" { # tfdoc:variable:source 0-org-setup description = "FAST-managed resource manager tag keys." - type = map(string) - nullable = false - default = {} + type = map(object({ + namespaced_name = string + allowed_values_regex = optional(string) + })) + default = {} + nullable = false } variable "tag_values" { diff --git a/fast/stages/2-project-factory/README.md b/fast/stages/2-project-factory/README.md index a41d9bad4..c61887756 100644 --- a/fast/stages/2-project-factory/README.md +++ b/fast/stages/2-project-factory/README.md @@ -497,8 +497,9 @@ Pattern-based files make specific assumptions: | [service_accounts](variables-fast.tf#L110) | Service accounts created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [stage_name](variables.tf#L58) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | | | [subnet_self_links](variables-fast.tf#L118) | Shared VPC subnet IDs. | map(map(string)) | | {} | 2-networking | +| [tag_keys](variables-fast.tf#L134) | FAST-managed resource manager tag keys. | map(object({…})) | | {} | 0-org-setup | | [tag_values](variables-fast.tf#L126) | FAST-managed resource manager tag values. | map(string) | | {} | 0-org-setup | -| [universe](variables-fast.tf#L134) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | 0-globals | +| [universe](variables-fast.tf#L145) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | 0-globals | ## Outputs diff --git a/fast/stages/2-project-factory/main.tf b/fast/stages/2-project-factory/main.tf index ba7f4e765..070b0087e 100644 --- a/fast/stages/2-project-factory/main.tf +++ b/fast/stages/2-project-factory/main.tf @@ -102,7 +102,14 @@ module "factory" { project_ids = merge( var.project_ids, var.host_project_ids, local.context.project_ids ) - tag_values = merge(var.tag_values, local.context.tag_values) + tag_values = merge(var.tag_values, local.context.tag_values) + tag_vars = { + projects = try(local.context.tag_vars.projects, {}) + organization = merge({ + for k, v in var.tag_keys : k => v.namespaced_name + if v.allowed_values_regex != null + }, try(local.context.tag_vars.organization, {})) + } vpc_sc_perimeters = merge(var.perimeters, local.context.vpc_sc_perimeters) } data_defaults = local.project_defaults.defaults diff --git a/fast/stages/2-project-factory/schemas/defaults.schema.json b/fast/stages/2-project-factory/schemas/defaults.schema.json index 9125f1d35..1243240d4 100644 --- a/fast/stages/2-project-factory/schemas/defaults.schema.json +++ b/fast/stages/2-project-factory/schemas/defaults.schema.json @@ -542,6 +542,24 @@ "type": "string" } }, + "tag_vars": { + "type": "object", + "additionalProperties": false, + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "organization": { + "type": "string" + } + } + }, "vpc_host_projects": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-project-factory/schemas/defaults.schema.md b/fast/stages/2-project-factory/schemas/defaults.schema.md index 63a7c9494..3122239c5 100644 --- a/fast/stages/2-project-factory/schemas/defaults.schema.md +++ b/fast/stages/2-project-factory/schemas/defaults.schema.md @@ -153,6 +153,11 @@
*additional properties: string* - **tag_values**: *object*
*additional properties: string* + - **tag_vars**: *object* +
*additional properties: false* + - **projects**: *object* +
*additional properties: object* + - **organization**: *string* - **vpc_host_projects**: *object*
*additional properties: string* - **vpc_sc_perimeters**: *object* diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json index 4f3b15610..32e1bd81f 100644 --- a/fast/stages/2-project-factory/schemas/project.schema.json +++ b/fast/stages/2-project-factory/schemas/project.schema.json @@ -834,6 +834,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "description": { "type": "string" }, @@ -873,7 +876,23 @@ } } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ] } }, "tag_bindings": { diff --git a/fast/stages/2-project-factory/variables-fast.tf b/fast/stages/2-project-factory/variables-fast.tf index 6214c04bf..5ffe7e595 100644 --- a/fast/stages/2-project-factory/variables-fast.tf +++ b/fast/stages/2-project-factory/variables-fast.tf @@ -131,6 +131,17 @@ variable "tag_values" { default = {} } +variable "tag_keys" { + # tfdoc:variable:source 0-org-setup + description = "FAST-managed resource manager tag keys." + type = map(object({ + namespaced_name = string + allowed_values_regex = optional(string) + })) + default = {} + nullable = false +} + variable "universe" { # tfdoc:variable:source 0-globals description = "GCP universe where to deploy projects. The prefix will be prepended to the project id." diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md index 3eea3cbab..402674b25 100644 --- a/fast/stages/2-security/README.md +++ b/fast/stages/2-security/README.md @@ -198,9 +198,9 @@ A reference Certificate Authority Services (CAS) is also part of this stage, all | [project_ids](variables-fast.tf#L67) | Projects created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [service_accounts](variables-fast.tf#L75) | Service accounts created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [storage_buckets](variables-fast.tf#L83) | Storage buckets created in the bootstrap stage. | map(string) | | {} | 0-org-setup | -| [tag_keys](variables-fast.tf#L91) | FAST-managed resource manager tag keys. | map(string) | | {} | 0-org-setup | -| [tag_values](variables-fast.tf#L99) | FAST-managed resource manager tag values. | map(string) | | {} | 0-org-setup | -| [universe](variables-fast.tf#L107) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | 0-org-setup | +| [tag_keys](variables-fast.tf#L91) | FAST-managed resource manager tag keys. | map(object({…})) | | {} | 0-org-setup | +| [tag_values](variables-fast.tf#L102) | FAST-managed resource manager tag values. | map(string) | | {} | 0-org-setup | +| [universe](variables-fast.tf#L110) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | 0-org-setup | ## Outputs diff --git a/fast/stages/2-security/main.tf b/fast/stages/2-security/main.tf index 267b1c72f..74997766d 100644 --- a/fast/stages/2-security/main.tf +++ b/fast/stages/2-security/main.tf @@ -38,10 +38,17 @@ locals { }, local._ctx.iam_principals ) - project_ids = merge(var.project_ids, local._ctx.project_ids) - storage_buckets = merge(var.storage_buckets, local._ctx.storage_buckets) - tag_keys = merge(var.tag_keys, local._ctx.tag_keys) - tag_values = merge(var.tag_values, local._ctx.tag_values) + project_ids = merge(var.project_ids, local._ctx.project_ids) + storage_buckets = merge(var.storage_buckets, local._ctx.storage_buckets) + tag_keys = merge(var.tag_keys, local._ctx.tag_keys) + tag_values = merge(var.tag_values, local._ctx.tag_values) + tag_vars = { + projects = try(local._ctx.tag_vars.projects, {}) + organization = merge({ + for k, v in var.tag_keys : k => v.namespaced_name + if v.allowed_values_regex != null + }, try(local._ctx.tag_vars.organization, {})) + } vpc_sc_perimeters = merge(var.perimeters, local._ctx.vpc_sc_perimeters) }) # normalize defaults diff --git a/fast/stages/2-security/schemas/defaults.schema.json b/fast/stages/2-security/schemas/defaults.schema.json index 048bf018e..9782fa052 100644 --- a/fast/stages/2-security/schemas/defaults.schema.json +++ b/fast/stages/2-security/schemas/defaults.schema.json @@ -495,6 +495,24 @@ "type": "string" } }, + "tag_vars": { + "type": "object", + "additionalProperties": false, + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "organization": { + "type": "string" + } + } + }, "vpc_host_projects": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-security/schemas/defaults.schema.md b/fast/stages/2-security/schemas/defaults.schema.md index 4bb55dd76..3287f67fb 100644 --- a/fast/stages/2-security/schemas/defaults.schema.md +++ b/fast/stages/2-security/schemas/defaults.schema.md @@ -144,6 +144,11 @@
*additional properties: string* - **tag_values**: *object*
*additional properties: string* + - **tag_vars**: *object* +
*additional properties: false* + - **projects**: *object* +
*additional properties: object* + - **organization**: *string* - **vpc_host_projects**: *object*
*additional properties: string* - **vpc_sc_perimeters**: *object* diff --git a/fast/stages/2-security/schemas/project.schema.json b/fast/stages/2-security/schemas/project.schema.json index 4f3b15610..32e1bd81f 100644 --- a/fast/stages/2-security/schemas/project.schema.json +++ b/fast/stages/2-security/schemas/project.schema.json @@ -834,6 +834,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "description": { "type": "string" }, @@ -873,7 +876,23 @@ } } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ] } }, "tag_bindings": { diff --git a/fast/stages/2-security/variables-fast.tf b/fast/stages/2-security/variables-fast.tf index 32a50f53d..6238f236c 100644 --- a/fast/stages/2-security/variables-fast.tf +++ b/fast/stages/2-security/variables-fast.tf @@ -91,9 +91,12 @@ variable "storage_buckets" { variable "tag_keys" { # tfdoc:variable:source 0-org-setup description = "FAST-managed resource manager tag keys." - type = map(string) - nullable = false - default = {} + type = map(object({ + namespaced_name = string + allowed_values_regex = optional(string) + })) + default = {} + nullable = false } variable "tag_values" { diff --git a/modules/agent-engine/main.tf b/modules/agent-engine/main.tf index 2195be6d1..7f3c8a75e 100644 --- a/modules/agent-engine/main.tf +++ b/modules/agent-engine/main.tf @@ -29,7 +29,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } location = lookup( local.ctx.locations, var.region, var.region diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md index def8441a4..f0b2dc80b 100644 --- a/modules/artifact-registry/README.md +++ b/modules/artifact-registry/README.md @@ -312,22 +312,22 @@ module "additive_iam" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [cleanup_policies](variables.tf#L17) | Object containing details about the cleanup policies for an Artifact Registry repository. | map(object({…default = null | ✓ | | -| [format](variables.tf#L79) | Repository format. | object({…}) | ✓ | | -| [location](variables.tf#L229) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | ✓ | | -| [name](variables.tf#L234) | Registry name. | string | ✓ | | -| [project_id](variables.tf#L239) | Registry project id. | string | ✓ | | +| [format](variables.tf#L83) | Repository format. | object({…}) | ✓ | | +| [location](variables.tf#L233) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | ✓ | | +| [name](variables.tf#L238) | Registry name. | string | ✓ | | +| [project_id](variables.tf#L243) | Registry project id. | string | ✓ | | | [cleanup_policy_dry_run](variables.tf#L38) | If true, the cleanup pipeline is prevented from deleting versions in this repository. | bool | | null | | [context](variables.tf#L44) | Context-specific interpolations. | object({…}) | | {} | -| [description](variables.tf#L61) | An optional description for the repository. | string | | "Terraform-managed registry" | -| [enable_vulnerability_scanning](variables.tf#L67) | Whether vulnerability scanning should be enabled in the repository. | bool | | null | -| [encryption_key](variables.tf#L73) | The KMS key name to use for encryption at rest. | string | | null | +| [description](variables.tf#L65) | An optional description for the repository. | string | | "Terraform-managed registry" | +| [enable_vulnerability_scanning](variables.tf#L71) | Whether vulnerability scanning should be enabled in the repository. | bool | | null | +| [encryption_key](variables.tf#L77) | The KMS key name to use for encryption at rest. | string | | null | | [iam](variables-iam.tf#L36) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L43) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L58) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L73) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | -| [labels](variables.tf#L223) | Labels to be attached to the registry. | map(string) | | {} | -| [tag_bindings](variables.tf#L244) | Tag bindings for this repository, in key => tag value id format. | map(string) | | {} | -| [universe](variables.tf#L251) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [labels](variables.tf#L227) | Labels to be attached to the registry. | map(string) | | {} | +| [tag_bindings](variables.tf#L248) | Tag bindings for this repository, in key => tag value id format. | map(string) | | {} | +| [universe](variables.tf#L255) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | ## Outputs diff --git a/modules/artifact-registry/main.tf b/modules/artifact-registry/main.tf index 1d270c04a..f11eebea8 100644 --- a/modules/artifact-registry/main.tf +++ b/modules/artifact-registry/main.tf @@ -18,9 +18,12 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") + } + ctx_p = "$" + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) } - ctx_p = "$" format_obj = one([for k, v in var.format : v if v != null]) format_string = one([for k, v in var.format : k if v != null]) location = lookup(local.ctx.locations, var.location, var.location) @@ -240,5 +243,5 @@ resource "google_tags_location_tag_binding" "binding" { for_each = var.tag_bindings parent = "//artifactregistry.googleapis.com/${google_artifact_registry_repository.registry.id}" location = local.location - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/artifact-registry/variables.tf b/modules/artifact-registry/variables.tf index 3d99c2ca6..d101da8ea 100644 --- a/modules/artifact-registry/variables.tf +++ b/modules/artifact-registry/variables.tf @@ -53,6 +53,10 @@ variable "context" { project_ids = optional(map(string), {}) secrets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/bigquery-connection/main.tf b/modules/bigquery-connection/main.tf index 13d0a81e5..b3f88498b 100644 --- a/modules/bigquery-connection/main.tf +++ b/modules/bigquery-connection/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" } diff --git a/modules/bigquery-dataset/README.md b/modules/bigquery-dataset/README.md index d43dfe1c9..1de1a9f53 100644 --- a/modules/bigquery-dataset/README.md +++ b/modules/bigquery-dataset/README.md @@ -359,30 +359,30 @@ module "bigquery-dataset" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [id](variables.tf#L111) | Dataset id. | string | ✓ | | -| [project_id](variables.tf#L175) | Id of the project where datasets will be created. | string | ✓ | | +| [id](variables.tf#L115) | Dataset id. | string | ✓ | | +| [project_id](variables.tf#L179) | Id of the project where datasets will be created. | string | ✓ | | | [access](variables.tf#L17) | Map of access rules with role and identity type. Keys are arbitrary and must match those in the `access_identities` variable, types are `domain`, `group`, `special_group`, `user`, `view`. | map(object({…})) | | {} | | [access_identities](variables.tf#L33) | Map of access identities used for basic access roles. View identities have the format 'project_id\|dataset_id\|table_id'. | map(string) | | {} | | [authorized_datasets](variables.tf#L39) | An array of datasets to be authorized on the dataset. | list(object({…})) | | [] | | [authorized_routines](variables.tf#L48) | An array of routines to be authorized on the dataset. | list(object({…})) | | [] | | [authorized_views](variables.tf#L58) | An array of views to be authorized on the dataset. | list(object({…})) | | [] | | [context](variables.tf#L68) | Context-specific interpolations. | object({…}) | | {} | -| [dataset_access](variables.tf#L83) | Set access in the dataset resource instead of using separate resources. | bool | | false | -| [description](variables.tf#L89) | Optional description. | string | | "Terraform managed." | -| [encryption_key](variables.tf#L95) | Self link of the KMS key that will be used to protect destination table. | string | | null | -| [friendly_name](variables.tf#L101) | Dataset friendly name. | string | | null | +| [dataset_access](variables.tf#L87) | Set access in the dataset resource instead of using separate resources. | bool | | false | +| [description](variables.tf#L93) | Optional description. | string | | "Terraform managed." | +| [encryption_key](variables.tf#L99) | Self link of the KMS key that will be used to protect destination table. | string | | null | +| [friendly_name](variables.tf#L105) | Dataset friendly name. | string | | null | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L23) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L38) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L53) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | -| [labels](variables.tf#L116) | Dataset labels. | map(string) | | {} | -| [location](variables.tf#L122) | Dataset location. | string | | "EU" | -| [materialized_views](variables.tf#L128) | Materialized views definitions. | map(object({…})) | | {} | -| [options](variables.tf#L161) | Dataset options. | object({…}) | | {} | -| [routines](variables.tf#L180) | Routine definitions. | map(object({…})) | | {} | -| [tables](variables.tf#L219) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L304) | Tag bindings for this dataset, in key => tag value id format. | map(string) | | {} | -| [views](variables.tf#L311) | View definitions. | map(object({…})) | | {} | +| [labels](variables.tf#L120) | Dataset labels. | map(string) | | {} | +| [location](variables.tf#L126) | Dataset location. | string | | "EU" | +| [materialized_views](variables.tf#L132) | Materialized views definitions. | map(object({…})) | | {} | +| [options](variables.tf#L165) | Dataset options. | object({…}) | | {} | +| [routines](variables.tf#L184) | Routine definitions. | map(object({…})) | | {} | +| [tables](variables.tf#L223) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L308) | Tag bindings for this dataset, in key => tag value id format. | map(string) | | {} | +| [views](variables.tf#L315) | View definitions. | map(object({…})) | | {} | ## Outputs diff --git a/modules/bigquery-dataset/main.tf b/modules/bigquery-dataset/main.tf index 83e4fd952..6f60ea97b 100644 --- a/modules/bigquery-dataset/main.tf +++ b/modules/bigquery-dataset/main.tf @@ -41,7 +41,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" ctx_kms_keys = try(local.ctx.kms_keys, {}) diff --git a/modules/bigquery-dataset/tags.tf b/modules/bigquery-dataset/tags.tf index 6f6d75e86..f3df99806 100644 --- a/modules/bigquery-dataset/tags.tf +++ b/modules/bigquery-dataset/tags.tf @@ -14,9 +14,15 @@ * limitations under the License. */ +locals { + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } +} + resource "google_tags_location_tag_binding" "binding" { for_each = var.tag_bindings parent = "//bigquery.googleapis.com/${google_bigquery_dataset.default.id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) location = lookup(local.ctx.locations, var.location, var.location) } diff --git a/modules/bigquery-dataset/variables.tf b/modules/bigquery-dataset/variables.tf index 03dc0b261..32bbf2572 100644 --- a/modules/bigquery-dataset/variables.tf +++ b/modules/bigquery-dataset/variables.tf @@ -75,6 +75,10 @@ variable "context" { locations = optional(map(string), {}) project_ids = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/certificate-authority-service/main.tf b/modules/certificate-authority-service/main.tf index 790092319..821b685d3 100644 --- a/modules/certificate-authority-service/main.tf +++ b/modules/certificate-authority-service/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" location = lookup(local.ctx.locations, var.location, var.location) diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index 0ad8a3309..bce86ffbc 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -19,7 +19,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } bucket = ( var.bucket_config == null diff --git a/modules/cloud-function-v2/main.tf b/modules/cloud-function-v2/main.tf index 84c889761..e5be5df0f 100644 --- a/modules/cloud-function-v2/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -19,7 +19,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } bucket = ( var.bucket_config == null diff --git a/modules/cloud-run-v2/README.md b/modules/cloud-run-v2/README.md index 5ae3d8359..2dedeada8 100644 --- a/modules/cloud-run-v2/README.md +++ b/modules/cloud-run-v2/README.md @@ -983,26 +983,26 @@ module "worker" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L178) | Name used for Cloud Run service. | string | ✓ | | -| [project_id](variables.tf#L183) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L188) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L182) | Name used for Cloud Run service. | string | ✓ | | +| [project_id](variables.tf#L187) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L192) | Region used for all resources. | string | ✓ | | | [containers](variables.tf#L17) | Containers in name => attributes format. | map(object({…})) | | {} | | [context](variables.tf#L97) | Context-specific interpolations. | object({…}) | | {} | -| [deletion_protection](variables.tf#L115) | Deletion protection setting for this Cloud Run service. | string | | null | -| [encryption_key](variables.tf#L121) | The full resource name of the Cloud KMS CryptoKey. | string | | null | -| [iam](variables.tf#L127) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [job_config](variables.tf#L133) | Cloud Run Job specific configuration. | object({…}) | | {} | -| [labels](variables.tf#L148) | Resource labels. | map(string) | | {} | -| [launch_stage](variables.tf#L154) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | -| [managed_revision](variables.tf#L171) | Whether the Terraform module should control the deployment of revisions. | bool | | true | -| [revision](variables.tf#L193) | Revision template configurations. | object({…}) | | {} | +| [deletion_protection](variables.tf#L119) | Deletion protection setting for this Cloud Run service. | string | | null | +| [encryption_key](variables.tf#L125) | The full resource name of the Cloud KMS CryptoKey. | string | | null | +| [iam](variables.tf#L131) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [job_config](variables.tf#L137) | Cloud Run Job specific configuration. | object({…}) | | {} | +| [labels](variables.tf#L152) | Resource labels. | map(string) | | {} | +| [launch_stage](variables.tf#L158) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | +| [managed_revision](variables.tf#L175) | Whether the Terraform module should control the deployment of revisions. | bool | | true | +| [revision](variables.tf#L197) | Revision template configurations. | object({…}) | | {} | | [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | -| [service_config](variables.tf#L260) | Cloud Run service specific configuration options. | object({…}) | | {} | -| [tag_bindings](variables.tf#L323) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | -| [type](variables.tf#L330) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | string | | "SERVICE" | -| [volumes](variables.tf#L340) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | +| [service_config](variables.tf#L264) | Cloud Run service specific configuration options. | object({…}) | | {} | +| [tag_bindings](variables.tf#L327) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | +| [type](variables.tf#L334) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | string | | "SERVICE" | +| [volumes](variables.tf#L344) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | | [vpc_connector_create](variables-vpcconnector.tf#L17) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | -| [workerpool_config](variables.tf#L374) | Cloud Run Worker Pool specific configuration. | object({…}) | | {} | +| [workerpool_config](variables.tf#L378) | Cloud Run Worker Pool specific configuration. | object({…}) | | {} | ## Outputs diff --git a/modules/cloud-run-v2/main.tf b/modules/cloud-run-v2/main.tf index 1ae0186f5..82c3dae66 100644 --- a/modules/cloud-run-v2/main.tf +++ b/modules/cloud-run-v2/main.tf @@ -19,7 +19,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } connector = ( var.vpc_connector_create != null diff --git a/modules/cloud-run-v2/tags.tf b/modules/cloud-run-v2/tags.tf index b461d1388..ae496233d 100644 --- a/modules/cloud-run-v2/tags.tf +++ b/modules/cloud-run-v2/tags.tf @@ -15,6 +15,9 @@ */ locals { + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } resource_types = { JOB = "jobs" SERVICE = "services" @@ -27,6 +30,6 @@ resource "google_tags_location_tag_binding" "binding" { parent = ( "//run.googleapis.com/projects/${local.project_id}/locations/${local.location}/${local.resource_types[var.type]}/${local.resource.name}" ) - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) location = local.location } diff --git a/modules/cloud-run-v2/variables.tf b/modules/cloud-run-v2/variables.tf index 2f78c9ee2..5d74fc09f 100644 --- a/modules/cloud-run-v2/variables.tf +++ b/modules/cloud-run-v2/variables.tf @@ -107,6 +107,10 @@ variable "context" { project_ids = optional(map(string), {}) subnets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) nullable = false default = {} diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md index d685e915c..5c7aa9a8b 100644 --- a/modules/compute-vm/README.md +++ b/modules/compute-vm/README.md @@ -1213,45 +1213,45 @@ module "sole-tenancy" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L354) | Instance name. | string | ✓ | | -| [network_interfaces](variables.tf#L366) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | -| [project_id](variables.tf#L406) | Project id. | string | ✓ | | -| [zone](variables.tf#L563) | Compute zone. | string | ✓ | | +| [name](variables.tf#L358) | Instance name. | string | ✓ | | +| [network_interfaces](variables.tf#L370) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | +| [project_id](variables.tf#L410) | Project id. | string | ✓ | | +| [zone](variables.tf#L567) | Compute zone. | string | ✓ | | | [attached_disks](variables.tf#L17) | Additional disks. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | map(object({…})) | | {} | | [boot_disk](variables.tf#L57) | Boot disk properties. | object({…}) | | {} | | [can_ip_forward](variables.tf#L114) | Enable IP forwarding. | bool | | false | | [confidential_compute](variables.tf#L120) | Confidential Compute configuration. Set to 'SEV' or 'SEV_SNP' to enable. | string | | null | | [context](variables.tf#L130) | Context-specific interpolations. | object({…}) | | {} | -| [create_template](variables.tf#L147) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | -| [description](variables.tf#L156) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | -| [enable_display](variables.tf#L162) | Enable virtual display on the instances. | bool | | false | -| [encryption](variables.tf#L168) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | -| [gpu](variables.tf#L179) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | -| [group](variables.tf#L214) | Instance group configuration. Set 'named_ports' to create a new unmanaged instance group, or provide an existing group self_link/id in 'membership' to join one. | object({…}) | | null | -| [hostname](variables.tf#L223) | Instance FQDN name. | string | | null | -| [iam](variables.tf#L229) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [instance_schedule](variables.tf#L235) | Assign or create and assign an instance schedule policy. Set active to null to detach a policy from vm before destroying. | object({…}) | | null | -| [kms_autokeys](variables.tf#L259) | KMS Autokey key handles. If location is not specified it will be inferred from the zone. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | -| [labels](variables.tf#L277) | Instance labels. | map(string) | | {} | -| [lifecycle_config](variables.tf#L283) | Instance lifecycle and operational configurations. | object({…}) | | {} | -| [machine_features_config](variables.tf#L305) | Machine-level configuration. | object({…}) | | {} | -| [machine_type](variables.tf#L329) | Machine type. | string | | "e2-micro" | -| [metadata](variables.tf#L335) | Instance metadata. | map(string) | | {} | -| [metadata_startup_script](variables.tf#L341) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | -| [min_cpu_platform](variables.tf#L348) | Minimum CPU platform. | string | | null | -| [network_attached_interfaces](variables.tf#L359) | Network interfaces using network attachments. | list(string) | | [] | -| [network_performance_tier](variables.tf#L389) | Network performance total egress bandwidth tier. | string | | null | -| [network_tag_bindings](variables.tf#L399) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [project_number](variables.tf#L411) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | -| [resource_policies](variables.tf#L417) | Resource policies to attach to the instance or template. | list(string) | | null | -| [scheduling_config](variables.tf#L424) | Scheduling configuration for the instance. | object({…}) | | {} | -| [scratch_disks](variables.tf#L459) | Scratch disks configuration. | object({…}) | | {…} | -| [service_account](variables.tf#L472) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | -| [shielded_config](variables.tf#L483) | Shielded VM configuration of the instances. | object({…}) | | null | -| [snapshot_schedules](variables.tf#L493) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L536) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [tag_bindings_immutable](variables.tf#L543) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | -| [tags](variables.tf#L557) | Instance network tags for firewall rule targets. | list(string) | | [] | +| [create_template](variables.tf#L151) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | +| [description](variables.tf#L160) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | +| [enable_display](variables.tf#L166) | Enable virtual display on the instances. | bool | | false | +| [encryption](variables.tf#L172) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | +| [gpu](variables.tf#L183) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | +| [group](variables.tf#L218) | Instance group configuration. Set 'named_ports' to create a new unmanaged instance group, or provide an existing group self_link/id in 'membership' to join one. | object({…}) | | null | +| [hostname](variables.tf#L227) | Instance FQDN name. | string | | null | +| [iam](variables.tf#L233) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [instance_schedule](variables.tf#L239) | Assign or create and assign an instance schedule policy. Set active to null to detach a policy from vm before destroying. | object({…}) | | null | +| [kms_autokeys](variables.tf#L263) | KMS Autokey key handles. If location is not specified it will be inferred from the zone. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | +| [labels](variables.tf#L281) | Instance labels. | map(string) | | {} | +| [lifecycle_config](variables.tf#L287) | Instance lifecycle and operational configurations. | object({…}) | | {} | +| [machine_features_config](variables.tf#L309) | Machine-level configuration. | object({…}) | | {} | +| [machine_type](variables.tf#L333) | Machine type. | string | | "e2-micro" | +| [metadata](variables.tf#L339) | Instance metadata. | map(string) | | {} | +| [metadata_startup_script](variables.tf#L345) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | +| [min_cpu_platform](variables.tf#L352) | Minimum CPU platform. | string | | null | +| [network_attached_interfaces](variables.tf#L363) | Network interfaces using network attachments. | list(string) | | [] | +| [network_performance_tier](variables.tf#L393) | Network performance total egress bandwidth tier. | string | | null | +| [network_tag_bindings](variables.tf#L403) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [project_number](variables.tf#L415) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | +| [resource_policies](variables.tf#L421) | Resource policies to attach to the instance or template. | list(string) | | null | +| [scheduling_config](variables.tf#L428) | Scheduling configuration for the instance. | object({…}) | | {} | +| [scratch_disks](variables.tf#L463) | Scratch disks configuration. | object({…}) | | {…} | +| [service_account](variables.tf#L476) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | +| [shielded_config](variables.tf#L487) | Shielded VM configuration of the instances. | object({…}) | | null | +| [snapshot_schedules](variables.tf#L497) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L540) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [tag_bindings_immutable](variables.tf#L547) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | +| [tags](variables.tf#L561) | Instance network tags for firewall rule targets. | list(string) | | [] | ## Outputs diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf index a5457b41d..4b7186417 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -22,7 +22,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } + } if !endswith(k, "_vars") } ctx_kms_keys = merge(local.ctx.kms_keys, { for k, v in google_kms_key_handle.default : diff --git a/modules/compute-vm/tags.tf b/modules/compute-vm/tags.tf index e99f13586..bd25336d6 100644 --- a/modules/compute-vm/tags.tf +++ b/modules/compute-vm/tags.tf @@ -17,8 +17,14 @@ # tfdoc:file:description Tag bindings. locals { + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } + _network_tag_bindings = { + for k, v in var.network_tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } boot_disk_tags = flatten([ - for k, v in var.tag_bindings : [ + for k, v in local._tag_bindings : [ for dk, dv in google_compute_disk.boot : { disk_id = dv.disk_id key = "${dk}/${k}" @@ -27,7 +33,7 @@ locals { ] ]) disk_tags = flatten([ - for k, v in var.tag_bindings : [ + for k, v in local._tag_bindings : [ for dk, dv in google_compute_disk.disks : { disk_id = dv.disk_id key = "${dk}/${k}" @@ -36,7 +42,7 @@ locals { ] ]) region_disk_tags = flatten([ - for k, v in var.tag_bindings : [ + for k, v in local._tag_bindings : [ for dk, dv in google_compute_region_disk.disks : { disk_id = dv.disk_id key = "${dk}/${k}" @@ -53,20 +59,20 @@ locals { # use a different resource to avoid overlapping key issues resource "google_tags_location_tag_binding" "network" { - for_each = local.is_template ? {} : var.network_tag_bindings + for_each = local.is_template ? {} : local._network_tag_bindings parent = ( "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(each.value, var.context.tag_vars) location = local.zone } resource "google_tags_location_tag_binding" "instance" { - for_each = local.is_template ? {} : var.tag_bindings + for_each = local.is_template ? {} : local._tag_bindings parent = ( "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(each.value, var.context.tag_vars) location = local.zone } @@ -77,7 +83,7 @@ resource "google_tags_location_tag_binding" "boot_disks" { parent = ( "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" ) - tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) + tag_value = templatestring(each.value.tag_value, var.context.tag_vars) location = local.zone } @@ -88,7 +94,7 @@ resource "google_tags_location_tag_binding" "disks" { parent = ( "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" ) - tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) + tag_value = templatestring(each.value.tag_value, var.context.tag_vars) location = local.zone } @@ -99,7 +105,7 @@ resource "google_tags_location_tag_binding" "disks_regional" { parent = ( "${local.tag_parent_base}/regions/${local.region}/disks/${each.value.disk_id}" ) - tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) + tag_value = templatestring(each.value.tag_value, var.context.tag_vars) location = local.region } diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf index 46e0dcb7d..626ee8e5e 100644 --- a/modules/compute-vm/variables.tf +++ b/modules/compute-vm/variables.tf @@ -139,6 +139,10 @@ variable "context" { project_ids = optional(map(string), {}) subnets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/data-catalog-policy-tag/main.tf b/modules/data-catalog-policy-tag/main.tf index 4b2677ccf..189dc9bb0 100644 --- a/modules/data-catalog-policy-tag/main.tf +++ b/modules/data-catalog-policy-tag/main.tf @@ -20,7 +20,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" location = try(local.ctx.locations[var.location], var.location) diff --git a/modules/dataplex-aspect-types/main.tf b/modules/dataplex-aspect-types/main.tf index 5f2f57097..6e8d137fa 100644 --- a/modules/dataplex-aspect-types/main.tf +++ b/modules/dataplex-aspect-types/main.tf @@ -40,7 +40,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" location = try(local.ctx.locations[var.location], var.location) diff --git a/modules/folder/README.md b/modules/folder/README.md index bfc5b02e6..2a56ae60a 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -760,29 +760,29 @@ module "folder" { | [autokey_config](variables.tf#L144) | Enable autokey support for this folder's children. Project accepts either project id or number. | object({…}) | | null | | [contacts](variables.tf#L153) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [context](variables.tf#L172) | Context-specific interpolations. | object({…}) | | {} | -| [deletion_protection](variables.tf#L193) | Deletion protection setting for this folder. | bool | | false | -| [factories_config](variables.tf#L199) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L211) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L222) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [deletion_protection](variables.tf#L197) | Deletion protection setting for this folder. | bool | | false | +| [factories_config](variables.tf#L203) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L215) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L226) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [id](variables.tf#L232) | Folder ID in case you use folder_create=false. | string | | null | +| [id](variables.tf#L236) | Folder ID in case you use folder_create=false. | string | | null | | [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-logging.tf#L28) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L45) | Logging sinks to create for the folder. | map(object({…})) | | {} | -| [name](variables.tf#L238) | Folder name. | string | | null | -| [org_policies](variables.tf#L244) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [name](variables.tf#L242) | Folder name. | string | | null | +| [org_policies](variables.tf#L248) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L272) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [parent](variables.tf#L276) | Parent in folders/folder_id or organizations/org_id format. | string | | null | | [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L27) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L286) | Service agents configuration. | object({…}) | | {} | -| [tag_bindings](variables.tf#L296) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [service_agents_config](variables.tf#L290) | Service agents configuration. | object({…}) | | {} | +| [tag_bindings](variables.tf#L300) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/main.tf b/modules/folder/main.tf index 2dc96584c..2d93d3876 100644 --- a/modules/folder/main.tf +++ b/modules/folder/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" _folder_id = ( diff --git a/modules/folder/tags.tf b/modules/folder/tags.tf index a0bfa472e..e12b17359 100644 --- a/modules/folder/tags.tf +++ b/modules/folder/tags.tf @@ -14,8 +14,14 @@ * limitations under the License. */ +locals { + _tag_bindings = { + for k, v in coalesce(var.tag_bindings, {}) : k => lookup(local.ctx.tag_values, v, v) + } +} + resource "google_tags_tag_binding" "binding" { for_each = coalesce(var.tag_bindings, {}) parent = "//cloudresourcemanager.googleapis.com/${local.folder_id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 8be11070a..5a4fb8509 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -185,6 +185,10 @@ variable "context" { pubsub_topics = optional(map(string), {}) storage_buckets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/gcs/README.md b/modules/gcs/README.md index 7bb5a7824..d5218277e 100644 --- a/modules/gcs/README.md +++ b/modules/gcs/README.md @@ -417,42 +417,42 @@ module "bucket" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L224) | Bucket name suffix. | string | ✓ | | +| [name](variables.tf#L228) | Bucket name suffix. | string | ✓ | | | [autoclass](variables.tf#L17) | Enable autoclass to automatically transition objects to appropriate storage classes based on their access pattern. If set to true, storage_class must be set to STANDARD. Defaults to false. | bool | | null | | [bucket_create](variables.tf#L23) | Create bucket. | bool | | true | | [context](variables.tf#L30) | Context-specific interpolations. | object({…}) | | {} | -| [cors](variables.tf#L46) | CORS configuration for the bucket. Defaults to null. | object({…}) | | null | -| [custom_placement_config](variables.tf#L57) | The bucket's custom location configuration, which specifies the individual regions that comprise a dual-region bucket. If the bucket is designated as REGIONAL or MULTI_REGIONAL, the parameters are empty. | list(string) | | null | -| [default_event_based_hold](variables.tf#L63) | Enable event based hold to new objects added to specific bucket, defaults to false. | bool | | null | -| [enable_hierarchical_namespace](variables.tf#L69) | Enables hierarchical namespace. | bool | | null | -| [enable_object_retention](variables.tf#L75) | Enables object retention on a storage bucket. | bool | | null | -| [encryption_key](variables.tf#L81) | KMS key that will be used for encryption. | string | | null | -| [force_destroy](variables.tf#L87) | Optional map to set force destroy keyed by name, defaults to false. | bool | | false | +| [cors](variables.tf#L50) | CORS configuration for the bucket. Defaults to null. | object({…}) | | null | +| [custom_placement_config](variables.tf#L61) | The bucket's custom location configuration, which specifies the individual regions that comprise a dual-region bucket. If the bucket is designated as REGIONAL or MULTI_REGIONAL, the parameters are empty. | list(string) | | null | +| [default_event_based_hold](variables.tf#L67) | Enable event based hold to new objects added to specific bucket, defaults to false. | bool | | null | +| [enable_hierarchical_namespace](variables.tf#L73) | Enables hierarchical namespace. | bool | | null | +| [enable_object_retention](variables.tf#L79) | Enables object retention on a storage bucket. | bool | | null | +| [encryption_key](variables.tf#L85) | KMS key that will be used for encryption. | string | | null | +| [force_destroy](variables.tf#L91) | Optional map to set force destroy keyed by name, defaults to false. | bool | | false | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L23) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L38) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L53) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | -| [ip_filter](variables.tf#L93) | The bucket's IP filter configuration. | object({…}) | | null | -| [kms_autokeys](variables.tf#L104) | KMS Autokey key handles. If location is not specified the bucket location will be used. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | -| [labels](variables.tf#L122) | Labels to be attached to all buckets. | map(string) | | {} | -| [lifecycle_rules](variables.tf#L128) | Bucket lifecycle rule. | map(object({…})) | | {} | -| [location](variables.tf#L177) | Bucket location. | string | | null | -| [logging_config](variables.tf#L187) | Bucket logging configuration. | object({…}) | | null | -| [managed_folders](variables.tf#L196) | Managed folders to create within the bucket in {PATH => CONFIG} format. | map(object({…})) | | {} | -| [notification_config](variables.tf#L229) | GCS Notification configuration. | object({…}) | | null | -| [objects_to_upload](variables.tf#L247) | Objects to be uploaded to bucket. | map(object({…})) | | {} | -| [prefix](variables.tf#L273) | Optional prefix used to generate the bucket name. | string | | null | -| [project_id](variables.tf#L283) | Bucket project id. Only required when creating buckets, or notification config topics. | string | | null | -| [public_access_prevention](variables.tf#L302) | Prevents public access to the bucket. | string | | null | -| [requester_pays](variables.tf#L312) | Enables Requester Pays on a storage bucket. | bool | | null | -| [retention_policy](variables.tf#L318) | Bucket retention policy. | object({…}) | | null | -| [rpo](variables.tf#L327) | Bucket recovery point objective. | string | | null | -| [soft_delete_retention](variables.tf#L337) | The duration in seconds that soft-deleted objects in the bucket will be retained and cannot be permanently deleted. Set to 0 to override the default and disable. | number | | null | -| [storage_class](variables.tf#L343) | Bucket storage class. | string | | "STANDARD" | -| [tag_bindings](variables.tf#L353) | Tag bindings for this folder, in key => tag value id format. | map(string) | | {} | -| [uniform_bucket_level_access](variables.tf#L360) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true | -| [versioning](variables.tf#L366) | Enable versioning, defaults to false. | bool | | null | -| [website](variables.tf#L372) | Bucket website. | object({…}) | | null | +| [ip_filter](variables.tf#L97) | The bucket's IP filter configuration. | object({…}) | | null | +| [kms_autokeys](variables.tf#L108) | KMS Autokey key handles. If location is not specified the bucket location will be used. Key handle names will be added to the kms_keys context with an `autokeys/` prefix. | map(object({…})) | | {} | +| [labels](variables.tf#L126) | Labels to be attached to all buckets. | map(string) | | {} | +| [lifecycle_rules](variables.tf#L132) | Bucket lifecycle rule. | map(object({…})) | | {} | +| [location](variables.tf#L181) | Bucket location. | string | | null | +| [logging_config](variables.tf#L191) | Bucket logging configuration. | object({…}) | | null | +| [managed_folders](variables.tf#L200) | Managed folders to create within the bucket in {PATH => CONFIG} format. | map(object({…})) | | {} | +| [notification_config](variables.tf#L233) | GCS Notification configuration. | object({…}) | | null | +| [objects_to_upload](variables.tf#L251) | Objects to be uploaded to bucket. | map(object({…})) | | {} | +| [prefix](variables.tf#L277) | Optional prefix used to generate the bucket name. | string | | null | +| [project_id](variables.tf#L287) | Bucket project id. Only required when creating buckets, or notification config topics. | string | | null | +| [public_access_prevention](variables.tf#L306) | Prevents public access to the bucket. | string | | null | +| [requester_pays](variables.tf#L316) | Enables Requester Pays on a storage bucket. | bool | | null | +| [retention_policy](variables.tf#L322) | Bucket retention policy. | object({…}) | | null | +| [rpo](variables.tf#L331) | Bucket recovery point objective. | string | | null | +| [soft_delete_retention](variables.tf#L341) | The duration in seconds that soft-deleted objects in the bucket will be retained and cannot be permanently deleted. Set to 0 to override the default and disable. | number | | null | +| [storage_class](variables.tf#L347) | Bucket storage class. | string | | "STANDARD" | +| [tag_bindings](variables.tf#L357) | Tag bindings for this folder, in key => tag value id format. | map(string) | | {} | +| [uniform_bucket_level_access](variables.tf#L364) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true | +| [versioning](variables.tf#L370) | Enable versioning, defaults to false. | bool | | null | +| [website](variables.tf#L376) | Bucket website. | object({…}) | | null | ## Outputs diff --git a/modules/gcs/main.tf b/modules/gcs/main.tf index 25b5b281e..01b089c51 100644 --- a/modules/gcs/main.tf +++ b/modules/gcs/main.tf @@ -19,7 +19,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_kms_keys = merge(local.ctx.kms_keys, { for k, v in google_kms_key_handle.default : "$kms_keys:autokeys/${k}" => v.kms_key diff --git a/modules/gcs/tags.tf b/modules/gcs/tags.tf index a8c894620..93fe9adf8 100644 --- a/modules/gcs/tags.tf +++ b/modules/gcs/tags.tf @@ -14,11 +14,19 @@ * limitations under the License. */ +locals { + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } +} + resource "google_tags_location_tag_binding" "binding" { - for_each = var.tag_bindings - parent = "//storage.googleapis.com/projects/_/buckets/${local._name}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) - location = lookup(local.ctx.locations, var.location, var.location) + for_each = var.tag_bindings + parent = "//storage.googleapis.com/projects/_/buckets/${local._name}" + tag_value = templatestring( + local._tag_bindings[each.key], var.context.tag_vars + ) + location = lookup(local.ctx.locations, var.location, var.location) depends_on = [ google_storage_bucket.bucket, google_storage_bucket_iam_binding.bindings diff --git a/modules/gcs/variables.tf b/modules/gcs/variables.tf index 671dd0f35..add19ff3c 100644 --- a/modules/gcs/variables.tf +++ b/modules/gcs/variables.tf @@ -38,6 +38,10 @@ variable "context" { project_ids = optional(map(string), {}) storage_buckets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md index 38e28713c..12be10cf0 100644 --- a/modules/iam-service-account/README.md +++ b/modules/iam-service-account/README.md @@ -151,11 +151,11 @@ module "service-account-with-tags" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L58) | Name of the service account to create. | string | ✓ | | +| [name](variables.tf#L62) | Name of the service account to create. | string | ✓ | | | [context](variables.tf#L17) | External context used in replacements. | object({…}) | | {} | -| [create_ignore_already_exists](variables.tf#L33) | If set to true, skip service account creation if a service account with the same email already exists. | bool | | null | -| [description](variables.tf#L44) | Optional description. | string | | null | -| [display_name](variables.tf#L51) | Display name of the service account to create. | string | | "Terraform-managed." | +| [create_ignore_already_exists](variables.tf#L37) | If set to true, skip service account creation if a service account with the same email already exists. | bool | | null | +| [description](variables.tf#L48) | Optional description. | string | | null | +| [display_name](variables.tf#L55) | Display name of the service account to create. | string | | "Terraform-managed." | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_billing_roles](variables-iam.tf#L24) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | @@ -167,11 +167,11 @@ module "service-account-with-tags" { | [iam_project_roles](variables-iam.tf#L89) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | | [iam_sa_roles](variables-iam.tf#L96) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | | [iam_storage_roles](variables-iam.tf#L103) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | -| [prefix](variables.tf#L64) | Prefix applied to service account names. | string | | null | -| [project_id](variables.tf#L75) | Project id where service account will be created. This can be left null when reusing service accounts. | string | | null | -| [project_number](variables.tf#L89) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. This can be left null when reusing service accounts and tags are not used. | string | | null | -| [service_account_reuse](variables.tf#L96) | Reuse existing service account if not null. Data source can be forced disabled if tag bindings are not used, or unique id is set. | object({…}) | | null | -| [tag_bindings](variables.tf#L112) | Tag bindings for this service accounts, in key => tag value id format. | map(string) | | {} | +| [prefix](variables.tf#L68) | Prefix applied to service account names. | string | | null | +| [project_id](variables.tf#L79) | Project id where service account will be created. This can be left null when reusing service accounts. | string | | null | +| [project_number](variables.tf#L93) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. This can be left null when reusing service accounts and tags are not used. | string | | null | +| [service_account_reuse](variables.tf#L100) | Reuse existing service account if not null. Data source can be forced disabled if tag bindings are not used, or unique id is set. | object({…}) | | null | +| [tag_bindings](variables.tf#L116) | Tag bindings for this service accounts, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/iam-service-account/main.tf b/modules/iam-service-account/main.tf index 3f61fe377..e69290a28 100644 --- a/modules/iam-service-account/main.tf +++ b/modules/iam-service-account/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" iam_email = ( @@ -75,6 +75,9 @@ locals { ? {} : var.tag_bindings ) + _tag_bindings = { + for k, v in local.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } } data "google_service_account" "service_account" { @@ -99,5 +102,5 @@ resource "google_service_account" "service_account" { resource "google_tags_tag_binding" "binding" { for_each = local.tag_bindings parent = "//iam.googleapis.com/projects/${coalesce(var.project_number, var.project_id)}/serviceAccounts/${local.service_account.unique_id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/iam-service-account/variables.tf b/modules/iam-service-account/variables.tf index 6577fb91d..610618725 100644 --- a/modules/iam-service-account/variables.tf +++ b/modules/iam-service-account/variables.tf @@ -25,6 +25,10 @@ variable "context" { service_account_ids = optional(map(string), {}) storage_buckets = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) nullable = false default = {} diff --git a/modules/kms/README.md b/modules/kms/README.md index b7bb14629..24cd0b33a 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -167,16 +167,16 @@ module "kms" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [keyring](variables.tf#L80) | Keyring attributes. | object({…}) | ✓ | | -| [project_id](variables.tf#L149) | Project id where the keyring will be created. | string | ✓ | | +| [keyring](variables.tf#L84) | Keyring attributes. | object({…}) | ✓ | | +| [project_id](variables.tf#L153) | Project id where the keyring will be created. | string | ✓ | | | [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [iam](variables.tf#L33) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L40) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L55) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [import_job](variables.tf#L70) | Keyring import job attributes. | object({…}) | | null | -| [keyring_create](variables.tf#L89) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | -| [keys](variables.tf#L95) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L154) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | +| [iam](variables.tf#L37) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L44) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L59) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [import_job](variables.tf#L74) | Keyring import job attributes. | object({…}) | | null | +| [keyring_create](variables.tf#L93) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | +| [keys](variables.tf#L99) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L158) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/kms/main.tf b/modules/kms/main.tf index e27e3ffd1..745ddd511 100644 --- a/modules/kms/main.tf +++ b/modules/kms/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" keyring = ( diff --git a/modules/kms/tags.tf b/modules/kms/tags.tf index 38425156b..fd5faa06a 100644 --- a/modules/kms/tags.tf +++ b/modules/kms/tags.tf @@ -14,9 +14,15 @@ * limitations under the License. */ +locals { + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } +} + resource "google_tags_location_tag_binding" "binding" { for_each = var.tag_bindings parent = "//cloudkms.googleapis.com/${local.keyring.id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) location = lookup(local.ctx.locations, var.keyring.location, var.keyring.location) } diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf index 5833bc5e0..a37858299 100644 --- a/modules/kms/variables.tf +++ b/modules/kms/variables.tf @@ -25,6 +25,10 @@ variable "context" { project_ids = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/logging-bucket/README.md b/modules/logging-bucket/README.md index 629170c25..e91d07005 100644 --- a/modules/logging-bucket/README.md +++ b/modules/logging-bucket/README.md @@ -118,18 +118,18 @@ module "bucket" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L71) | Name of the logging bucket. | string | ✓ | | -| [parent](variables.tf#L76) | ID of the parent resource containing the bucket in the format 'project_id' 'folders/folder_id', 'organizations/organization_id' or 'billing_account_id'. | string | ✓ | | +| [name](variables.tf#L75) | Name of the logging bucket. | string | ✓ | | +| [parent](variables.tf#L80) | ID of the parent resource containing the bucket in the format 'project_id' 'folders/folder_id', 'organizations/organization_id' or 'billing_account_id'. | string | ✓ | | | [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [description](variables.tf#L32) | Human-readable description for the logging bucket. | string | | null | -| [kms_key_name](variables.tf#L38) | To enable CMEK for a project logging bucket, set this field to a valid name. The associated service account requires cloudkms.cryptoKeyEncrypterDecrypter roles assigned for the key. | string | | null | -| [location](variables.tf#L44) | Location of the bucket. | string | | "global" | -| [locked](variables.tf#L50) | Whether the bucket is locked. Locked buckets may only be deleted if they are empty. This can only be set for project-level buckets. | bool | | null | -| [log_analytics](variables.tf#L60) | Enable and configure Analytics Log. | object({…}) | | {} | -| [parent_type](variables.tf#L84) | Parent object type for the bucket (project, folder, organization, billing_account). | string | | "project" | -| [retention](variables.tf#L91) | Retention time in days for the logging bucket. | number | | 30 | -| [tag_bindings](variables.tf#L97) | Tag bindings for this bucket, in key => tag value id format. | map(string) | | {} | -| [views](variables.tf#L104) | Log views for this bucket. | map(object({…})) | | {} | +| [description](variables.tf#L36) | Human-readable description for the logging bucket. | string | | null | +| [kms_key_name](variables.tf#L42) | To enable CMEK for a project logging bucket, set this field to a valid name. The associated service account requires cloudkms.cryptoKeyEncrypterDecrypter roles assigned for the key. | string | | null | +| [location](variables.tf#L48) | Location of the bucket. | string | | "global" | +| [locked](variables.tf#L54) | Whether the bucket is locked. Locked buckets may only be deleted if they are empty. This can only be set for project-level buckets. | bool | | null | +| [log_analytics](variables.tf#L64) | Enable and configure Analytics Log. | object({…}) | | {} | +| [parent_type](variables.tf#L88) | Parent object type for the bucket (project, folder, organization, billing_account). | string | | "project" | +| [retention](variables.tf#L95) | Retention time in days for the logging bucket. | number | | 30 | +| [tag_bindings](variables.tf#L101) | Tag bindings for this bucket, in key => tag value id format. | map(string) | | {} | +| [views](variables.tf#L108) | Log views for this bucket. | map(object({…})) | | {} | ## Outputs diff --git a/modules/logging-bucket/main.tf b/modules/logging-bucket/main.tf index 0c1148a6d..94dd856ae 100644 --- a/modules/logging-bucket/main.tf +++ b/modules/logging-bucket/main.tf @@ -24,9 +24,12 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } + } if !endswith(k, "_vars") } ctx_p = "$" + _tag_bindings = { + for k, v in var.tag_bindings : k => lookup(local.ctx.tag_values, v, v) + } location = lookup( local.ctx.locations, var.location, var.location ) @@ -110,5 +113,5 @@ resource "google_logging_log_view" "views" { resource "google_tags_tag_binding" "binding" { for_each = var.tag_bindings parent = "//logging.googleapis.com/${local.bucket.id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/logging-bucket/variables.tf b/modules/logging-bucket/variables.tf index 3cc60d55b..eec083281 100644 --- a/modules/logging-bucket/variables.tf +++ b/modules/logging-bucket/variables.tf @@ -24,6 +24,10 @@ variable "context" { locations = optional(map(string), {}) project_ids = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/organization/README.md b/modules/organization/README.md index 2ba7d4f8d..48e26a5d5 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -771,6 +771,10 @@ module "org" { source = "./fabric/modules/organization" organization_id = var.organization_id tags = { + cost_center = { + description = "Cost center code." + allowed_values_regex = "^cc-[0-9]{3}$" + } environment = { description = "Environment specification." iam = { @@ -832,7 +836,7 @@ module "org" { env-prod = module.org.tag_values["environment/prod"].id } } -# tftest modules=1 resources=11 inventory=tags.yaml +# tftest modules=1 resources=12 inventory=tags.yaml ``` You can also define network tags, through a dedicated variable *network_tags*: @@ -1029,14 +1033,14 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L173) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L177) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | | [asset_search](variables.tf#L51) | Cloud Asset Inventory search configurations. | map(object({…})) | | {} | | [contacts](variables.tf#L61) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [context](variables.tf#L79) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L100) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L107) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L122) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [custom_roles](variables.tf#L104) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [factories_config](variables.tf#L111) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L126) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | @@ -1048,15 +1052,15 @@ module "org" { | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L45) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L131) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L159) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L135) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L163) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | | [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L28) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L182) | Service agents configuration. | object({…}) | | {} | +| [service_agents_config](variables.tf#L186) | Service agents configuration. | object({…}) | | {} | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L96) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [tags_config](variables-tags.tf#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | +| [tags_config](variables-tags.tf#L171) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | | [workforce_identity_pools](variables-identity-providers.tf#L17) | Workforce Identity Federation pools and providers. | map(object({…})) | | {} | ## Outputs diff --git a/modules/organization/main.tf b/modules/organization/main.tf index 8036e4fce..ccf85e132 100644 --- a/modules/organization/main.tf +++ b/modules/organization/main.tf @@ -18,7 +18,7 @@ locals { _ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } # add service agents into the iam_principals context namespace ctx = merge(local._ctx, { diff --git a/modules/organization/schemas/tags.schema.json b/modules/organization/schemas/tags.schema.json index 64e2827b0..22b5357b4 100644 --- a/modules/organization/schemas/tags.schema.json +++ b/modules/organization/schemas/tags.schema.json @@ -4,6 +4,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "name": { "type": "string" }, @@ -56,6 +59,22 @@ } } }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ], "$defs": { "iam": { "type": "object", diff --git a/modules/organization/schemas/tags.schema.md b/modules/organization/schemas/tags.schema.md index a7b5bc865..592e01ada 100644 --- a/modules/organization/schemas/tags.schema.md +++ b/modules/organization/schemas/tags.schema.md @@ -6,6 +6,7 @@ *additional properties: false* +- **allowed_values_regex**: *string* - **name**: *string* - **description**: *string* - **id**: *string* diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index 4a94dad7a..8ad212525 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -26,6 +26,7 @@ locals { for f, v_raw in local._factory_tags_data_raw : coalesce(lookup(v_raw, "name", null), trimsuffix(f, ".yaml")) => { id = lookup(v_raw, "id", null) + allowed_values_regex = lookup(v_raw, "allowed_values_regex", null) description = lookup(v_raw, "description", null) iam = lookup(v_raw, "iam", {}) iam_bindings = lookup(v_raw, "iam_bindings", {}) @@ -48,7 +49,8 @@ locals { id = v.id != null ? v.id : ( var.tags_config.force_context_ids == true ? "$tag_keys:${k}" : null ) - description = v.description + allowed_values_regex = lookup(v, "allowed_values_regex", null) + description = v.description iam = var.tags_config.ignore_iam == true ? {} : { for ik, iv in v.iam : ik => coalesce(iv, []) } @@ -171,6 +173,9 @@ locals { tag_values = { for v in local._tag_values : v.key => v } + _tag_bindings = { + for k, v in coalesce(var.tag_bindings, {}) : k => lookup(local.ctx.tag_values, v, v) + } } # keys @@ -186,8 +191,9 @@ resource "google_tags_tag_key" "default" { each.value.network == "ALL" ? { organization = "auto" } : { network = each.value.network } ) ) - short_name = each.key - description = each.value.description + short_name = each.key + description = each.value.description + allowed_values_regex = each.value.allowed_values_regex depends_on = [ google_organization_iam_binding.authoritative, google_organization_iam_binding.bindings, @@ -335,5 +341,5 @@ resource "google_tags_tag_value_iam_member" "bindings" { resource "google_tags_tag_binding" "binding" { for_each = coalesce(var.tag_bindings, {}) parent = "//cloudresourcemanager.googleapis.com/${var.organization_id}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/organization/variables-tags.tf b/modules/organization/variables-tags.tf index dae95563d..573b1d1c8 100644 --- a/modules/organization/variables-tags.tf +++ b/modules/organization/variables-tags.tf @@ -96,8 +96,9 @@ variable "tag_bindings" { variable "tags" { description = "Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level." type = map(object({ - description = optional(string, "Managed by the Terraform organization module.") - iam = optional(map(list(string)), {}) + allowed_values_regex = optional(string) + description = optional(string, "Managed by the Terraform organization module.") + iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) role = string @@ -156,6 +157,15 @@ variable "tags" { ) error_message = "Use an empty map instead of null as value." } + validation { + condition = alltrue([ + for k, v in var.tags : + v.allowed_values_regex == null || ( + length(v.values) == 0 + ) + ]) + error_message = "If allowed_values_regex is set, values must not be set." + } } variable "tags_config" { diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 27181c4f0..fca1360f5 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -92,6 +92,10 @@ variable "context" { storage_buckets = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) nullable = false default = {} diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index da99713d1..c65d836a6 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -881,11 +881,11 @@ compute.disableSerialPortAccess: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factories_config](variables.tf#L166) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [factories_config](variables.tf#L170) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | | [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [data_defaults](variables.tf#L43) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L108) | 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#L127) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [data_defaults](variables.tf#L47) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L112) | 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#L131) | 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({…})) | | {} | diff --git a/modules/project-factory/folders.tf b/modules/project-factory/folders.tf index fa51f46d6..35e33ec34 100644 --- a/modules/project-factory/folders.tf +++ b/modules/project-factory/folders.tf @@ -69,7 +69,6 @@ module "folder-1" { } org_policies = lookup(each.value, "org_policies", {}) pam_entitlements = lookup(each.value, "pam_entitlements", {}) - tag_bindings = lookup(each.value, "tag_bindings", {}) assured_workload_config = lookup(each.value, "assured_workload_config", null) logging_settings = anytrue([ try(each.value.logging.storage_location, null) != null, @@ -107,7 +106,12 @@ module "folder-1-iam" { iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) + tag_bindings = lookup(each.value, "tag_bindings", {}) context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = local.ctx_iam_principals kms_keys = merge(local.ctx.kms_keys, local.kms_keys) project_ids = local.ctx_project_ids @@ -137,7 +141,6 @@ module "folder-2" { } org_policies = lookup(each.value, "org_policies", {}) pam_entitlements = lookup(each.value, "pam_entitlements", {}) - tag_bindings = lookup(each.value, "tag_bindings", {}) assured_workload_config = lookup(each.value, "assured_workload_config", null) logging_settings = anytrue([ try(each.value.logging.storage_location, null) != null, @@ -180,10 +183,15 @@ module "folder-2-iam" { iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) + tag_bindings = lookup(each.value, "tag_bindings", {}) context = merge(local.ctx, { folder_ids = merge(local.ctx.folder_ids, { for k, v in module.folder-1 : k => v.id }) + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = local.ctx_iam_principals kms_keys = merge(local.ctx.kms_keys, local.kms_keys) project_ids = local.ctx_project_ids @@ -213,7 +221,6 @@ module "folder-3" { } org_policies = lookup(each.value, "org_policies", {}) pam_entitlements = lookup(each.value, "pam_entitlements", {}) - tag_bindings = lookup(each.value, "tag_bindings", {}) assured_workload_config = lookup(each.value, "assured_workload_config", null) logging_settings = anytrue([ try(each.value.logging.storage_location, null) != null, @@ -256,10 +263,15 @@ module "folder-3-iam" { iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) + tag_bindings = lookup(each.value, "tag_bindings", {}) context = merge(local.ctx, { folder_ids = merge(local.ctx.folder_ids, { for k, v in module.folder-2 : k => v.id }) + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = local.ctx_iam_principals kms_keys = merge(local.ctx.kms_keys, local.kms_keys) project_ids = local.ctx_project_ids @@ -289,7 +301,6 @@ module "folder-4" { } org_policies = lookup(each.value, "org_policies", {}) pam_entitlements = lookup(each.value, "pam_entitlements", {}) - tag_bindings = lookup(each.value, "tag_bindings", {}) assured_workload_config = lookup(each.value, "assured_workload_config", null) logging_settings = anytrue([ try(each.value.logging.storage_location, null) != null, @@ -332,10 +343,15 @@ module "folder-4-iam" { iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) + tag_bindings = lookup(each.value, "tag_bindings", {}) context = merge(local.ctx, { folder_ids = merge(local.ctx.folder_ids, { for k, v in module.folder-3 : k => v.id }) + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = local.ctx_iam_principals kms_keys = merge(local.ctx.kms_keys, local.kms_keys) project_ids = local.ctx_project_ids diff --git a/modules/project-factory/projects-bigquery.tf b/modules/project-factory/projects-bigquery.tf index fd6590e12..7c5af5994 100644 --- a/modules/project-factory/projects-bigquery.tf +++ b/modules/project-factory/projects-bigquery.tf @@ -51,6 +51,10 @@ module "bigquery-datasets" { project_id = module.projects-iam[each.value.project_key].project_id id = each.value.id context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = merge( local.ctx.iam_principals, local.projects_sas_iam_emails, diff --git a/modules/project-factory/projects-buckets.tf b/modules/project-factory/projects-buckets.tf index c510377bb..b310a71b0 100644 --- a/modules/project-factory/projects-buckets.tf +++ b/modules/project-factory/projects-buckets.tf @@ -73,6 +73,10 @@ module "buckets" { encryption_key = each.value.encryption_key force_destroy = each.value.force_destroy context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = merge( local.ctx.iam_principals, local.projects_sas_iam_emails, diff --git a/modules/project-factory/projects-defaults.tf b/modules/project-factory/projects-defaults.tf index 07f1182f7..6b3842a84 100644 --- a/modules/project-factory/projects-defaults.tf +++ b/modules/project-factory/projects-defaults.tf @@ -149,6 +149,7 @@ locals { ) tags = { for tag_name, tag_data in try(v.tags, {}) : tag_name => { + allowed_values_regex = try(tag_data.allowed_values_regex, null) description = try( tag_data.description, "Managed by the Terraform project-factory module." diff --git a/modules/project-factory/projects-kms.tf b/modules/project-factory/projects-kms.tf index 3550cdb5b..866f1a0e9 100644 --- a/modules/project-factory/projects-kms.tf +++ b/modules/project-factory/projects-kms.tf @@ -68,6 +68,10 @@ module "kms" { tag_bindings = each.value.tag_bindings keys = each.value.keys context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } iam_principals = merge( local.ctx.iam_principals, local.projects_sas_iam_emails, diff --git a/modules/project-factory/projects-service-accounts.tf b/modules/project-factory/projects-service-accounts.tf index f1c50894c..b24b8a899 100644 --- a/modules/project-factory/projects-service-accounts.tf +++ b/modules/project-factory/projects-service-accounts.tf @@ -85,6 +85,10 @@ module "service-accounts" { description = each.value.description display_name = each.value.display_name context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } project_ids = local.ctx_project_ids tag_values = local.ctx_tag_values }) diff --git a/modules/project-factory/projects.tf b/modules/project-factory/projects.tf index 2e9e593cb..6b94138c7 100644 --- a/modules/project-factory/projects.tf +++ b/modules/project-factory/projects.tf @@ -63,6 +63,13 @@ locals { } ]...) : k => v }) + tag_vars_projects = { + for k, v in local.projects_input : v.name => { + for kk, vv in module.projects[k].tag_keys : + kk => vv.namespaced_name + if vv.allowed_values_regex != null + } + } per_project_service_agents = { for k, v in module.projects : k => { for kk, vv in v.service_agents : @@ -169,6 +176,10 @@ module "projects-iam" { } } context = merge(local.ctx, { + tag_vars = { + projects = merge(try(local.ctx.tag_vars.projects, {}), local.tag_vars_projects) + organization = try(local.ctx.tag_vars.organization, {}) + } folder_ids = local.ctx.folder_ids kms_keys = merge(local.ctx.kms_keys, local.kms_keys) iam_principals = merge( diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json index 4f3b15610..32e1bd81f 100644 --- a/modules/project-factory/schemas/project.schema.json +++ b/modules/project-factory/schemas/project.schema.json @@ -834,6 +834,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "description": { "type": "string" }, @@ -873,7 +876,23 @@ } } } - } + }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ] } }, "tag_bindings": { diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index 09badec0b..3b4c59fa6 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -33,8 +33,12 @@ variable "context" { 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), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) + vpc_host_projects = optional(map(string), {}) + vpc_sc_perimeters = optional(map(string), {}) }) default = {} nullable = false diff --git a/modules/project/README.md b/modules/project/README.md index aa6955046..d5572c214 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -1097,6 +1097,10 @@ module "project" { "compute.googleapis.com", ] tags = { + cost_center = { + description = "Cost center code." + allowed_values_regex = "^cc-[0-9]{3}$" + } environment = { description = "Environment specification." iam = { @@ -1149,7 +1153,7 @@ module "project" { env-prod = module.project.tag_values["environment/prod"].id } } -# tftest modules=1 resources=13 inventory=tags.yaml +# tftest modules=1 resources=14 inventory=tags.yaml ``` You can also define network tags through the dedicated `network_tags` variable: @@ -2282,7 +2286,7 @@ module "project" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L248) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L252) | Project name and id suffix. | string | ✓ | | | [alerts](variables-observability.tf#L17) | Monitoring alerts. | map(object({…})) | | {} | | [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | | [asset_search](variables.tf#L51) | Cloud Asset Inventory search configurations. | map(object({…})) | | {} | @@ -2292,21 +2296,21 @@ module "project" { | [compute_metadata](variables.tf#L110) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | | [contacts](variables.tf#L117) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [context](variables.tf#L135) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L158) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_network_tier](variables.tf#L165) | Default compute network tier for the project. | string | | null | -| [default_service_account](variables.tf#L171) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [deletion_policy](variables.tf#L184) | Deletion policy setting for this project. | string | | "DELETE" | -| [descriptive_name](variables.tf#L195) | Descriptive project name. Set when name differs from project id. | string | | null | -| [factories_config](variables.tf#L201) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [custom_roles](variables.tf#L162) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [default_network_tier](variables.tf#L169) | Default compute network tier for the project. | string | | null | +| [default_service_account](variables.tf#L175) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [deletion_policy](variables.tf#L188) | Deletion policy setting for this project. | string | | "DELETE" | +| [descriptive_name](variables.tf#L199) | Descriptive project name. Set when name differs from project id. | string | | null | +| [factories_config](variables.tf#L205) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [kms_autokeys](variables.tf#L217) | KMS Autokey key handles. | map(object({…})) | | {} | -| [labels](variables.tf#L235) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L242) | If non-empty, creates a project lien with this description. | string | | null | +| [kms_autokeys](variables.tf#L221) | KMS Autokey key handles. | map(object({…})) | | {} | +| [labels](variables.tf#L239) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L246) | If non-empty, creates a project lien with this description. | string | | null | | [log_scopes](variables-observability.tf#L117) | Log scopes under this project. | map(object({…})) | | {} | | [logging_data_access](variables-observability.tf#L127) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-observability.tf#L138) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | @@ -2315,26 +2319,26 @@ module "project" { | [metric_scopes](variables-observability.tf#L216) | List of projects that will act as metric scopes for this project. | list(string) | | [] | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [notification_channels](variables-observability.tf#L223) | Monitoring notification channels. | map(object({…})) | | {} | -| [org_policies](variables.tf#L253) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L257) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L281) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L295) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L305) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [parent](variables.tf#L285) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L299) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L309) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | | [quotas](variables-quotas.tf#L17) | Service quota configuration. | map(object({…})) | | {} | | [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L28) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L325) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L336) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L348) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L355) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L361) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L371) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L408) | Deprecated. Use deletion_policy. | bool | | null | +| [service_agents_config](variables.tf#L329) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L340) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L352) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L359) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L365) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L375) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L412) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L96) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [tags_config](variables-tags.tf#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | -| [universe](variables.tf#L420) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L431) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [tags_config](variables-tags.tf#L171) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | +| [universe](variables.tf#L424) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L435) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | | [workload_identity_pools](variables-identity-providers.tf#L17) | Workload Identity Federation pools and providers. | map(object({…})) | | {} | ## Outputs diff --git a/modules/project/main.tf b/modules/project/main.tf index f50d6e2e4..8d9659782 100644 --- a/modules/project/main.tf +++ b/modules/project/main.tf @@ -36,7 +36,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" descriptive_name = ( diff --git a/modules/project/schemas/tags.schema.json b/modules/project/schemas/tags.schema.json index 64e2827b0..22b5357b4 100644 --- a/modules/project/schemas/tags.schema.json +++ b/modules/project/schemas/tags.schema.json @@ -4,6 +4,9 @@ "type": "object", "additionalProperties": false, "properties": { + "allowed_values_regex": { + "type": "string" + }, "name": { "type": "string" }, @@ -56,6 +59,22 @@ } } }, + "allOf": [ + { + "if": { + "required": [ + "allowed_values_regex" + ] + }, + "then": { + "not": { + "required": [ + "values" + ] + } + } + } + ], "$defs": { "iam": { "type": "object", diff --git a/modules/project/schemas/tags.schema.md b/modules/project/schemas/tags.schema.md index a7b5bc865..592e01ada 100644 --- a/modules/project/schemas/tags.schema.md +++ b/modules/project/schemas/tags.schema.md @@ -6,6 +6,7 @@ *additional properties: false* +- **allowed_values_regex**: *string* - **name**: *string* - **description**: *string* - **id**: *string* diff --git a/modules/project/tags.tf b/modules/project/tags.tf index a3d46452c..28c39ff06 100644 --- a/modules/project/tags.tf +++ b/modules/project/tags.tf @@ -26,6 +26,7 @@ locals { for f, v_raw in local._factory_tags_data_raw : coalesce(lookup(v_raw, "name", null), trimsuffix(f, ".yaml")) => { id = lookup(v_raw, "id", null) + allowed_values_regex = lookup(v_raw, "allowed_values_regex", null) description = lookup(v_raw, "description", null) iam = lookup(v_raw, "iam", {}) iam_bindings = lookup(v_raw, "iam_bindings", {}) @@ -48,7 +49,8 @@ locals { id = v.id != null ? v.id : ( var.tags_config.force_context_ids == true ? "$tag_keys:${var.name}/${k}" : null ) - description = v.description + allowed_values_regex = lookup(v, "allowed_values_regex", null) + description = v.description iam = var.tags_config.ignore_iam == true ? {} : { for ik, iv in v.iam : ik => coalesce(iv, []) } @@ -171,6 +173,9 @@ locals { tag_values = { for v in local._tag_values : v.key => v } + _tag_bindings = { + for k, v in coalesce(var.tag_bindings, {}) : k => lookup(local.ctx.tag_values, v, v) + } } # keys @@ -186,8 +191,9 @@ resource "google_tags_tag_key" "default" { each.value.network == "ALL" ? { organization = "auto" } : { network = each.value.network } ) ) - short_name = each.key - description = each.value.description + short_name = each.key + description = each.value.description + allowed_values_regex = each.value.allowed_values_regex # depends_on = [ # google_organization_iam_binding.authoritative, # google_organization_iam_binding.bindings, @@ -335,5 +341,5 @@ resource "google_tags_tag_value_iam_member" "bindings" { resource "google_tags_tag_binding" "binding" { for_each = coalesce(var.tag_bindings, {}) parent = "//cloudresourcemanager.googleapis.com/projects/${local.project.number}" - tag_value = lookup(local.ctx.tag_values, each.value, each.value) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/project/variables-tags.tf b/modules/project/variables-tags.tf index 080a7a66f..e454fc188 100644 --- a/modules/project/variables-tags.tf +++ b/modules/project/variables-tags.tf @@ -96,9 +96,10 @@ variable "tag_bindings" { variable "tags" { description = "Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level." type = map(object({ - id = optional(string) - description = optional(string, "Managed by the Terraform project module.") - iam = optional(map(list(string)), {}) + allowed_values_regex = optional(string) + id = optional(string) + description = optional(string, "Managed by the Terraform project module.") + iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) role = string @@ -156,6 +157,15 @@ variable "tags" { ) error_message = "Use an empty map instead of null as value." } + validation { + condition = alltrue([ + for k, v in var.tags : + v.allowed_values_regex == null || ( + length(v.values) == 0 + ) + ]) + error_message = "If allowed_values_regex is set, values must not be set." + } } variable "tags_config" { diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 8a4d495f3..d1f76fa81 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -149,7 +149,11 @@ variable "context" { storage_buckets = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) - vpc_sc_perimeters = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) + vpc_sc_perimeters = optional(map(string), {}) }) default = {} nullable = false diff --git a/modules/secret-manager/README.md b/modules/secret-manager/README.md index 430554c37..beff66756 100644 --- a/modules/secret-manager/README.md +++ b/modules/secret-manager/README.md @@ -214,10 +214,10 @@ module "secret-manager" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L40) | Project id where the keyring will be created. | string | ✓ | | +| [project_id](variables.tf#L44) | Project id where the keyring will be created. | string | ✓ | | | [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [project_number](variables.tf#L45) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | string | | null | -| [secrets](variables.tf#L51) | Map of secrets to manage. Defaults to global secrets unless region is set. | map(object({…})) | | {} | +| [project_number](variables.tf#L49) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | string | | null | +| [secrets](variables.tf#L55) | Map of secrets to manage. Defaults to global secrets unless region is set. | map(object({…})) | | {} | ## Outputs diff --git a/modules/secret-manager/global.tf b/modules/secret-manager/global.tf index 7e9dac302..749ed29a7 100644 --- a/modules/secret-manager/global.tf +++ b/modules/secret-manager/global.tf @@ -127,5 +127,5 @@ resource "google_tags_tag_binding" "binding" { local.tag_project, google_secret_manager_secret.default[each.value.secret].secret_id ) - tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/secret-manager/main.tf b/modules/secret-manager/main.tf index 03e854d87..792fe6ff0 100644 --- a/modules/secret-manager/main.tf +++ b/modules/secret-manager/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) @@ -33,6 +33,9 @@ locals { } if v.tag_bindings != null ]...) + _tag_bindings = { + for k, v in local.tag_bindings : k => lookup(local.ctx.tag_values, v.tag, v.tag) + } versions = flatten([ for k, v in var.secrets : [ for sk, sv in v.versions : merge(sv, { diff --git a/modules/secret-manager/regional.tf b/modules/secret-manager/regional.tf index bfc3c2674..640157810 100644 --- a/modules/secret-manager/regional.tf +++ b/modules/secret-manager/regional.tf @@ -86,5 +86,5 @@ resource "google_tags_location_tag_binding" "binding" { google_secret_manager_regional_secret.default[each.value.secret].secret_id ) location = lookup(local.ctx.locations, each.value.location, each.value.location) - tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag) + tag_value = templatestring(local._tag_bindings[each.key], var.context.tag_vars) } diff --git a/modules/secret-manager/variables.tf b/modules/secret-manager/variables.tf index 5b1731211..311891d57 100644 --- a/modules/secret-manager/variables.tf +++ b/modules/secret-manager/variables.tf @@ -25,6 +25,10 @@ variable "context" { project_ids = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) + tag_vars = optional(object({ + projects = optional(map(map(string)), {}) + organization = optional(map(string), {}) + }), {}) }) default = {} nullable = false diff --git a/modules/workstation-cluster/main.tf b/modules/workstation-cluster/main.tf index 51836bccf..508514667 100644 --- a/modules/workstation-cluster/main.tf +++ b/modules/workstation-cluster/main.tf @@ -18,7 +18,7 @@ locals { ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv - } if k != "condition_vars" + } if !endswith(k, "_vars") } ctx_p = "$" workstation_configs = merge( diff --git a/tests/fast/stages/s0_org_setup/hardened.yaml b/tests/fast/stages/s0_org_setup/hardened.yaml index 879dbf01b..538f943b4 100644 --- a/tests/fast/stages/s0_org_setup/hardened.yaml +++ b/tests/fast/stages/s0_org_setup/hardened.yaml @@ -1261,6 +1261,8 @@ values: module.factory.module.folder-1-iam["teams"].google_folder_iam_binding.authoritative["roles/storage.admin"]: condition: [] role: roles/storage.admin + module.factory.module.folder-1-iam["teams"].google_tags_tag_binding.binding["context"]: + timeouts: null module.factory.module.folder-1["networking"].google_folder.folder[0]: deletion_protection: false display_name: Networking @@ -1279,8 +1281,6 @@ values: parent: organizations/1234567890 tags: null timeouts: null - module.factory.module.folder-1["teams"].google_tags_tag_binding.binding["context"]: - timeouts: null ? module.factory.module.folder-2-iam["networking/dev"].google_folder_iam_binding.authoritative["$custom_roles:project_iam_viewer"] : condition: [] role: organizations/1234567890/roles/projectIamViewer @@ -1291,37 +1291,37 @@ values: \ 'organizations/1234567890/roles/serviceProjectNetworkAdmin'\n])\n" title: Data platform dev delegated IAM grant. role: roles/resourcemanager.projectIamAdmin + module.factory.module.folder-2-iam["networking/dev"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-2-iam["networking/prod"].google_tags_tag_binding.binding["environment"]: + timeouts: null ? module.factory.module.folder-2-iam["security/dev"].google_folder_iam_binding.authoritative["$custom_roles:cloudkms_viewer"] : condition: [] role: organizations/1234567890/roles/cloudKmsViewer + module.factory.module.folder-2-iam["security/dev"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-2-iam["security/prod"].google_tags_tag_binding.binding["environment"]: + timeouts: null module.factory.module.folder-2["networking/dev"].google_folder.folder[0]: deletion_protection: false display_name: Development tags: null timeouts: null - module.factory.module.folder-2["networking/dev"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["networking/prod"].google_folder.folder[0]: deletion_protection: false display_name: Production tags: null timeouts: null - module.factory.module.folder-2["networking/prod"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["security/dev"].google_folder.folder[0]: deletion_protection: false display_name: Development tags: null timeouts: null - module.factory.module.folder-2["security/dev"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["security/prod"].google_folder.folder[0]: deletion_protection: false display_name: Production tags: null timeouts: null - module.factory.module.folder-2["security/prod"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.kms["billing-0/ew1"].google_kms_crypto_key.default["bigquery"]: effective_labels: goog-terraform-provisioned: 'true' @@ -8399,50 +8399,6 @@ values: output: null triggers_replace: null -counts: - google_bigquery_dataset: 1 - google_bigquery_default_service_account: 2 - google_billing_account_iam_member: 6 - google_essential_contacts_contact: 1 - google_folder: 7 - google_folder_iam_binding: 74 - google_kms_crypto_key: 3 - google_kms_crypto_key_iam_member: 3 - google_kms_key_ring: 3 - google_logging_metric: 10 - google_logging_organization_settings: 1 - google_logging_organization_sink: 3 - google_logging_project_bucket_config: 6 - google_logging_project_settings: 3 - google_monitoring_alert_policy: 10 - google_org_policy_custom_constraint: 89 - google_org_policy_policy: 167 - google_organization_iam_audit_config: 1 - google_organization_iam_binding: 40 - google_organization_iam_custom_role: 14 - google_project: 3 - google_project_iam_binding: 64 - google_project_iam_member: 17 - google_project_service: 37 - google_project_service_identity: 11 - google_scc_management_organization_security_health_analytics_custom_module: 18 - google_service_account: 12 - google_service_account_iam_binding: 2 - google_service_account_iam_member: 4 - google_storage_bucket: 3 - google_storage_bucket_iam_binding: 4 - google_storage_bucket_object: 10 - google_storage_managed_folder: 4 - google_storage_managed_folder_iam_binding: 8 - google_storage_project_service_account: 3 - google_tags_tag_binding: 5 - google_tags_tag_key: 3 - google_tags_tag_value: 6 - google_tags_tag_value_iam_binding: 4 - local_file: 9 - modules: 52 - resources: 675 - terraform_data: 4 outputs: iam_principals: diff --git a/tests/fast/stages/s0_org_setup/simple.yaml b/tests/fast/stages/s0_org_setup/simple.yaml index 39d9e2954..2787bc6dd 100644 --- a/tests/fast/stages/s0_org_setup/simple.yaml +++ b/tests/fast/stages/s0_org_setup/simple.yaml @@ -972,6 +972,8 @@ values: module.factory.module.folder-1-iam["teams"].google_folder_iam_binding.authoritative["roles/viewer"]: condition: [] role: roles/viewer + module.factory.module.folder-1-iam["teams"].google_tags_tag_binding.binding["context"]: + timeouts: null module.factory.module.folder-1["networking"].google_folder.folder[0]: deletion_protection: false display_name: Networking @@ -990,8 +992,6 @@ values: parent: organizations/1234567890 tags: null timeouts: null - module.factory.module.folder-1["teams"].google_tags_tag_binding.binding["context"]: - timeouts: null ? module.factory.module.folder-2-iam["networking/dev"].google_folder_iam_binding.authoritative["$custom_roles:project_iam_viewer"] : condition: [] role: organizations/1234567890/roles/projectIamViewer @@ -1002,34 +1002,34 @@ values: \ 'organizations/1234567890/roles/serviceProjectNetworkAdmin'\n])\n" title: Data platform dev delegated IAM grant. role: roles/resourcemanager.projectIamAdmin + module.factory.module.folder-2-iam["networking/dev"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-2-iam["networking/prod"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-2-iam["security/dev"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-2-iam["security/prod"].google_tags_tag_binding.binding["environment"]: + timeouts: null module.factory.module.folder-2["networking/dev"].google_folder.folder[0]: deletion_protection: false display_name: Development tags: null timeouts: null - module.factory.module.folder-2["networking/dev"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["networking/prod"].google_folder.folder[0]: deletion_protection: false display_name: Production tags: null timeouts: null - module.factory.module.folder-2["networking/prod"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["security/dev"].google_folder.folder[0]: deletion_protection: false display_name: Development tags: null timeouts: null - module.factory.module.folder-2["security/dev"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-2["security/prod"].google_folder.folder[0]: deletion_protection: false display_name: Production tags: null timeouts: null - module.factory.module.folder-2["security/prod"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.log-buckets["log-0/audit-logs"].google_logging_project_bucket_config.bucket[0]: bucket_id: audit-logs cmek_settings: [] @@ -2893,7 +2893,7 @@ counts: google_tags_tag_value: 5 google_tags_tag_value_iam_binding: 4 local_file: 9 - modules: 44 + modules: 47 resources: 308 terraform_data: 4 diff --git a/tests/fast/stages/s0_org_setup/starter-gcd.yaml b/tests/fast/stages/s0_org_setup/starter-gcd.yaml index a8fae262c..d776eddae 100644 --- a/tests/fast/stages/s0_org_setup/starter-gcd.yaml +++ b/tests/fast/stages/s0_org_setup/starter-gcd.yaml @@ -278,22 +278,22 @@ values: condition: [] role: roles/storage.admin timeouts: null + module.factory.module.folder-1-iam["dev"].google_tags_tag_binding.binding["environment"]: + timeouts: null + module.factory.module.folder-1-iam["prod"].google_tags_tag_binding.binding["environment"]: + timeouts: null module.factory.module.folder-1["dev"].google_folder.folder[0]: deletion_protection: false display_name: Development parent: organizations/1234567890 tags: null timeouts: null - module.factory.module.folder-1["dev"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.folder-1["prod"].google_folder.folder[0]: deletion_protection: false display_name: Production parent: organizations/1234567890 tags: null timeouts: null - module.factory.module.folder-1["prod"].google_tags_tag_binding.binding["environment"]: - timeouts: null module.factory.module.log-buckets["iac-0/audit-logs"].google_logging_project_bucket_config.bucket[0]: bucket_id: audit-logs cmek_settings: [] @@ -1176,6 +1176,7 @@ values: delete_default_routes_on_create: true description: Terraform managed enable_ula_internal_ipv6: null + mtu: 1500 name: dev-shared-0 network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL network_profile: null @@ -1248,6 +1249,7 @@ values: delete_default_routes_on_create: true description: Terraform managed enable_ula_internal_ipv6: null + mtu: 1500 name: prod-shared-0 network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL network_profile: null @@ -1322,7 +1324,6 @@ values: input: null output: null triggers_replace: null - counts: google_bigquery_dataset: 1 google_bigquery_default_service_account: 3 @@ -1353,7 +1354,7 @@ counts: google_tags_tag_value: 2 google_tags_tag_value_iam_binding: 4 local_file: 4 - modules: 25 + modules: 27 resources: 173 terraform_data: 4 diff --git a/tests/fast/stages/s2_project_factory/simple.yaml b/tests/fast/stages/s2_project_factory/simple.yaml index 058e4a133..5988faa40 100644 --- a/tests/fast/stages/s2_project_factory/simple.yaml +++ b/tests/fast/stages/s2_project_factory/simple.yaml @@ -23,6 +23,6 @@ counts: google_project_service_identity: 2 google_storage_bucket_object: 1 google_tags_tag_binding: 2 - modules: 8 + modules: 10 resources: 26 terraform_data: 2 diff --git a/tests/modules/bigquery_dataset/context.tfvars b/tests/modules/bigquery_dataset/context.tfvars index de038c230..12ae90347 100644 --- a/tests/modules/bigquery_dataset/context.tfvars +++ b/tests/modules/bigquery_dataset/context.tfvars @@ -23,6 +23,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } } project_id = "$project_ids:test" id = "dataset_0" @@ -37,5 +44,7 @@ iam = { ] } tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } diff --git a/tests/modules/bigquery_dataset/context.yaml b/tests/modules/bigquery_dataset/context.yaml index 6550af94b..343caded8 100644 --- a/tests/modules/bigquery_dataset/context.yaml +++ b/tests/modules/bigquery_dataset/context.yaml @@ -48,14 +48,22 @@ values: - serviceAccount:test@test-project.iam.gserviceaccount.com project: foo-test-0 role: roles/viewer - google_tags_location_tag_binding.binding["foo"]: + google_tags_location_tag_binding.binding["bar"]: + location: europe-west8 + tag_value: tagValues/1234567891 + timeouts: null + google_tags_location_tag_binding.binding["baz"]: location: europe-west8 tag_value: tagValues/1234567890 timeouts: null + google_tags_location_tag_binding.binding["foo"]: + location: europe-west8 + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null counts: google_bigquery_dataset: 1 google_bigquery_dataset_iam_binding: 2 - google_tags_location_tag_binding: 1 + google_tags_location_tag_binding: 3 modules: 0 - resources: 4 + resources: 6 diff --git a/tests/modules/cloud_run_v2/context.tfvars b/tests/modules/cloud_run_v2/context.tfvars index 16d5bb097..bf19aeb91 100644 --- a/tests/modules/cloud_run_v2/context.tfvars +++ b/tests/modules/cloud_run_v2/context.tfvars @@ -24,6 +24,16 @@ context = { subnets = { test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" } + tag_values = { + "test/one" = "tagValues/1234567890" + } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } } kms_key = "$kms_keys:test" iam = { @@ -52,3 +62,8 @@ vpc_connector_create = { min = 3 } } +tag_bindings = { + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" +} diff --git a/tests/modules/cloud_run_v2/context.yaml b/tests/modules/cloud_run_v2/context.yaml index 8cd587478..bdfbec77d 100644 --- a/tests/modules/cloud_run_v2/context.yaml +++ b/tests/modules/cloud_run_v2/context.yaml @@ -79,8 +79,21 @@ values: region: europe-west8 subnet: [] timeouts: null + google_tags_location_tag_binding.binding["bar"]: + location: europe-west8 + tag_value: tagValues/1234567891 + timeouts: null + google_tags_location_tag_binding.binding["baz"]: + location: europe-west8 + tag_value: tagValues/1234567890 + timeouts: null + google_tags_location_tag_binding.binding["foo"]: + location: europe-west8 + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null counts: google_cloud_run_v2_service: 1 google_cloud_run_v2_service_iam_binding: 1 + google_tags_location_tag_binding: 3 google_vpc_access_connector: 1 diff --git a/tests/modules/compute_vm/context-vm.tfvars b/tests/modules/compute_vm/context-vm.tfvars index 9c8e4e8ab..806574647 100644 --- a/tests/modules/compute_vm/context-vm.tfvars +++ b/tests/modules/compute_vm/context-vm.tfvars @@ -30,6 +30,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } } encryption = { encrypt_boot = true @@ -52,6 +59,8 @@ network_interfaces = [{ }] project_id = "$project_ids:test" tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } zone = "$locations:ew8a" diff --git a/tests/modules/compute_vm/context-vm.yaml b/tests/modules/compute_vm/context-vm.yaml index eaa02ec82..988794ebb 100644 --- a/tests/modules/compute_vm/context-vm.yaml +++ b/tests/modules/compute_vm/context-vm.yaml @@ -146,18 +146,34 @@ values: project: foo-test-0 role: organizations/366118655033/roles/myRoleOne zone: europe-west8-a + google_tags_location_tag_binding.disks["data-0/bar"]: + location: europe-west8-a + tag_value: tagValues/1234567891 + timeouts: null + google_tags_location_tag_binding.disks["data-0/baz"]: + location: europe-west8-a + tag_value: tagValues/1234567890 + timeouts: null google_tags_location_tag_binding.disks["data-0/foo"]: + location: europe-west8-a + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null + google_tags_location_tag_binding.instance["bar"]: + location: europe-west8-a + tag_value: tagValues/1234567891 + timeouts: null + google_tags_location_tag_binding.instance["baz"]: location: europe-west8-a tag_value: tagValues/1234567890 timeouts: null google_tags_location_tag_binding.instance["foo"]: location: europe-west8-a - tag_value: tagValues/1234567890 + tag_value: foo-test-0/dynamic_test/cc-123 timeouts: null counts: google_compute_disk: 1 google_compute_instance: 1 google_compute_instance_iam_binding: 1 - google_tags_location_tag_binding: 2 + google_tags_location_tag_binding: 6 modules: 0 - resources: 5 + resources: 9 diff --git a/tests/modules/folder/context.tfvars b/tests/modules/folder/context.tfvars index 3ff9670bc..82d94481b 100644 --- a/tests/modules/folder/context.tfvars +++ b/tests/modules/folder/context.tfvars @@ -30,6 +30,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } } asset_feeds = { test = { @@ -126,5 +133,7 @@ pam_entitlements = { } } tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } diff --git a/tests/modules/folder/context.yaml b/tests/modules/folder/context.yaml index af68b8651..f5c640da2 100644 --- a/tests/modules/folder/context.yaml +++ b/tests/modules/folder/context.yaml @@ -145,9 +145,15 @@ values: project: test-prod-audit-logs-0 role: roles/pubsub.publisher topic: audit-logs - google_tags_tag_binding.binding["foo"]: + google_tags_tag_binding.binding["bar"]: + tag_value: tagValues/1234567891 + timeouts: null + google_tags_tag_binding.binding["baz"]: tag_value: tagValues/1234567890 timeouts: null + google_tags_tag_binding.binding["foo"]: + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null counts: google_cloud_asset_folder_feed: 1 @@ -160,6 +166,6 @@ counts: google_logging_folder_sink: 1 google_privileged_access_manager_entitlement: 1 google_pubsub_topic_iam_member: 1 - google_tags_tag_binding: 1 + google_tags_tag_binding: 3 modules: 0 - resources: 17 + resources: 19 diff --git a/tests/modules/gcs/context.tfvars b/tests/modules/gcs/context.tfvars index b339fddb7..dbfe9939a 100644 --- a/tests/modules/gcs/context.tfvars +++ b/tests/modules/gcs/context.tfvars @@ -22,6 +22,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "dev-test-00/dynamic_test" + } + } + } } project_id = "myproject" name = "mybucket" @@ -93,5 +100,7 @@ managed_folders = { } } tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } diff --git a/tests/modules/gcs/context.yaml b/tests/modules/gcs/context.yaml index 1773e439d..53bb74a45 100644 --- a/tests/modules/gcs/context.yaml +++ b/tests/modules/gcs/context.yaml @@ -117,7 +117,7 @@ values: google_tags_location_tag_binding.binding["foo"]: location: europe-west8 parent: //storage.googleapis.com/projects/_/buckets/mybucket - tag_value: tagValues/1234567890 + tag_value: dev-test-00/dynamic_test/cc-123 timeouts: null counts: @@ -127,9 +127,9 @@ counts: google_storage_managed_folder: 1 google_storage_managed_folder_iam_binding: 3 google_storage_managed_folder_iam_member: 1 - google_tags_location_tag_binding: 1 + google_tags_location_tag_binding: 3 modules: 0 - resources: 14 + resources: 16 outputs: bucket: __missing__ diff --git a/tests/modules/kms/context.tfvars b/tests/modules/kms/context.tfvars index 4eeca0f38..ee2409416 100644 --- a/tests/modules/kms/context.tfvars +++ b/tests/modules/kms/context.tfvars @@ -22,6 +22,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } } project_id = "myproject" keyring = { @@ -107,5 +114,7 @@ iam_by_principals = { ] } tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } diff --git a/tests/modules/kms/context.yaml b/tests/modules/kms/context.yaml index 4f5c2fbe5..5920fb64f 100644 --- a/tests/modules/kms/context.yaml +++ b/tests/modules/kms/context.yaml @@ -98,9 +98,18 @@ values: condition: [] member: serviceAccount:test@test-project.iam.gserviceaccount.com role: organizations/366118655033/roles/myRoleThree - google_tags_location_tag_binding.binding["foo"]: + google_tags_location_tag_binding.binding["bar"]: + location: europe-west8 + tag_value: tagValues/1234567891 + timeouts: null + google_tags_location_tag_binding.binding["baz"]: location: europe-west8 tag_value: tagValues/1234567890 + timeouts: null + google_tags_location_tag_binding.binding["foo"]: + location: europe-west8 + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null counts: google_kms_crypto_key: 3 @@ -109,6 +118,6 @@ counts: google_kms_key_ring: 1 google_kms_key_ring_iam_binding: 3 google_kms_key_ring_iam_member: 1 - google_tags_location_tag_binding: 1 + google_tags_location_tag_binding: 3 modules: 0 - resources: 13 + resources: 15 diff --git a/tests/modules/organization/examples/tags.yaml b/tests/modules/organization/examples/tags.yaml index 43d8044ab..ca52182e2 100644 --- a/tests/modules/organization/examples/tags.yaml +++ b/tests/modules/organization/examples/tags.yaml @@ -72,13 +72,22 @@ values: member: group:app2-team@example.org role: roles/resourcemanager.tagAdmin + module.org.google_tags_tag_key.default["cost_center"]: + allowed_values_regex: "^cc-[0-9]{3}$" + description: Cost center code. + parent: organizations/1122334455 + purpose: null + purpose_data: null + short_name: cost_center + timeouts: null + counts: google_tags_tag_binding: 1 - google_tags_tag_key: 1 + google_tags_tag_key: 2 google_tags_tag_key_iam_binding: 2 google_tags_tag_key_iam_member: 1 google_tags_tag_value: 2 google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 2 modules: 1 - resources: 11 + resources: 12 diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars index db7277832..61d2258f7 100644 --- a/tests/modules/project/context.tfvars +++ b/tests/modules/project/context.tfvars @@ -31,6 +31,13 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + tag_vars = { + projects = { + "test-00" = { + test = "foo-test-0/dynamic_test" + } + } + } log_buckets = { audit = "logging.googleapis.com/projects/my-project/locations/global/buckets/audit-bucket" } @@ -200,7 +207,9 @@ iam_by_principals_conditional = { } } tag_bindings = { - foo = "$tag_values:test/one" + bar = "tagValues/1234567891" + baz = "$tag_values:test/one" + foo = "$${projects[\"test-00\"].test}/cc-123" } tags = { test = { diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml index b45ea1424..a78fb09bd 100644 --- a/tests/modules/project/context.yaml +++ b/tests/modules/project/context.yaml @@ -274,9 +274,15 @@ values: project: test-prod-audit-logs-0 role: roles/pubsub.publisher topic: audit-logs - google_tags_tag_binding.binding["foo"]: + google_tags_tag_binding.binding["bar"]: + tag_value: tagValues/1234567891 + timeouts: null + google_tags_tag_binding.binding["baz"]: tag_value: tagValues/1234567890 timeouts: null + google_tags_tag_binding.binding["foo"]: + tag_value: foo-test-0/dynamic_test/cc-123 + timeouts: null google_tags_tag_key_iam_binding.bindings["test:tag_user"]: condition: [] members: @@ -329,10 +335,10 @@ counts: google_project_iam_member: 7 google_project_service: 1 google_pubsub_topic_iam_member: 1 - google_tags_tag_binding: 1 + google_tags_tag_binding: 3 google_tags_tag_key_iam_binding: 2 google_tags_tag_key_iam_member: 1 google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 36 + resources: 38 diff --git a/tests/modules/project/examples/tags.yaml b/tests/modules/project/examples/tags.yaml index 411581f1c..cd54af8a0 100644 --- a/tests/modules/project/examples/tags.yaml +++ b/tests/modules/project/examples/tags.yaml @@ -35,7 +35,16 @@ values: timeouts: null module.project.google_tags_tag_binding.binding["env-prod"]: timeouts: null + module.project.google_tags_tag_key.default["cost_center"]: + allowed_values_regex: "^cc-[0-9]{3}$" + description: Cost center code. + parent: projects/test-project + purpose: null + purpose_data: null + short_name: cost_center + timeouts: null module.project.google_tags_tag_key.default["environment"]: + allowed_values_regex: null description: Environment specification. parent: projects/test-project purpose: null @@ -89,13 +98,13 @@ counts: google_project_iam_member: 1 google_project_service: 1 google_tags_tag_binding: 1 - google_tags_tag_key: 1 + google_tags_tag_key: 2 google_tags_tag_key_iam_binding: 2 google_tags_tag_key_iam_member: 1 google_tags_tag_value: 2 google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 1 - resources: 13 + resources: 14 outputs: {} diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml index d01b9e74a..b83294bb6 100644 --- a/tests/modules/project_factory/examples/example.yaml +++ b/tests/modules/project_factory/examples/example.yaml @@ -290,7 +290,7 @@ values: display_name: App 0 tags: null timeouts: null - module.project-factory.module.folder-2["team-b/app-0"].google_tags_tag_binding.binding["drs-allow-all"]: + module.project-factory.module.folder-2-iam["team-b/app-0"].google_tags_tag_binding.binding["drs-allow-all"]: tag_value: tagValues/123456 timeouts: null module.project-factory.module.folder-2["team-c/apps"].google_folder.folder[0]: @@ -995,6 +995,6 @@ counts: google_tags_tag_key: 1 google_tags_tag_value: 2 google_tags_tag_value_iam_binding: 1 - modules: 34 + modules: 35 resources: 119 terraform_data: 2