From 0a2cc758ac11e9985fa27bd44e3ebc1c6ab1c4f8 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 3 Nov 2025 08:53:29 +0100 Subject: [PATCH] Essential contacts in schemas, and email context substitutions (#3495) * modules * fast * duplicate diff * fix contacts in FAST stage 0 datasets, update contacts in YAML schemas --- fast/stages/0-org-setup/README.md | 7 +- .../datasets/classic/defaults.yaml | 2 + .../classic/organization/.config.yaml | 4 +- .../datasets/hardened/defaults.yaml | 2 + .../hardened/organization/.config.yaml | 4 +- .../0-org-setup/schemas/defaults.schema.json | 5 + .../0-org-setup/schemas/folder.schema.json | 23 +- .../schemas/organization.schema.json | 13 +- .../0-org-setup/schemas/project.schema.json | 15 +- fast/stages/0-org-setup/variables.tf | 1 + fast/stages/0-org-setup/wif-providers.tf | 2 +- fast/stages/2-networking/README.md | 6 +- .../2-networking/schemas/defaults.schema.json | 5 + .../2-networking/schemas/folder.schema.json | 23 +- .../2-networking/schemas/project.schema.json | 15 +- fast/stages/2-networking/variables.tf | 1 + fast/stages/2-project-factory/README.md | 6 +- .../schemas/defaults.schema.json | 5 + .../schemas/folder.schema.json | 577 +++++++++- .../schemas/project.schema.json | 1018 ++++++++++++++++- fast/stages/2-project-factory/variables.tf | 1 + fast/stages/2-security/README.md | 4 +- .../2-security/schemas/defaults.schema.json | 6 + .../2-security/schemas/folder.schema.json | 576 ++++++++++ .../2-security/schemas/project.schema.json | 15 +- fast/stages/2-security/variables.tf | 1 + modules/folder/README.md | 20 +- modules/folder/main.tf | 10 +- modules/folder/variables.tf | 22 +- modules/organization/README.md | 14 +- modules/organization/main.tf | 10 +- modules/organization/variables.tf | 12 + modules/project-factory/README.md | 10 +- .../schemas/folder.schema.json | 23 +- .../schemas/project.schema.json | 15 +- modules/project-factory/variables.tf | 1 + modules/project/README.md | 46 +- modules/project/main.tf | 10 +- modules/project/variables.tf | 12 + tests/modules/folder/context.tfvars | 6 + tests/modules/folder/context.yaml | 9 +- tests/modules/organization/context.tfvars | 6 + tests/modules/organization/context.yaml | 9 +- tests/modules/project/context.tfvars | 6 + tests/modules/project/context.yaml | 9 +- tools/duplicate-diff.py | 5 +- 46 files changed, 2489 insertions(+), 103 deletions(-) mode change 120000 => 100644 fast/stages/2-project-factory/schemas/folder.schema.json mode change 120000 => 100644 fast/stages/2-project-factory/schemas/project.schema.json create mode 100644 fast/stages/2-security/schemas/folder.schema.json diff --git a/fast/stages/0-org-setup/README.md b/fast/stages/0-org-setup/README.md index 0e34b18d3..09433fc8f 100644 --- a/fast/stages/0-org-setup/README.md +++ b/fast/stages/0-org-setup/README.md @@ -745,9 +745,9 @@ Define values for the `var.environments` variable in a tfvars file. | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [factories_config](variables.tf#L37) | Configuration for the resource factories or external data. | object({…}) | | {} | -| [org_policies_imports](variables.tf#L52) | List of org policies to import. These need to also be defined in data files. | list(string) | | [] | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [factories_config](variables.tf#L38) | Configuration for the resource factories or external data. | object({…}) | | {} | +| [org_policies_imports](variables.tf#L53) | List of org policies to import. These need to also be defined in data files. | list(string) | | [] | ## Outputs @@ -757,4 +757,3 @@ Define values for the `var.environments` variable in a tfvars file. | [projects](outputs.tf#L22) | Attributes for managed projects. | | | [tfvars](outputs.tf#L27) | Stage tfvars. | ✓ | - diff --git a/fast/stages/0-org-setup/datasets/classic/defaults.yaml b/fast/stages/0-org-setup/datasets/classic/defaults.yaml index 91d030719..2c04275f2 100644 --- a/fast/stages/0-org-setup/datasets/classic/defaults.yaml +++ b/fast/stages/0-org-setup/datasets/classic/defaults.yaml @@ -33,6 +33,8 @@ projects: overrides: {} context: # you can populate context variables here for use in YAML replacements + email_addresses: + gcp-organization-admins: gcp-organization-admins@example.com iam_principals: # this is the default group used in bootstrap, initial user must be a member gcp-organization-admins: group:gcp-organization-admins@example.com diff --git a/fast/stages/0-org-setup/datasets/classic/organization/.config.yaml b/fast/stages/0-org-setup/datasets/classic/organization/.config.yaml index 65741b295..89aad3cc4 100644 --- a/fast/stages/0-org-setup/datasets/classic/organization/.config.yaml +++ b/fast/stages/0-org-setup/datasets/classic/organization/.config.yaml @@ -18,8 +18,8 @@ id: $defaults:organization/id contacts: - default: - - $iam_principals:gcp-organization-admins + $email_addresses:gcp-organization-admins: + - ALL # conditional authoritative IAM bindings iam_bindings: # these don't conflict with IAM / IAM by principal diff --git a/fast/stages/0-org-setup/datasets/hardened/defaults.yaml b/fast/stages/0-org-setup/datasets/hardened/defaults.yaml index 91d030719..2c04275f2 100644 --- a/fast/stages/0-org-setup/datasets/hardened/defaults.yaml +++ b/fast/stages/0-org-setup/datasets/hardened/defaults.yaml @@ -33,6 +33,8 @@ projects: overrides: {} context: # you can populate context variables here for use in YAML replacements + email_addresses: + gcp-organization-admins: gcp-organization-admins@example.com iam_principals: # this is the default group used in bootstrap, initial user must be a member gcp-organization-admins: group:gcp-organization-admins@example.com diff --git a/fast/stages/0-org-setup/datasets/hardened/organization/.config.yaml b/fast/stages/0-org-setup/datasets/hardened/organization/.config.yaml index 6de4656d2..852aa089c 100644 --- a/fast/stages/0-org-setup/datasets/hardened/organization/.config.yaml +++ b/fast/stages/0-org-setup/datasets/hardened/organization/.config.yaml @@ -18,8 +18,8 @@ id: $defaults:organization/id contacts: - default: - - $iam_principals:gcp-organization-admins + $email_addresses:gcp-organization-admins: + - ALL # conditional authoritative IAM bindings iam_bindings: # these don't conflict with IAM / IAM by principal diff --git a/fast/stages/0-org-setup/schemas/defaults.schema.json b/fast/stages/0-org-setup/schemas/defaults.schema.json index 9720eb96b..7e869f5d4 100644 --- a/fast/stages/0-org-setup/schemas/defaults.schema.json +++ b/fast/stages/0-org-setup/schemas/defaults.schema.json @@ -538,6 +538,11 @@ "type": "string" } }, + "email_addresses": { + "additionalProperties": { + "type": "string" + } + }, "folder_ids": { "type": "object", "additionalProperties": { diff --git a/fast/stages/0-org-setup/schemas/folder.schema.json b/fast/stages/0-org-setup/schemas/folder.schema.json index 778dc0572..076464aca 100644 --- a/fast/stages/0-org-setup/schemas/folder.schema.json +++ b/fast/stages/0-org-setup/schemas/folder.schema.json @@ -64,6 +64,27 @@ } } }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, "factories_config": { "type": "object", "additionalProperties": false, @@ -552,4 +573,4 @@ } } } -} +} \ No newline at end of file diff --git a/fast/stages/0-org-setup/schemas/organization.schema.json b/fast/stages/0-org-setup/schemas/organization.schema.json index c0afbdeff..349861e87 100644 --- a/fast/stages/0-org-setup/schemas/organization.schema.json +++ b/fast/stages/0-org-setup/schemas/organization.schema.json @@ -11,10 +11,19 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] } } } diff --git a/fast/stages/0-org-setup/schemas/project.schema.json b/fast/stages/0-org-setup/schemas/project.schema.json index 228c10c06..0df37926b 100644 --- a/fast/stages/0-org-setup/schemas/project.schema.json +++ b/fast/stages/0-org-setup/schemas/project.schema.json @@ -80,10 +80,19 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^[a-z0-9_-]+$": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] } } } @@ -1005,4 +1014,4 @@ } } } -} +} \ No newline at end of file diff --git a/fast/stages/0-org-setup/variables.tf b/fast/stages/0-org-setup/variables.tf index 99b2057ae..450c658f4 100644 --- a/fast/stages/0-org-setup/variables.tf +++ b/fast/stages/0-org-setup/variables.tf @@ -18,6 +18,7 @@ variable "context" { description = "Context-specific interpolations." type = object({ custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) iam_principals = optional(map(string), {}) locations = optional(map(string), {}) diff --git a/fast/stages/0-org-setup/wif-providers.tf b/fast/stages/0-org-setup/wif-providers.tf index f2b3278b6..76b664b12 100644 --- a/fast/stages/0-org-setup/wif-providers.tf +++ b/fast/stages/0-org-setup/wif-providers.tf @@ -52,7 +52,7 @@ resource "google_iam_workload_identity_pool_provider" "default" { # If users don't provide an issuer_uri, we set the public one for the platform chosen. issuer_uri = coalesce( try(each.value.custom_settings.issuer_uri, null), - try(each.value.custom_settings.okta == null ? null : "https://${each.value.custom_settings.okta.organization_name}/oauth2/${each.value.custom_settings.okta.auth_server_name}", null), + try("https://${each.value.custom_settings.okta.organization_name}/oauth2/${each.value.custom_settings.okta.auth_server_name}", null), try(each.value.issuer_uri, null), ) # OIDC JWKs in JSON String format. If no value is provided, they key is diff --git a/fast/stages/2-networking/README.md b/fast/stages/2-networking/README.md index 72153c266..2079e86dc 100644 --- a/fast/stages/2-networking/README.md +++ b/fast/stages/2-networking/README.md @@ -316,9 +316,9 @@ Internally created resources are mapped to context namespaces, and use specific | [billing_account](variables-fast.tf#L17) | Billing account id. | object({…}) | ✓ | | | [organization](variables-fast.tf#L49) | Organization details. | object({…}) | ✓ | | | [prefix](variables-fast.tf#L66) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | | [custom_roles](variables-fast.tf#L25) | Custom roles defined at the org level, in key => id format. | map(string) | | {} | -| [factories_config](variables.tf#L36) | Configuration for the resource factories or external data. | object({…}) | | {} | +| [factories_config](variables.tf#L37) | Configuration for the resource factories or external data. | object({…}) | | {} | | [folder_ids](variables-fast.tf#L33) | Folders created in the bootstrap stage. | map(string) | | {} | | [iam_principals](variables-fast.tf#L41) | IAM-format principals. | map(string) | | {} | | [perimeters](variables-fast.tf#L58) | Optional VPC-SC perimeter ids. | map(string) | | {} | @@ -327,7 +327,7 @@ Internally created resources are mapped to context namespaces, and use specific | [storage_buckets](variables-fast.tf#L92) | Storage buckets created in the bootstrap stage. | map(string) | | {} | | [tag_keys](variables-fast.tf#L100) | FAST-managed resource manager tag keys. | map(string) | | {} | | [tag_values](variables-fast.tf#L108) | FAST-managed resource manager tag values. | map(string) | | {} | -| [universe](variables.tf#L53) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | +| [universe](variables.tf#L54) | 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/schemas/defaults.schema.json b/fast/stages/2-networking/schemas/defaults.schema.json index 89f6400df..4eedc6e68 100644 --- a/fast/stages/2-networking/schemas/defaults.schema.json +++ b/fast/stages/2-networking/schemas/defaults.schema.json @@ -549,6 +549,11 @@ "type": "string" } }, + "email_addresses": { + "additionalProperties": { + "type": "string" + } + }, "folder_ids": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-networking/schemas/folder.schema.json b/fast/stages/2-networking/schemas/folder.schema.json index 778dc0572..076464aca 100644 --- a/fast/stages/2-networking/schemas/folder.schema.json +++ b/fast/stages/2-networking/schemas/folder.schema.json @@ -64,6 +64,27 @@ } } }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, "factories_config": { "type": "object", "additionalProperties": false, @@ -552,4 +573,4 @@ } } } -} +} \ No newline at end of file diff --git a/fast/stages/2-networking/schemas/project.schema.json b/fast/stages/2-networking/schemas/project.schema.json index 228c10c06..0df37926b 100644 --- a/fast/stages/2-networking/schemas/project.schema.json +++ b/fast/stages/2-networking/schemas/project.schema.json @@ -80,10 +80,19 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^[a-z0-9_-]+$": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] } } } @@ -1005,4 +1014,4 @@ } } } -} +} \ No newline at end of file diff --git a/fast/stages/2-networking/variables.tf b/fast/stages/2-networking/variables.tf index 47b47f910..1fc182d39 100644 --- a/fast/stages/2-networking/variables.tf +++ b/fast/stages/2-networking/variables.tf @@ -19,6 +19,7 @@ variable "context" { type = object({ cidr_ranges_sets = optional(map(list(string)), {}) custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) kms_keys = optional(map(string), {}) iam_principals = optional(map(string), {}) diff --git a/fast/stages/2-project-factory/README.md b/fast/stages/2-project-factory/README.md index 53953a04a..e1b28cff8 100644 --- a/fast/stages/2-project-factory/README.md +++ b/fast/stages/2-project-factory/README.md @@ -481,12 +481,12 @@ Pattern-based files make specific assumptions: | [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-org-setup | | [billing_account](variables-fast.tf#L26) | Billing account id. | object({…}) | ✓ | | 0-org-setup | | [prefix](variables-fast.tf#L82) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | 0-org-setup | -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | | | [custom_roles](variables-fast.tf#L34) | Custom roles defined at the org level, in key => id format. | map(string) | | {} | 0-org-setup | | [data_defaults](variables-projects.tf#L17) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | | | [data_merges](variables-projects.tf#L93) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | | | [data_overrides](variables-projects.tf#L112) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | | -| [factories_config](variables.tf#L36) | Path to folder with YAML resource description data files. | object({…}) | | {} | | +| [factories_config](variables.tf#L37) | Path to folder with YAML resource description data files. | object({…}) | | {} | | | [folder_ids](variables-fast.tf#L42) | Folders created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [host_project_ids](variables-fast.tf#L58) | Host project for the shared VPC. | map(string) | | {} | 2-networking | | [iam_principals](variables-fast.tf#L50) | IAM-format principals. | map(string) | | {} | 0-org-setup | @@ -494,7 +494,7 @@ Pattern-based files make specific assumptions: | [perimeters](variables-fast.tf#L74) | Optional VPC-SC perimeter ids. | map(string) | | {} | 1-vpcsc | | [project_ids](variables-fast.tf#L92) | Projects created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [service_accounts](variables-fast.tf#L100) | Service accounts created in the bootstrap stage. | map(string) | | {} | 0-org-setup | -| [stage_name](variables.tf#L57) | FAST stage name. Used to separate output files across different factories. | string | | "2-project-factory" | | +| [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#L108) | Shared VPC subnet IDs. | map(map(string)) | | {} | 2-networking | | [tag_values](variables-fast.tf#L116) | FAST-managed resource manager tag values. | map(string) | | {} | 0-org-setup | | [universe](variables-fast.tf#L124) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | object({…}) | | null | 0-org-setup | diff --git a/fast/stages/2-project-factory/schemas/defaults.schema.json b/fast/stages/2-project-factory/schemas/defaults.schema.json index 29b4ab2d8..d0ef30e99 100644 --- a/fast/stages/2-project-factory/schemas/defaults.schema.json +++ b/fast/stages/2-project-factory/schemas/defaults.schema.json @@ -572,6 +572,11 @@ "type": "string" } }, + "email_addresses": { + "additionalProperties": { + "type": "string" + } + }, "folder_ids": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-project-factory/schemas/folder.schema.json b/fast/stages/2-project-factory/schemas/folder.schema.json deleted file mode 120000 index d58a2759b..000000000 --- a/fast/stages/2-project-factory/schemas/folder.schema.json +++ /dev/null @@ -1 +0,0 @@ -../../../../modules/project-factory/schemas/folder.schema.json \ No newline at end of file diff --git a/fast/stages/2-project-factory/schemas/folder.schema.json b/fast/stages/2-project-factory/schemas/folder.schema.json new file mode 100644 index 000000000..076464aca --- /dev/null +++ b/fast/stages/2-project-factory/schemas/folder.schema.json @@ -0,0 +1,576 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Folder", + "type": "object", + "additionalProperties": false, + "properties": { + "automation": { + "type": "object", + "additionalProperties": false, + "required": [ + "project" + ], + "properties": { + "prefix": { + "type": "string" + }, + "project": { + "type": "string" + }, + "bucket": { + "$ref": "#/$defs/bucket" + }, + "service_accounts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_billing_roles": { + "$ref": "#/$defs/iam_billing_roles" + }, + "iam_folder_roles": { + "$ref": "#/$defs/iam_folder_roles" + }, + "iam_organization_roles": { + "$ref": "#/$defs/iam_organization_roles" + }, + "iam_project_roles": { + "$ref": "#/$defs/iam_project_roles" + }, + "iam_sa_roles": { + "$ref": "#/$defs/iam_sa_roles" + }, + "iam_storage_roles": { + "$ref": "#/$defs/iam_storage_roles" + } + } + } + } + } + } + }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, + "factories_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "org_policies": { + "type": "string" + }, + "pam_entitlements": { + "type": "string" + }, + "scc_sha_custom_modules": { + "type": "string" + } + } + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + }, + "name": { + "type": "string" + }, + "org_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z]+\\.": { + "type": "object", + "properties": { + "inherit_from_parent": { + "type": "boolean" + }, + "reset": { + "type": "boolean" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "deny": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "enforce": { + "type": "boolean" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "location": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "pam_entitlements": { + "$ref": "#/$defs/pam_entitlements" + }, + "parent": { + "type": "string", + "pattern": "^(?:folders/[0-9]+|organizations/[0-9]+|\\$folder_ids:[a-z0-9_-]+)$" + }, + "tag_bindings": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "string" + } + } + } + }, + "$defs": { + "bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "force_destroy": { + "type": "boolean" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "location": { + "type": "string" + }, + "managed_folders": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9][a-zA-Z0-9_/-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "force_destroy": { + "type": "boolean" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + } + } + } + } + }, + "prefix": { + "type": "string" + }, + "storage_class": { + "type": "string" + }, + "uniform_bucket_level_access": { + "type": "boolean" + }, + "versioning": { + "type": "boolean" + } + } + }, + "iam": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:roles/|\\$custom_roles:)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)" + } + } + } + }, + "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:|\\$iam_principals:)" + } + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_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:|\\$iam_principals:)" + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "title" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + }, + "iam_by_principals": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + } + } + } + }, + "iam_billing_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_folder_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_organization_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_project_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_sa_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_storage_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "pam_entitlements": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z][a-z0-9-]{0,61}[a-z0-9]$": { + "type": "object", + "properties": { + "max_request_duration": { + "type": "string" + }, + "eligible_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "privileged_access": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "condition": { + "type": "string" + } + }, + "required": [ + "role" + ], + "additionalProperties": false + } + }, + "requester_justification_config": { + "type": "object", + "properties": { + "not_mandatory": { + "type": "boolean" + }, + "unstructured": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "manual_approvals": { + "type": "object", + "properties": { + "require_approver_justification": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "approvers": { + "type": "array", + "items": { + "type": "string" + } + }, + "approvals_needed": { + "type": "number" + }, + "approver_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "approvers" + ], + "additionalProperties": false + } + } + }, + "required": [ + "require_approver_justification", + "steps" + ], + "additionalProperties": false + }, + "additional_notification_targets": { + "type": "object", + "properties": { + "admin_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + }, + "requester_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "max_request_duration", + "eligible_users", + "privileged_access" + ], + "additionalProperties": false + } + } + } + } +} \ No newline at end of file diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json deleted file mode 120000 index 11f161f17..000000000 --- a/fast/stages/2-project-factory/schemas/project.schema.json +++ /dev/null @@ -1 +0,0 @@ -../../../../modules/project-factory/schemas/project.schema.json \ No newline at end of file diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json new file mode 100644 index 000000000..0df37926b --- /dev/null +++ b/fast/stages/2-project-factory/schemas/project.schema.json @@ -0,0 +1,1017 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Project", + "type": "object", + "additionalProperties": false, + "properties": { + "automation": { + "type": "object", + "additionalProperties": false, + "required": [ + "project" + ], + "properties": { + "prefix": { + "type": "string" + }, + "project": { + "type": "string" + }, + "bucket": { + "$ref": "#/$defs/bucket" + }, + "service_accounts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_billing_roles": { + "$ref": "#/$defs/iam_billing_roles" + }, + "iam_folder_roles": { + "$ref": "#/$defs/iam_folder_roles" + }, + "iam_organization_roles": { + "$ref": "#/$defs/iam_organization_roles" + }, + "iam_project_roles": { + "$ref": "#/$defs/iam_project_roles" + }, + "iam_sa_roles": { + "$ref": "#/$defs/iam_sa_roles" + }, + "iam_storage_roles": { + "$ref": "#/$defs/iam_storage_roles" + } + } + } + } + } + } + }, + "billing_account": { + "type": "string" + }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, + "buckets": { + "$ref": "#/$defs/buckets" + }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, + "datasets": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "friendly_name": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + }, + "deletion_policy": { + "type": "string", + "enum": [ + "PREVENT", + "DELETE", + "ABANDON" + ] + }, + "factories_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "custom_roles": { + "type": "string" + }, + "observability": { + "type": "string" + }, + "org_policies": { + "type": "string" + }, + "org_policies": { + "type": "string" + }, + "quotas": { + "type": "string" + }, + "scc_sha_custom_modules": { + "type": "string" + }, + "tags": { + "type": "string" + } + } + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + }, + "iam_by_principals_additive": { + "$ref": "#/$defs/iam_by_principals" + }, + "labels": { + "type": "object" + }, + "pam_entitlements": { + "$ref": "#/$defs/pam_entitlements" + }, + "log_buckets": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "$ref": "#/$defs/log_bucket" + } + } + }, + "metric_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "org_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z]+\\.": { + "type": "object", + "properties": { + "inherit_from_parent": { + "type": "boolean" + }, + "reset": { + "type": "boolean" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "deny": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "enforce": { + "type": "boolean" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "location": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "quotas": { + "title": "Quotas", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "service", + "quota_id", + "preferred_value" + ], + "properties": { + "service": { + "type": "string" + }, + "quota_id": { + "type": "string" + }, + "preferred_value": { + "type": "number" + }, + "dimensions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "justification": { + "type": "string" + }, + "contact_email": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "ignore_safety_checks": { + "type": "string", + "enum": [ + "QUOTA_DECREASE_BELOW_USAGE", + "QUOTA_DECREASE_PERCENTAGE_TOO_HIGH", + "QUOTA_SAFETY_CHECK_UNSPECIFIED" + ] + } + } + } + } + }, + "parent": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "project_reuse": { + "type": "object", + "additionalProperties": false, + "properties": { + "use_data_source": { + "type": "boolean" + }, + "attributes": { + "type": "object", + "required": [ + "name", + "number" + ], + "properties": { + "name": { + "type": "string" + }, + "number": { + "type": "number" + }, + "services_enabled": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "project_template": { + "type": "string" + }, + "service_accounts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_self_roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "iam_project_roles": { + "$ref": "#/$defs/iam_project_roles" + }, + "iam_sa_roles": { + "$ref": "#/$defs/iam_sa_roles" + } + } + } + } + }, + "service_encryption_key_ids": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z-]+\\.googleapis\\.com$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "services": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z-]+\\.googleapis\\.com$" + } + }, + "shared_vpc_host_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "service_projects": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "shared_vpc_service_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "host_project" + ], + "properties": { + "host_project": { + "type": "string" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "network_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "service_agent_iam": { + "type": "object", + "additionalItems": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "service_agent_subnet_iam": { + "type": "object", + "additionalItems": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "service_iam_grants": { + "type": "array", + "items": { + "type": "string" + } + }, + "network_subnet_users": { + "type": "object", + "additionalItems": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "tag_bindings": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "string" + } + } + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "id": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "id": { + "type": "string" + } + } + } + } + } + } + }, + "universe": { + "type": "object", + "additionalProperties": false, + "properties": { + "prefix": { + "type": "string" + }, + "forced_jit_service_identities": { + "type": "array", + "items": { + "type": "string" + } + }, + "unavailable_services": { + "type": "array", + "items": { + "type": "string" + } + }, + "unavailable_service_identities": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "vpc_sc": { + "type": "object", + "additionalItems": false, + "required": [ + "perimeter_name" + ], + "properties": { + "perimeter_name": { + "type": "string" + }, + "is_dry_run": { + "type": "boolean" + } + } + } + }, + "$defs": { + "bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "create": { + "type": "boolean", + "default": true + }, + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "force_destroy": { + "type": "boolean" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "location": { + "type": "string" + }, + "managed_folders": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9][a-zA-Z0-9_/-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "force_destroy": { + "type": "boolean" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + } + } + } + } + }, + "prefix": { + "type": "string" + }, + "storage_class": { + "type": "string" + }, + "uniform_bucket_level_access": { + "type": "boolean" + }, + "versioning": { + "type": "boolean" + }, + "retention_policy": { + "type": "object", + "additionalProperties": false, + "properties": { + "retention_period": { + "type": "number" + }, + "is_locked": { + "type": "boolean" + } + } + }, + "enable_object_retention": { + "type": "boolean" + } + } + }, + "buckets": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "$ref": "#/$defs/bucket" + } + } + }, + "iam": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:roles/|\\$custom_roles:)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:||\\$iam_principals:[a-z0-9_-]+)" + } + } + } + }, + "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:|\\$iam_principals:[a-z0-9_-]+)" + } + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_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:|\\$iam_principals:[a-z0-9_-]+)" + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "title" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + }, + "iam_by_principals": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + } + } + } + }, + "iam_billing_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_folder_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_organization_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_project_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:[a-z0-9-]|\\$project_ids:[a-z0-9_-])+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_sa_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:\\$service_account_ids:|projects/)": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_storage_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "log_bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "kms_key_name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "log_analytics": { + "type": "object", + "additionalProperties": false, + "properties": { + "enable": { + "type": "boolean", + "default": false + }, + "dataset_link_id": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "retention": { + "type": "number" + } + } + }, + "pam_entitlements": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z][a-z0-9-]{0,61}[a-z0-9]$": { + "type": "object", + "properties": { + "max_request_duration": { + "type": "string" + }, + "eligible_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "privileged_access": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "condition": { + "type": "string" + } + }, + "required": [ + "role" + ], + "additionalProperties": false + } + }, + "requester_justification_config": { + "type": "object", + "properties": { + "not_mandatory": { + "type": "boolean" + }, + "unstructured": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "manual_approvals": { + "type": "object", + "properties": { + "require_approver_justification": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "approvers": { + "type": "array", + "items": { + "type": "string" + } + }, + "approvals_needed": { + "type": "number" + }, + "approver_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "approvers" + ], + "additionalProperties": false + } + } + }, + "required": [ + "require_approver_justification", + "steps" + ], + "additionalProperties": false + }, + "additional_notification_targets": { + "type": "object", + "properties": { + "admin_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + }, + "requester_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "max_request_duration", + "eligible_users", + "privileged_access" + ], + "additionalProperties": false + } + } + } + } +} \ No newline at end of file diff --git a/fast/stages/2-project-factory/variables.tf b/fast/stages/2-project-factory/variables.tf index df0105199..c4aab0cb5 100644 --- a/fast/stages/2-project-factory/variables.tf +++ b/fast/stages/2-project-factory/variables.tf @@ -19,6 +19,7 @@ variable "context" { type = object({ condition_vars = optional(map(map(string)), {}) custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) iam_principals = optional(map(string), {}) kms_keys = optional(map(string), {}) diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md index ebd1a475e..7b46a0d30 100644 --- a/fast/stages/2-security/README.md +++ b/fast/stages/2-security/README.md @@ -183,9 +183,9 @@ A reference Certificate Authority Services (CAS) is also part of this stage, all |---|---|:---:|:---:|:---:|:---:| | [billing_account](variables-fast.tf#L17) | Billing account id. | object({…}) | ✓ | | 0-org-setup | | [prefix](variables-fast.tf#L57) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | string | ✓ | | 0-org-setup | -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | | | [custom_roles](variables-fast.tf#L25) | Custom roles defined at the org level, in key => id format. | map(string) | | {} | 0-org-setup | -| [factories_config](variables.tf#L35) | Configuration for the resource factories or external data. | object({…}) | | {} | | +| [factories_config](variables.tf#L36) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [folder_ids](variables-fast.tf#L33) | Folders created in the bootstrap stage. | map(string) | | {} | 0-org-setup | | [iam_principals](variables-fast.tf#L41) | IAM-format principals. | map(string) | | {} | 0-org-setup | | [perimeters](variables-fast.tf#L49) | Optional VPC-SC perimeter ids. | map(string) | | {} | 1-vpcsc | diff --git a/fast/stages/2-security/schemas/defaults.schema.json b/fast/stages/2-security/schemas/defaults.schema.json index 92a0fe849..605e999f2 100644 --- a/fast/stages/2-security/schemas/defaults.schema.json +++ b/fast/stages/2-security/schemas/defaults.schema.json @@ -522,6 +522,12 @@ "type": "string" } }, + "email_addresses": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "folder_ids": { "type": "object", "additionalProperties": { diff --git a/fast/stages/2-security/schemas/folder.schema.json b/fast/stages/2-security/schemas/folder.schema.json new file mode 100644 index 000000000..076464aca --- /dev/null +++ b/fast/stages/2-security/schemas/folder.schema.json @@ -0,0 +1,576 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Folder", + "type": "object", + "additionalProperties": false, + "properties": { + "automation": { + "type": "object", + "additionalProperties": false, + "required": [ + "project" + ], + "properties": { + "prefix": { + "type": "string" + }, + "project": { + "type": "string" + }, + "bucket": { + "$ref": "#/$defs/bucket" + }, + "service_accounts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_billing_roles": { + "$ref": "#/$defs/iam_billing_roles" + }, + "iam_folder_roles": { + "$ref": "#/$defs/iam_folder_roles" + }, + "iam_organization_roles": { + "$ref": "#/$defs/iam_organization_roles" + }, + "iam_project_roles": { + "$ref": "#/$defs/iam_project_roles" + }, + "iam_sa_roles": { + "$ref": "#/$defs/iam_sa_roles" + }, + "iam_storage_roles": { + "$ref": "#/$defs/iam_storage_roles" + } + } + } + } + } + } + }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, + "factories_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "org_policies": { + "type": "string" + }, + "pam_entitlements": { + "type": "string" + }, + "scc_sha_custom_modules": { + "type": "string" + } + } + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + }, + "name": { + "type": "string" + }, + "org_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z]+\\.": { + "type": "object", + "properties": { + "inherit_from_parent": { + "type": "boolean" + }, + "reset": { + "type": "boolean" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "deny": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "enforce": { + "type": "boolean" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "location": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "pam_entitlements": { + "$ref": "#/$defs/pam_entitlements" + }, + "parent": { + "type": "string", + "pattern": "^(?:folders/[0-9]+|organizations/[0-9]+|\\$folder_ids:[a-z0-9_-]+)$" + }, + "tag_bindings": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "string" + } + } + } + }, + "$defs": { + "bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "force_destroy": { + "type": "boolean" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "location": { + "type": "string" + }, + "managed_folders": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9][a-zA-Z0-9_/-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "force_destroy": { + "type": "boolean" + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + } + } + } + } + }, + "prefix": { + "type": "string" + }, + "storage_class": { + "type": "string" + }, + "uniform_bucket_level_access": { + "type": "boolean" + }, + "versioning": { + "type": "boolean" + } + } + }, + "iam": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:roles/|\\$custom_roles:)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)" + } + } + } + }, + "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:|\\$iam_principals:)" + } + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_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:|\\$iam_principals:)" + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "title" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + }, + "iam_by_principals": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:roles/|\\$custom_roles:)" + } + } + } + }, + "iam_billing_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_folder_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_organization_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_project_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_sa_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "iam_storage_roles": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "pam_entitlements": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z][a-z0-9-]{0,61}[a-z0-9]$": { + "type": "object", + "properties": { + "max_request_duration": { + "type": "string" + }, + "eligible_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "privileged_access": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "condition": { + "type": "string" + } + }, + "required": [ + "role" + ], + "additionalProperties": false + } + }, + "requester_justification_config": { + "type": "object", + "properties": { + "not_mandatory": { + "type": "boolean" + }, + "unstructured": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "manual_approvals": { + "type": "object", + "properties": { + "require_approver_justification": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "approvers": { + "type": "array", + "items": { + "type": "string" + } + }, + "approvals_needed": { + "type": "number" + }, + "approver_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "approvers" + ], + "additionalProperties": false + } + } + }, + "required": [ + "require_approver_justification", + "steps" + ], + "additionalProperties": false + }, + "additional_notification_targets": { + "type": "object", + "properties": { + "admin_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + }, + "requester_email_recipients": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "max_request_duration", + "eligible_users", + "privileged_access" + ], + "additionalProperties": false + } + } + } + } +} \ No newline at end of file diff --git a/fast/stages/2-security/schemas/project.schema.json b/fast/stages/2-security/schemas/project.schema.json index 228c10c06..0df37926b 100644 --- a/fast/stages/2-security/schemas/project.schema.json +++ b/fast/stages/2-security/schemas/project.schema.json @@ -80,10 +80,19 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^[a-z0-9_-]+$": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] } } } @@ -1005,4 +1014,4 @@ } } } -} +} \ No newline at end of file diff --git a/fast/stages/2-security/variables.tf b/fast/stages/2-security/variables.tf index cbe98a1a7..d3e290109 100644 --- a/fast/stages/2-security/variables.tf +++ b/fast/stages/2-security/variables.tf @@ -18,6 +18,7 @@ variable "context" { description = "Context-specific interpolations." type = object({ condition_vars = optional(map(map(string)), {}) + email_addresses = optional(map(string), {}) custom_roles = optional(map(string), {}) folder_ids = optional(map(string), {}) iam_principals = optional(map(string), {}) diff --git a/modules/folder/README.md b/modules/folder/README.md index 7a134aab3..d78448df8 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -541,27 +541,27 @@ module "folder" { |---|---|:---:|:---:|:---:| | [assured_workload_config](variables.tf#L17) | Create AssuredWorkloads folder instead of regular folder when value is provided. Incompatible with folder_create=false. | object({…}) | | null | | [contacts](variables.tf#L70) | 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#L78) | Context-specific interpolations. | object({…}) | | {} | -| [deletion_protection](variables.tf#L91) | Deletion protection setting for this folder. | bool | | false | -| [factories_config](variables.tf#L97) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L108) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L117) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [context](variables.tf#L89) | Context-specific interpolations. | object({…}) | | {} | +| [deletion_protection](variables.tf#L103) | Deletion protection setting for this folder. | bool | | false | +| [factories_config](variables.tf#L109) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L120) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L129) | 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)) | | {} | -| [id](variables.tf#L127) | Folder ID in case you use folder_create=false. | string | | null | +| [id](variables.tf#L139) | 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#L133) | Folder name. | string | | null | -| [org_policies](variables.tf#L139) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [name](variables.tf#L145) | Folder name. | string | | null | +| [org_policies](variables.tf#L151) | 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#L167) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [parent](variables.tf#L179) | Parent in folders/folder_id or organizations/org_id format. | string | | null | | [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L181) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [tag_bindings](variables.tf#L193) | 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 cb788fd32..ecdd45453 100644 --- a/modules/folder/main.tf +++ b/modules/folder/main.tf @@ -51,10 +51,12 @@ resource "google_folder" "folder" { } resource "google_essential_contacts_contact" "contact" { - provider = google-beta - for_each = var.contacts - parent = local.folder_id - email = each.key + provider = google-beta + for_each = var.contacts + parent = local.folder_id + email = lookup( + local.ctx.email_addresses, each.key, each.key + ) language_tag = "en" notification_category_subscriptions = each.value depends_on = [ diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 7cfc941d8..edb800b0a 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -72,17 +72,29 @@ variable "contacts" { type = map(list(string)) default = {} nullable = false + validation { + condition = alltrue(flatten([ + for k, v in var.contacts : [ + for vv in v : contains([ + "ALL", "SUSPENSION", "SECURITY", "TECHNICAL", "BILLING", "LEGAL", + "PRODUCT_UPDATES" + ], vv) + ] + ])) + error_message = "Invalid contact notification value." + } } variable "context" { description = "Context-specific interpolations." type = object({ - condition_vars = optional(map(map(string)), {}) - custom_roles = optional(map(string), {}) - folder_ids = optional(map(string), {}) - iam_principals = optional(map(string), {}) - tag_values = optional(map(string), {}) + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) + folder_ids = optional(map(string), {}) + iam_principals = optional(map(string), {}) + tag_values = optional(map(string), {}) }) default = {} nullable = false diff --git a/modules/organization/README.md b/modules/organization/README.md index 45af6eecd..3c48e543c 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -736,12 +736,12 @@ values: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L115) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L127) | 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)) | | {} | -| [context](variables.tf#L24) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L43) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L50) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L64) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [context](variables.tf#L35) | Context-specific interpolations. | object({…}) | | {} | +| [custom_roles](variables.tf#L55) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [factories_config](variables.tf#L62) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L76) | 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({…})) | | {} | @@ -752,8 +752,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#L73) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L101) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L85) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L113) | 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_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L82) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | diff --git a/modules/organization/main.tf b/modules/organization/main.tf index 5b8f341d3..a46129be5 100644 --- a/modules/organization/main.tf +++ b/modules/organization/main.tf @@ -25,10 +25,12 @@ locals { } resource "google_essential_contacts_contact" "contact" { - provider = google-beta - for_each = var.contacts - parent = var.organization_id - email = each.key + provider = google-beta + for_each = var.contacts + parent = var.organization_id + email = lookup( + local.ctx.email_addresses, each.key, each.key + ) language_tag = "en" notification_category_subscriptions = each.value depends_on = [ diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 0c9f13fca..fb4c81f47 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -19,6 +19,17 @@ variable "contacts" { type = map(list(string)) default = {} nullable = false + validation { + condition = alltrue(flatten([ + for k, v in var.contacts : [ + for vv in v : contains([ + "ALL", "SUSPENSION", "SECURITY", "TECHNICAL", "BILLING", "LEGAL", + "PRODUCT_UPDATES" + ], vv) + ] + ])) + error_message = "Invalid contact notification value." + } } variable "context" { @@ -27,6 +38,7 @@ variable "context" { bigquery_datasets = optional(map(string), {}) condition_vars = optional(map(map(string)), {}) custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) iam_principals = optional(map(string), {}) locations = optional(map(string), {}) log_buckets = optional(map(string), {}) diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index a5811dc60..426342d08 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -778,11 +778,11 @@ compute.disableSerialPortAccess: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [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#L37) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L107) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L126) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factories_config](variables.tf#L171) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [data_defaults](variables.tf#L38) | 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({…}) | | {} | | [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/schemas/folder.schema.json b/modules/project-factory/schemas/folder.schema.json index 778dc0572..076464aca 100644 --- a/modules/project-factory/schemas/folder.schema.json +++ b/modules/project-factory/schemas/folder.schema.json @@ -64,6 +64,27 @@ } } }, + "contacts": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] + } + } + } + }, "factories_config": { "type": "object", "additionalProperties": false, @@ -552,4 +573,4 @@ } } } -} +} \ No newline at end of file diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json index 228c10c06..0df37926b 100644 --- a/modules/project-factory/schemas/project.schema.json +++ b/modules/project-factory/schemas/project.schema.json @@ -80,10 +80,19 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^[a-z0-9_-]+$": { + "^(\\S+@\\S+\\.\\S+|\\$email_addresses:\\S+)$": { "type": "array", "items": { - "type": "string" + "type": "string", + "enum": [ + "ALL", + "BILLING", + "LEGAL", + "SECURITY", + "PRODUCT_UPDATES", + "SUSPENSION", + "TECHNICAL" + ] } } } @@ -1005,4 +1014,4 @@ } } } -} +} \ No newline at end of file diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index d32bf85ed..9c570dcd6 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -19,6 +19,7 @@ variable "context" { type = object({ condition_vars = optional(map(map(string)), {}) custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) iam_principals = optional(map(string), {}) kms_keys = optional(map(string), {}) diff --git a/modules/project/README.md b/modules/project/README.md index e3bb4d186..36a204744 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -1946,26 +1946,26 @@ alerts: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L133) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L145) | 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 | | [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | | [contacts](variables.tf#L36) | 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#L43) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L62) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_network_tier](variables.tf#L69) | Default compute network tier for the project. | string | | null | -| [default_service_account](variables.tf#L75) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [deletion_policy](variables.tf#L88) | Deletion policy setting for this project. | string | | "DELETE" | -| [descriptive_name](variables.tf#L99) | Name of the project name. Used for project name instead of `name` variable. | string | | null | -| [factories_config](variables.tf#L105) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [context](variables.tf#L54) | Context-specific interpolations. | object({…}) | | {} | +| [custom_roles](variables.tf#L74) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [default_network_tier](variables.tf#L81) | Default compute network tier for the project. | string | | null | +| [default_service_account](variables.tf#L87) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [deletion_policy](variables.tf#L100) | Deletion policy setting for this project. | string | | "DELETE" | +| [descriptive_name](variables.tf#L111) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [factories_config](variables.tf#L117) | 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#L120) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L127) | If non-empty, creates a project lien with this description. | string | | null | +| [labels](variables.tf#L132) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L139) | 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) | | {} | @@ -1974,25 +1974,25 @@ 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#L138) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L150) | 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#L166) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L180) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L190) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [parent](variables.tf#L178) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L192) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L202) | 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_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L210) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L220) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L232) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L239) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L245) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L255) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L292) | Deprecated. Use deletion_policy. | bool | | null | +| [service_agents_config](variables.tf#L222) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L232) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L244) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L251) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L257) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L267) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L304) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L82) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L89) | 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#L154) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | -| [universe](variables.tf#L304) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L315) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L316) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L327) | 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/main.tf b/modules/project/main.tf index 3979a14d7..6bb609419 100644 --- a/modules/project/main.tf +++ b/modules/project/main.tf @@ -137,10 +137,12 @@ resource "google_resource_manager_lien" "lien" { } resource "google_essential_contacts_contact" "contact" { - provider = google-beta - for_each = var.contacts - parent = "projects/${local.project.project_id}" - email = each.key + provider = google-beta + for_each = var.contacts + parent = "projects/${local.project.project_id}" + email = lookup( + local.ctx.email_addresses, each.key, each.key + ) language_tag = "en" notification_category_subscriptions = each.value depends_on = [ diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 6b771c1c5..49db4969a 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -38,6 +38,17 @@ variable "contacts" { type = map(list(string)) default = {} nullable = false + validation { + condition = alltrue(flatten([ + for k, v in var.contacts : [ + for vv in v : contains([ + "ALL", "SUSPENSION", "SECURITY", "TECHNICAL", "BILLING", "LEGAL", + "PRODUCT_UPDATES" + ], vv) + ] + ])) + error_message = "Invalid contact notification value." + } } variable "context" { @@ -45,6 +56,7 @@ variable "context" { type = object({ condition_vars = optional(map(map(string)), {}) custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) kms_keys = optional(map(string), {}) iam_principals = optional(map(string), {}) diff --git a/tests/modules/folder/context.tfvars b/tests/modules/folder/context.tfvars index 80ff9e02b..37e984252 100644 --- a/tests/modules/folder/context.tfvars +++ b/tests/modules/folder/context.tfvars @@ -10,6 +10,9 @@ context = { myrole_one = "organizations/366118655033/roles/myRoleOne" myrole_two = "organizations/366118655033/roles/myRoleTwo" } + email_addresses = { + default = "foo@example.com" + } folder_ids = { default = "organizations/1234567890" } @@ -22,6 +25,9 @@ context = { "test/one" = "tagValues/1234567890" } } +contacts = { + "$email_addresses:default" = ["ALL"] +} iam = { "$custom_roles:myrole_one" = [ "$iam_principals:myuser" diff --git a/tests/modules/folder/context.yaml b/tests/modules/folder/context.yaml index 49f50e7ff..e85e894be 100644 --- a/tests/modules/folder/context.yaml +++ b/tests/modules/folder/context.yaml @@ -13,6 +13,12 @@ # limitations under the License. values: + google_essential_contacts_contact.contact["$email_addresses:default"]: + email: foo@example.com + language_tag: en + notification_category_subscriptions: + - ALL + timeouts: null google_folder.folder[0]: deletion_protection: false display_name: Test Context @@ -81,10 +87,11 @@ values: tag_value: tagValues/1234567890 counts: + google_essential_contacts_contact: 1 google_folder: 1 google_folder_iam_binding: 4 google_folder_iam_member: 1 google_privileged_access_manager_entitlement: 1 google_tags_tag_binding: 1 modules: 0 - resources: 8 + resources: 9 diff --git a/tests/modules/organization/context.tfvars b/tests/modules/organization/context.tfvars index 993ab9836..42a52a5c5 100644 --- a/tests/modules/organization/context.tfvars +++ b/tests/modules/organization/context.tfvars @@ -11,6 +11,9 @@ context = { myrole_one = "organizations/366118655033/roles/myRoleOne" myrole_two = "organizations/366118655033/roles/myRoleTwo" } + email_addresses = { + default = "foo@example.com" + } iam_principals = { mygroup = "group:test-group@example.com" mysa = "serviceAccount:test@test-project.iam.gserviceaccount.com" @@ -38,6 +41,9 @@ context = { "test/one" = "tagValues/1234567890" } } +contacts = { + "$email_addresses:default" = ["ALL"] +} iam = { "$custom_roles:myrole_one" = [ "$iam_principals:myuser" diff --git a/tests/modules/organization/context.yaml b/tests/modules/organization/context.yaml index 6efce634e..99f6de3f8 100644 --- a/tests/modules/organization/context.yaml +++ b/tests/modules/organization/context.yaml @@ -18,6 +18,12 @@ values: dataset_id: logs project: test-prod-audit-logs-0 role: roles/bigquery.dataEditor + google_essential_contacts_contact.contact["$email_addresses:default"]: + email: foo@example.com + language_tag: en + notification_category_subscriptions: + - ALL + timeouts: null google_logging_organization_settings.default[0]: organization: '1234567890' storage_location: europe-west8 @@ -198,6 +204,7 @@ values: counts: google_bigquery_dataset_iam_member: 1 + google_essential_contacts_contact: 1 google_logging_organization_settings: 1 google_logging_organization_sink: 5 google_organization_iam_binding: 4 @@ -212,4 +219,4 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 24 + resources: 25 diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars index bafcb3889..80029fa8a 100644 --- a/tests/modules/project/context.tfvars +++ b/tests/modules/project/context.tfvars @@ -8,6 +8,9 @@ context = { myrole_one = "organizations/366118655033/roles/myRoleOne" myrole_two = "organizations/366118655033/roles/myRoleTwo" } + email_addresses = { + default = "foo@example.com" + } folder_ids = { "test/prod" = "folders/6789012345" } @@ -32,6 +35,9 @@ context = { default = "accessPolicies/888933661165/servicePerimeters/default" } } +contacts = { + "$email_addresses:default" = ["ALL"] +} parent = "$folder_ids:test/prod" iam = { "$custom_roles:myrole_one" = [ diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml index a2b4ba97a..030d08ab5 100644 --- a/tests/modules/project/context.yaml +++ b/tests/modules/project/context.yaml @@ -19,6 +19,12 @@ values: deletion_policy: null host_project: test-vpc-host service_project: my-project + google_essential_contacts_contact.contact["$email_addresses:default"]: + email: foo@example.com + language_tag: en + notification_category_subscriptions: + - ALL + timeouts: null google_kms_crypto_key_iam_member.service_agent_cmek["key-0.compute-system"]: condition: [] crypto_key_id: projects/kms-central-prj/locations/europe-west1/keyRings/my-keyring/cryptoKeys/ew1-compute @@ -174,6 +180,7 @@ values: counts: google_access_context_manager_service_perimeter_resource: 1 google_compute_shared_vpc_service_project: 1 + google_essential_contacts_contact: 1 google_kms_crypto_key_iam_member: 1 google_privileged_access_manager_entitlement: 1 google_project: 1 @@ -186,4 +193,4 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 24 + resources: 25 diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py index e3847045a..17c9dd1ca 100755 --- a/tools/duplicate-diff.py +++ b/tools/duplicate-diff.py @@ -72,9 +72,10 @@ duplicates = [ "modules/net-vpc-firewall/schemas/firewall-rules.schema.json", ], [ - "fast/stages/2-project-factory/schemas/folder.schema.json", - "fast/stages/2-networking/schemas/folder.schema.json", "fast/stages/0-org-setup/schemas/folder.schema.json", + "fast/stages/2-networking/schemas/folder.schema.json", + "fast/stages/2-project-factory/schemas/folder.schema.json", + "fast/stages/2-security/schemas/folder.schema.json", "modules/project-factory/schemas/folder.schema.json", ], [