diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index 7e5577e9a..3111cd8b0 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -327,18 +327,18 @@ terraform apply | [organization](variables-fast.tf#L135) | Organization details. | object({…}) | ✓ | | 0-bootstrap | | [prefix](variables-fast.tf#L165) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | | [custom_roles](variables-fast.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | -| [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | | +| [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_addon](variables-addons.tf#L17) | FAST addons configurations for stages 2. Keys are used as short names for the add-on resources. | map(object({…})) | | {} | | | [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | map(object({…})) | | {} | | | [fast_stage_3](variables-stages.tf#L117) | FAST stages 3 configurations. | map(object({…})) | | {} | | | [groups](variables-fast.tf#L93) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | | [locations](variables-fast.tf#L109) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | | [org_policy_tags](variables-fast.tf#L153) | Organization policy tags. | object({…}) | | {} | 0-bootstrap | -| [outputs_location](variables.tf#L37) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [resource_names](variables.tf#L43) | Resource names overrides for specific resources. Stage names are interpolated via `$${name}`. Prefix is always set via code, except where noted in the variable type. | object({…}) | | {} | | +| [outputs_location](variables.tf#L38) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [resource_names](variables.tf#L44) | Resource names overrides for specific resources. Stage names are interpolated via `$${name}`. Prefix is always set via code, except where noted in the variable type. | object({…}) | | {} | | | [root_node](variables-fast.tf#L171) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap | -| [tag_names](variables.tf#L63) | Customized names for resource management tags. | object({…}) | | {} | | -| [tags](variables.tf#L77) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | +| [tag_names](variables.tf#L64) | Customized names for resource management tags. | object({…}) | | {} | | +| [tags](variables.tf#L78) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | | [top_level_folders](variables-toplevel-folders.tf#L17) | Additional top-level folders. Keys are used for service account and bucket names, values implement the folders module interface with the addition of the 'automation' attribute. | map(object({…})) | | {} | | ## Outputs diff --git a/fast/stages/1-resman/main.tf b/fast/stages/1-resman/main.tf index 4d20a18cc..665a5a3bb 100644 --- a/fast/stages/1-resman/main.tf +++ b/fast/stages/1-resman/main.tf @@ -75,6 +75,9 @@ locals { top_level_service_accounts = { for k, v in module.top-level-sa : k => try(v.email) } + top_level_service_accounts_iam = { + for k, v in local.top_level_service_accounts : k => "serviceAccount:${v}" + } # leaving this here to document how to get self identity in a stage # automation_resman_sa = try( # data.google_client_openid_userinfo.provider_identity[0].email, null diff --git a/fast/stages/1-resman/organization.tf b/fast/stages/1-resman/organization.tf index 63e062923..b6e02e9d9 100644 --- a/fast/stages/1-resman/organization.tf +++ b/fast/stages/1-resman/organization.tf @@ -42,8 +42,24 @@ module "organization" { factories_config = { tags = var.factories_config.tags context = { - tag_keys = var.factories_config.context.tag_keys - tag_values = var.factories_config.context.tag_values + iam_principals = merge( + var.factories_config.context.iam_principals, + local.top_level_service_accounts_iam, + local.stage_service_accounts_iam + ) + tag_keys = merge( + var.factories_config.context.tag_keys, + { + (var.org_policy_tags.key_name) = var.org_policy_tags.key_id + } + ) + tag_values = merge( + var.factories_config.context.tag_values, + { + for k, v in var.org_policy_tags.values : + "${var.org_policy_tags.key_name}/${k}" => v + } + ) } } # do not assign tagViewer or tagUser roles here on tag keys and values as diff --git a/fast/stages/1-resman/schemas/tags.schema.json b/fast/stages/1-resman/schemas/tags.schema.json new file mode 120000 index 000000000..87081ef23 --- /dev/null +++ b/fast/stages/1-resman/schemas/tags.schema.json @@ -0,0 +1 @@ +../../../../modules/organization/schemas/tags.schema.json \ No newline at end of file diff --git a/fast/stages/1-resman/schemas/tags.schema.md b/fast/stages/1-resman/schemas/tags.schema.md new file mode 100644 index 000000000..d33e47b15 --- /dev/null +++ b/fast/stages/1-resman/schemas/tags.schema.md @@ -0,0 +1,60 @@ +# Resource Manager Tags + + + +## Properties + +*additional properties: false* + +- **name**: *string* +- **description**: *string* +- **id**: *string* +- **network**: *string* +- **iam**: *reference([iam](#refs-iam))* +- **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* +- **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* +- **values**: *object* +
*additional properties: false* + - **`^[a-z-][a-z0-9-]+$`**: *object* +
*additional properties: false* + - **name**: *string* + - **description**: *string* + - **id**: *string* + - **iam**: *reference([iam](#refs-iam))* + - **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* + - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* + +## Definitions + +- **iam**: *object* +
*additional properties: false* + - **`^roles/`**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* +- **iam_bindings**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **members**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^roles/* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* +- **iam_bindings_additive**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **member**: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^[a-zA-Z0-9_/]+$* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf index d0abaf90d..8bb0a96d7 100644 --- a/fast/stages/1-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -25,9 +25,10 @@ variable "factories_config" { tags = optional(string, "data/tags") top_level_folders = optional(string, "data/top-level-folders") context = optional(object({ - org_policies = optional(map(map(string)), {}) - tag_keys = optional(map(string), {}) - tag_values = optional(map(string), {}) + iam_principals = optional(map(string), {}) + org_policies = optional(map(map(string)), {}) + tag_keys = optional(map(string), {}) + tag_values = optional(map(string), {}) }), {}) }) nullable = false diff --git a/modules/net-vpc/schemas/subnet.schema.md b/modules/net-vpc/schemas/subnet.schema.md index 0022471c6..1d735e38e 100644 --- a/modules/net-vpc/schemas/subnet.schema.md +++ b/modules/net-vpc/schemas/subnet.schema.md @@ -23,8 +23,8 @@ - **ipv6**: *object*
*additional properties: false* - **access_type**: *string* - - +**ipv6_only**: *boolean* -- ⁺**ip_collection**: *string* + - **ipv6_only**: *boolean* +- **ip_collection**: *string* - **name**: *string* - ⁺**region**: *string* - **psc**: *boolean* diff --git a/modules/organization/README.md b/modules/organization/README.md index 4ed1b120a..fa35cec52 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -529,13 +529,24 @@ module "org" { organization_id = var.organization_id factories_config = { tags = "data/tags" + context = { + iam_principals = { + gcp-admins = "group:gcp-organization-admins@example.com" + } + tag_keys = { + foo = "tagKeys/1234567890" + } + tag_values = { + "foo/bar" = "tagValues/789012345" + } + } } } -# tftest modules=1 resources=4 files=cost-center inventory=tags-factory.yaml +# tftest modules=1 resources=7 files=0,1 inventory=tags-factory.yaml ``` ```yaml -# tftest-file id=cost-center path=data/tags/cost-center.yaml +# tftest-file id=0 path=data/tags/cost-center.yaml description: "Tag for internal cost allocation." iam: @@ -548,6 +559,26 @@ values: description: "Marketing department." ``` +```yaml +# tftest-file id=1 path=data/tags/foo.yaml + +description: "Tag that shows context expansion." +id: foo +iam: + "roles/resourcemanager.tagViewer": + - "group:finance-team@example.com" + - gcp-admins +values: + bar: + id: foo/bar + description: "This tag value will be reused." + iam: + "roles/resourcemanager.tagViewer": + - gcp-admins + baz: + description: "This tag value will be created." +``` + ## Files @@ -571,11 +602,11 @@ values: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L99) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L100) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | 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)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L48) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L49) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | | [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({…})) | | {} | @@ -586,8 +617,8 @@ values: | [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#L57) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L85) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L58) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L86) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L81) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L88) | 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({…})) | | {} | diff --git a/modules/organization/schemas/tags.schema.json b/modules/organization/schemas/tags.schema.json new file mode 100644 index 000000000..08312c959 --- /dev/null +++ b/modules/organization/schemas/tags.schema.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Resource Manager Tags", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "network": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "values": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z-][a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + } + } + } + } + } + }, + "$defs": { + "iam": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^roles/": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + } + } + }, + "iam_bindings": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "members": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + }, + "role": { + "type": "string", + "pattern": "^roles/" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "title" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + }, + "iam_bindings_additive": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "member": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + }, + "role": { + "type": "string", + "pattern": "^[a-zA-Z0-9_/]+$" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "title" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/organization/schemas/tags.schema.md b/modules/organization/schemas/tags.schema.md new file mode 100644 index 000000000..d33e47b15 --- /dev/null +++ b/modules/organization/schemas/tags.schema.md @@ -0,0 +1,60 @@ +# Resource Manager Tags + + + +## Properties + +*additional properties: false* + +- **name**: *string* +- **description**: *string* +- **id**: *string* +- **network**: *string* +- **iam**: *reference([iam](#refs-iam))* +- **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* +- **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* +- **values**: *object* +
*additional properties: false* + - **`^[a-z-][a-z0-9-]+$`**: *object* +
*additional properties: false* + - **name**: *string* + - **description**: *string* + - **id**: *string* + - **iam**: *reference([iam](#refs-iam))* + - **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* + - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* + +## Definitions + +- **iam**: *object* +
*additional properties: false* + - **`^roles/`**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* +- **iam_bindings**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **members**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^roles/* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* +- **iam_bindings_additive**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **member**: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^[a-zA-Z0-9_/]+$* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index 6e024f19b..0c42519cc 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -1,11 +1,11 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -43,9 +43,15 @@ locals { _tags_merged = merge(local._factory_tags_data, var.tags, var.network_tags) tags = { for k, v in local._tags_merged : k => { - description = v.description - iam = v.iam - iam_bindings = v.iam_bindings + description = v.description + iam = { + for ik, iv in v.iam : ik => coalesce(iv, []) + } + iam_bindings = { + for ik, iv in v.iam_bindings : ik => merge(iv, { + members = coalesce(iv.members, []) + }) + } iam_bindings_additive = v.iam_bindings_additive network = lookup(v, "network", null) id = try(coalesce( @@ -54,9 +60,15 @@ locals { ), null) values = { for vk, vv in lookup(v, "values", {}) : vk => { - description = vv.description - iam = vv.iam - iam_bindings = vv.iam_bindings + description = vv.description + iam = { + for ik, iv in vv.iam : ik => coalesce(iv, []) + } + iam_bindings = { + for ik, iv in vv.iam_bindings : ik => merge(iv, { + members = coalesce(iv.members, []) + }) + } iam_bindings_additive = vv.iam_bindings_additive id = try(coalesce( lookup(vv, "id", null), @@ -185,9 +197,10 @@ resource "google_tags_tag_key_iam_binding" "default" { : each.value.tag_id ) role = each.value.role - members = coalesce( - local.tags[each.value.tag]["iam"][each.value.role], [] - ) + members = [ + for v in local.tags[each.value.tag]["iam"][each.value.role] : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_key_iam_binding" "bindings" { @@ -198,9 +211,10 @@ resource "google_tags_tag_key_iam_binding" "bindings" { : each.value.tag_id ) role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role - members = ( - local.tags[each.value.tag]["iam_bindings"][each.value.binding].members - ) + members = [ + for v in local.tags[each.value.tag]["iam_bindings"][each.value.binding].members : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_key_iam_member" "bindings" { @@ -210,8 +224,12 @@ resource "google_tags_tag_key_iam_member" "bindings" { ? google_tags_tag_key.default[each.value.tag].id : each.value.tag_id ) - role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role - member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = lookup( + var.factories_config.context.iam_principals, + local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member, + local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member + ) } # values @@ -235,10 +253,10 @@ resource "google_tags_tag_value_iam_binding" "default" { : each.value.id ) role = each.value.role - members = coalesce( - local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role], - [] - ) + members = [ + for v in local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role] : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_value_iam_binding" "bindings" { @@ -251,9 +269,10 @@ resource "google_tags_tag_value_iam_binding" "bindings" { role = ( local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role ) - members = ( - local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members - ) + members = [ + for v in local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_value_iam_member" "bindings" { @@ -266,7 +285,9 @@ resource "google_tags_tag_value_iam_member" "bindings" { role = ( local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role ) - member = ( + member = lookup( + var.factories_config.context.iam_principals, + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member, local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member ) } diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 89440f3b3..6abd6644f 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -36,9 +36,10 @@ variable "factories_config" { org_policy_custom_constraints = optional(string) tags = optional(string) context = optional(object({ - org_policies = optional(map(map(string)), {}) - tag_keys = optional(map(string), {}) - tag_values = optional(map(string), {}) + iam_principals = optional(map(string), {}) + org_policies = optional(map(map(string)), {}) + tag_keys = optional(map(string), {}) + tag_values = optional(map(string), {}) }), {}) }) nullable = false diff --git a/modules/project/README.md b/modules/project/README.md index 79a94c39e..dca39cfb1 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -891,7 +891,6 @@ module "project" { } ``` - ## Tags Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. @@ -1067,7 +1066,7 @@ module "project" { ## Project-scoped Tags -To create project-scoped secure tags, use the `tags` and `network_tags` attributes. +To create project-scoped secure tags, use the `tags` and `network_tags` attributes. Tags can also be created via a factory, refer to the [organization module documentation](../organization/README.md#tags-factory) for an example. ```hcl module "project" { @@ -1730,7 +1729,7 @@ alerts: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L118) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L119) | Project name and id suffix. | string | ✓ | | | [alerts](variables-observability.tf#L17) | Monitoring alerts. | map(object({…})) | | {} | | [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | | [billing_account](variables.tf#L23) | Billing account id. | string | | null | @@ -1741,14 +1740,14 @@ alerts: | [default_service_account](variables.tf#L56) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | | [deletion_policy](variables.tf#L69) | Deletion policy setting for this project. | string | | "DELETE" | | [descriptive_name](variables.tf#L80) | Name of the project name. Used for project name instead of `name` variable. | string | | null | -| [factories_config](variables.tf#L86) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [factories_config](variables.tf#L86) | 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)) | | {} | -| [labels](variables.tf#L105) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L112) | If non-empty, creates a project lien with this description. | string | | null | +| [labels](variables.tf#L106) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L113) | 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) | | {} | @@ -1757,22 +1756,22 @@ alerts: | [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#L123) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L161) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L171) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [org_policies](variables.tf#L124) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L152) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L162) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L172) | 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({…})) | | {} | -| [service_agents_config](variables.tf#L191) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L201) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L213) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L220) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L226) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L236) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L273) | Deprecated. Use deletion_policy. | bool | | null | +| [service_agents_config](variables.tf#L192) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L202) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L214) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L221) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L227) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L237) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L274) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L81) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L88) | 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({…})) | | {} | -| [universe](variables.tf#L285) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L294) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L286) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L295) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs diff --git a/modules/project/schemas/tags.schema.json b/modules/project/schemas/tags.schema.json new file mode 120000 index 000000000..e647f3143 --- /dev/null +++ b/modules/project/schemas/tags.schema.json @@ -0,0 +1 @@ +../../organization/schemas/tags.schema.json \ No newline at end of file diff --git a/modules/project/schemas/tags.schema.md b/modules/project/schemas/tags.schema.md new file mode 100644 index 000000000..d33e47b15 --- /dev/null +++ b/modules/project/schemas/tags.schema.md @@ -0,0 +1,60 @@ +# Resource Manager Tags + + + +## Properties + +*additional properties: false* + +- **name**: *string* +- **description**: *string* +- **id**: *string* +- **network**: *string* +- **iam**: *reference([iam](#refs-iam))* +- **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* +- **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* +- **values**: *object* +
*additional properties: false* + - **`^[a-z-][a-z0-9-]+$`**: *object* +
*additional properties: false* + - **name**: *string* + - **description**: *string* + - **id**: *string* + - **iam**: *reference([iam](#refs-iam))* + - **iam_bindings**: *reference([iam_bindings](#refs-iam_bindings))* + - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* + +## Definitions + +- **iam**: *object* +
*additional properties: false* + - **`^roles/`**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* +- **iam_bindings**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **members**: *array* + - items: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^roles/* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* +- **iam_bindings_additive**: *object* +
*additional properties: false* + - **`^[a-z0-9_-]+$`**: *object* +
*additional properties: false* + - **member**: *string* +
*pattern: ^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])* + - **role**: *string* +
*pattern: ^[a-zA-Z0-9_/]+$* + - **condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - ⁺**title**: *string* + - **description**: *string* diff --git a/modules/project/tags.tf b/modules/project/tags.tf index d70a0f9aa..1d9f8d793 100644 --- a/modules/project/tags.tf +++ b/modules/project/tags.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,9 +43,15 @@ locals { _tags_merged = merge(local._factory_tags_data, var.tags, var.network_tags) tags = { for k, v in local._tags_merged : k => { - description = v.description - iam = v.iam - iam_bindings = v.iam_bindings + description = v.description + iam = { + for ik, iv in v.iam : ik => coalesce(iv, []) + } + iam_bindings = { + for ik, iv in v.iam_bindings : ik => merge(iv, { + members = coalesce(iv.members, []) + }) + } iam_bindings_additive = v.iam_bindings_additive network = lookup(v, "network", null) id = try(coalesce( @@ -54,9 +60,15 @@ locals { ), null) values = { for vk, vv in lookup(v, "values", {}) : vk => { - description = vv.description - iam = vv.iam - iam_bindings = vv.iam_bindings + description = vv.description + iam = { + for ik, iv in vv.iam : ik => coalesce(iv, []) + } + iam_bindings = { + for ik, iv in vv.iam_bindings : ik => merge(iv, { + members = coalesce(iv.members, []) + }) + } iam_bindings_additive = vv.iam_bindings_additive id = try(coalesce( lookup(vv, "id", null), @@ -69,7 +81,7 @@ locals { _tag_iam = flatten([ for k, v in local.tags : [ for role in keys(lookup(v, "iam", {})) : { - # we cycle on keys here so we don't risk injecting dynamic values + # We cycle on keys here so we don't risk injecting dynamic values. role = role tag = k tag_id = lookup(v, "id", null) @@ -185,9 +197,10 @@ resource "google_tags_tag_key_iam_binding" "default" { : each.value.tag_id ) role = each.value.role - members = coalesce( - local.tags[each.value.tag]["iam"][each.value.role], [] - ) + members = [ + for v in local.tags[each.value.tag]["iam"][each.value.role] : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_key_iam_binding" "bindings" { @@ -198,9 +211,10 @@ resource "google_tags_tag_key_iam_binding" "bindings" { : each.value.tag_id ) role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role - members = ( - local.tags[each.value.tag]["iam_bindings"][each.value.binding].members - ) + members = [ + for v in local.tags[each.value.tag]["iam_bindings"][each.value.binding].members : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_key_iam_member" "bindings" { @@ -210,8 +224,12 @@ resource "google_tags_tag_key_iam_member" "bindings" { ? google_tags_tag_key.default[each.value.tag].id : each.value.tag_id ) - role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role - member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = lookup( + var.factories_config.context.iam_principals, + local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member, + local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member + ) } # values @@ -235,10 +253,10 @@ resource "google_tags_tag_value_iam_binding" "default" { : each.value.id ) role = each.value.role - members = coalesce( - local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role], - [] - ) + members = [ + for v in local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role] : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_value_iam_binding" "bindings" { @@ -251,9 +269,10 @@ resource "google_tags_tag_value_iam_binding" "bindings" { role = ( local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role ) - members = ( - local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members - ) + members = [ + for v in local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members : + lookup(var.factories_config.context.iam_principals, v, v) + ] } resource "google_tags_tag_value_iam_member" "bindings" { @@ -266,7 +285,9 @@ resource "google_tags_tag_value_iam_member" "bindings" { role = ( local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role ) - member = ( + member = lookup( + var.factories_config.context.iam_principals, + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member, local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member ) } diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 61b35984f..223ceea1c 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -92,6 +92,7 @@ variable "factories_config" { quotas = optional(string) tags = optional(string) context = optional(object({ + iam_principals = optional(map(string), {}) notification_channels = optional(map(string), {}) org_policies = optional(map(map(string)), {}) tag_keys = optional(map(string), {}) diff --git a/tests/modules/organization/examples/tags-factory.yaml b/tests/modules/organization/examples/tags-factory.yaml index e3f27a66c..9f9fa4690 100644 --- a/tests/modules/organization/examples/tags-factory.yaml +++ b/tests/modules/organization/examples/tags-factory.yaml @@ -25,18 +25,37 @@ values: members: - group:finance-team@example.com role: roles/resourcemanager.tagViewer + module.org.google_tags_tag_key_iam_binding.default["foo:roles/resourcemanager.tagViewer"]: + condition: [] + members: + - group:finance-team@example.com + - group:gcp-organization-admins@example.com + role: roles/resourcemanager.tagViewer + tag_key: tagKeys/1234567890 module.org.google_tags_tag_value.default["cost-center/engineering"]: description: Engineering department. short_name: engineering timeouts: null module.org.google_tags_tag_value.default["cost-center/marketing"]: - description: "Marketing department." + description: Marketing department. short_name: marketing timeouts: null + module.org.google_tags_tag_value.default["foo/baz"]: + description: This tag value will be created. + parent: tagKeys/1234567890 + short_name: baz + timeouts: null + module.org.google_tags_tag_value_iam_binding.default["foo/bar:roles/resourcemanager.tagViewer"]: + condition: [] + members: + - group:gcp-organization-admins@example.com + role: roles/resourcemanager.tagViewer + tag_value: tagValues/789012345 counts: google_tags_tag_key: 1 - google_tags_tag_key_iam_binding: 1 - google_tags_tag_value: 2 + google_tags_tag_key_iam_binding: 2 + google_tags_tag_value: 3 + google_tags_tag_value_iam_binding: 1 modules: 1 - resources: 4 + resources: 7