From c291b218b6099107b33012810e689cf6cc5f4ff8 Mon Sep 17 00:00:00 2001 From: avh01 <106401285+avh01@users.noreply.github.com> Date: Tue, 9 Sep 2025 03:18:30 -0400 Subject: [PATCH 1/5] Added support for cross-project NEGs in net-lb-app-int module (#3286) * Added support for cross-project negs in net-lb-app-int module * Fixed formatting * Added example to readme. * Update README.md Made a small correction to the example I added to readme. --------- Co-authored-by: Ludovico Magnocavallo Co-authored-by: Julio Castillo --- modules/net-lb-app-int/README.md | 40 ++++++++++++++++++++++++++++++++ modules/net-lb-app-int/main.tf | 11 +++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/modules/net-lb-app-int/README.md b/modules/net-lb-app-int/README.md index b002cf6f0..46c926e4e 100644 --- a/modules/net-lb-app-int/README.md +++ b/modules/net-lb-app-int/README.md @@ -17,6 +17,7 @@ Due to the complexity of the underlying resources, changes to the configuration - [Hybrid NEG creation](#hybrid-neg-creation) - [Serverless NEG creation](#serverless-neg-creation) - [Private Service Connect NEG creation](#private-service-connect-neg-creation) + - [Private Service Connect NEG creation with Cross-project PSC and back-end](#private-service-connect-neg-creation-with-cross-project-psc-and-back-end) - [Internet NEG creation](#internet-neg-creation) - [URL Map](#url-map) - [SSL Certificates](#ssl-certificates) @@ -443,6 +444,45 @@ module "ilb-l7" { # tftest modules=1 resources=5 e2e ``` +#### Private Service Connect NEG creation with Cross-project PSC and back-end + +This example shows how to create the load balancer in one project `prj-host` while using a shared VPC deployed in the `prj-svc` project. Please note that the load balancer and its front-end will be created in the `prj-host` project and the back-end will be created in the `prj-svc` project. This is useful for situations where a shared VPC is being used that has been deployed in another project. Two subnetworks are needed, one for the loab balancer and another one for the PSC endpoint. + +```hcl +module "ilb-l7" { + source = "./fabric/modules/net-lb-app-int" + name = "ilb-test" + project_id = "prj-host" + region = "us-central1" + + backend_service_configs = { + default = { + project_id = "prj-svc" + backends = [{ + group = "neg-01" + }] + health_check_configs = {} + neg_configs = { + neg-01 = { + project_id = "prj-svc" + description = "Network Endpoint Group for service accessed using Private Service Connect" + psc = { + region = "us-central1" + target_service = "projects/producer_project/regions/us-central1/serviceAttachments/project_id" + network = var.vpc.self_link + subnetwork = "projects/prj-svc/regions/us-central1/subnetworks/psc_subnet" + } + } + } + } + } + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } +} +``` + #### Internet NEG creation This example shows how to create and manage internet NEGs: diff --git a/modules/net-lb-app-int/main.tf b/modules/net-lb-app-int/main.tf index da2163dd8..f562beb79 100644 --- a/modules/net-lb-app-int/main.tf +++ b/modules/net-lb-app-int/main.tf @@ -207,9 +207,13 @@ resource "google_compute_region_network_endpoint_group" "default" { resource "google_compute_region_network_endpoint_group" "psc" { for_each = local.neg_regional_psc - project = var.project_id - region = each.value.psc.region - name = "${var.name}-${each.key}" + project = ( + each.value.project_id == null + ? var.project_id + : each.value.project_id + ) + region = each.value.psc.region + name = "${var.name}-${each.key}" //description = coalesce(each.value.description, var.description) network_endpoint_type = "PRIVATE_SERVICE_CONNECT" psc_target_service = each.value.psc.target_service @@ -219,7 +223,6 @@ resource "google_compute_region_network_endpoint_group" "psc" { # ignore until https://github.com/hashicorp/terraform-provider-google/issues/20576 is fixed ignore_changes = [psc_data] } - } locals { From c1e8f9d70c20d65c55ab1041f4c051f93cb01929 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 9 Sep 2025 13:49:38 +0200 Subject: [PATCH 2/5] add support for startup script to compute-vm module (#3313) --- modules/compute-vm/README.md | 33 ++++++++++---------- modules/compute-vm/main.tf | 1 + modules/compute-vm/template.tf | 54 +++++++++++++++++---------------- modules/compute-vm/variables.tf | 7 +++++ 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md index fe6f3fae8..4569a7c2f 100644 --- a/modules/compute-vm/README.md +++ b/modules/compute-vm/README.md @@ -941,10 +941,10 @@ module "sole-tenancy" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L270) | Instance name. | string | ✓ | | -| [network_interfaces](variables.tf#L282) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | -| [project_id](variables.tf#L367) | Project id. | string | ✓ | | -| [zone](variables.tf#L480) | Compute zone. | string | ✓ | | +| [name](variables.tf#L277) | Instance name. | string | ✓ | | +| [network_interfaces](variables.tf#L289) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | +| [project_id](variables.tf#L374) | Project id. | string | ✓ | | +| [zone](variables.tf#L487) | Compute zone. | string | ✓ | | | [attached_disk_defaults](variables.tf#L17) | Defaults for attached disks options. | object({…}) | | {…} | | [attached_disks](variables.tf#L37) | Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | list(object({…})) | | [] | | [boot_disk](variables.tf#L82) | Boot disk properties. Initialize params are ignored when source is set. | object({…}) | | {…} | @@ -962,18 +962,19 @@ module "sole-tenancy" { | [instance_type](variables.tf#L246) | Instance type. | string | | "f1-micro" | | [labels](variables.tf#L252) | Instance labels. | map(string) | | {} | | [metadata](variables.tf#L258) | Instance metadata. | map(string) | | {} | -| [min_cpu_platform](variables.tf#L264) | Minimum CPU platform. | string | | null | -| [network_attached_interfaces](variables.tf#L275) | Network interfaces using network attachments. | list(string) | | [] | -| [network_tag_bindings](variables.tf#L303) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [options](variables.tf#L310) | Instance options. | object({…}) | | {…} | -| [project_number](variables.tf#L372) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | -| [scratch_disks](variables.tf#L378) | Scratch disks configuration. | object({…}) | | {…} | -| [service_account](variables.tf#L390) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | -| [shielded_config](variables.tf#L400) | Shielded VM configuration of the instances. | object({…}) | | null | -| [snapshot_schedules](variables.tf#L410) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L453) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | -| [tag_bindings_immutable](variables.tf#L460) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | -| [tags](variables.tf#L474) | Instance network tags for firewall rule targets. | list(string) | | [] | +| [metadata_startup_script](variables.tf#L264) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | +| [min_cpu_platform](variables.tf#L271) | Minimum CPU platform. | string | | null | +| [network_attached_interfaces](variables.tf#L282) | Network interfaces using network attachments. | list(string) | | [] | +| [network_tag_bindings](variables.tf#L310) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance only for networking purposes, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [options](variables.tf#L317) | Instance options. | object({…}) | | {…} | +| [project_number](variables.tf#L379) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | +| [scratch_disks](variables.tf#L385) | Scratch disks configuration. | object({…}) | | {…} | +| [service_account](variables.tf#L397) | Service account email and scopes. If email is null, the default Compute service account will be used unless auto_create is true, in which case a service account will be created. Set the variable to null to avoid attaching a service account. | object({…}) | | {} | +| [shielded_config](variables.tf#L407) | Shielded VM configuration of the instances. | object({…}) | | null | +| [snapshot_schedules](variables.tf#L417) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L460) | Resource manager tag bindings in arbitrary key => tag key or value id format. Set on both the instance and zonal disks, and modifiable without impacting the main resource lifecycle. | map(string) | | {} | +| [tag_bindings_immutable](variables.tf#L467) | Immutable resource manager tag bindings, in tagKeys/id => tagValues/id format. These are set on the instance or instance template at creation time, and trigger recreation if changed. | map(string) | | null | +| [tags](variables.tf#L481) | Instance network tags for firewall rule targets. | list(string) | | [] | ## Outputs diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf index f2c0faf35..daa6ced50 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -158,6 +158,7 @@ resource "google_compute_instance" "default" { enable_display = var.enable_display labels = var.labels metadata = var.metadata + metadata_startup_script = var.metadata_startup_script resource_policies = local.ischedule_attach dynamic "advanced_machine_features" { diff --git a/modules/compute-vm/template.tf b/modules/compute-vm/template.tf index c1f548c19..c58aee7bb 100644 --- a/modules/compute-vm/template.tf +++ b/modules/compute-vm/template.tf @@ -20,19 +20,20 @@ locals { } resource "google_compute_instance_template" "default" { - provider = google-beta - count = local.template_create && !local.template_regional ? 1 : 0 - project = var.project_id - region = local.region - name_prefix = "${var.name}-" - description = var.description - tags = var.tags - machine_type = var.instance_type - min_cpu_platform = var.min_cpu_platform - can_ip_forward = var.can_ip_forward - metadata = var.metadata - labels = var.labels - resource_manager_tags = var.tag_bindings_immutable + provider = google-beta + count = local.template_create && !local.template_regional ? 1 : 0 + project = var.project_id + region = local.region + name_prefix = "${var.name}-" + description = var.description + tags = var.tags + machine_type = var.instance_type + min_cpu_platform = var.min_cpu_platform + can_ip_forward = var.can_ip_forward + metadata = var.metadata + metadata_startup_script = var.metadata_startup_script + labels = var.labels + resource_manager_tags = var.tag_bindings_immutable dynamic "advanced_machine_features" { for_each = local.advanced_mf != null ? [""] : [] @@ -211,19 +212,20 @@ resource "google_compute_instance_template" "default" { } resource "google_compute_region_instance_template" "default" { - provider = google-beta - count = local.template_create && local.template_regional ? 1 : 0 - project = var.project_id - region = local.region - name_prefix = "${var.name}-" - description = var.description - tags = var.tags - machine_type = var.instance_type - min_cpu_platform = var.min_cpu_platform - can_ip_forward = var.can_ip_forward - metadata = var.metadata - labels = var.labels - resource_manager_tags = var.tag_bindings_immutable + provider = google-beta + count = local.template_create && local.template_regional ? 1 : 0 + project = var.project_id + region = local.region + name_prefix = "${var.name}-" + description = var.description + tags = var.tags + machine_type = var.instance_type + min_cpu_platform = var.min_cpu_platform + can_ip_forward = var.can_ip_forward + metadata = var.metadata + metadata_startup_script = var.metadata_startup_script + labels = var.labels + resource_manager_tags = var.tag_bindings_immutable dynamic "advanced_machine_features" { for_each = local.advanced_mf != null ? [""] : [] diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf index cafb63b99..9cd482119 100644 --- a/modules/compute-vm/variables.tf +++ b/modules/compute-vm/variables.tf @@ -261,6 +261,13 @@ variable "metadata" { default = {} } +variable "metadata_startup_script" { + description = "Instance startup script. Will trigger recreation on change, even after importing." + type = string + nullable = true + default = null +} + variable "min_cpu_platform" { description = "Minimum CPU platform." type = string From 63a22cd9a2f4b80a8d620efde006dd0da716ef64 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 10 Sep 2025 13:47:35 +0200 Subject: [PATCH 3/5] Refactor secret manager module (#3315) * wip * wip * wip * wip * tested, missing versions * working * fix secops stage * readme * tests * tflint --- fast/stages/3-secops-dev/secrets.tf | 51 +-- modules/cloud-function-v1/README.md | 4 +- modules/cloud-function-v2/README.md | 4 +- modules/cloud-run-v2/README.md | 30 +- modules/cloud-run/README.md | 30 +- modules/dataform-repository/README.md | 20 +- modules/secret-manager/README.md | 365 ++++++++++-------- modules/secret-manager/global.tf | 131 +++++++ modules/secret-manager/iam-regional.tf | 72 ++++ modules/secret-manager/iam.tf | 106 +++++ modules/secret-manager/main.tf | 111 ++---- modules/secret-manager/outputs.tf | 69 +++- modules/secret-manager/regional.tf | 91 +++++ modules/secret-manager/variables.tf | 120 ++++-- tests/examples/conftest.py | 5 + tests/fixtures/secret-credentials.tf | 21 +- tests/modules/cloud_run/examples/secrets.yaml | 61 ++- tests/modules/cloud_run/examples/simple.yaml | 44 ++- .../examples/service-iam-env.yaml | 18 +- .../secret_manager/examples/context.yaml | 54 +++ .../modules/secret_manager/examples/iam.yaml | 57 +-- .../secret_manager/examples/secret-cmek.yaml | 71 ---- .../examples/secret-regional.yaml | 59 +++ .../secret_manager/examples/secret.yaml | 79 +++- .../secret_manager/examples/versions.yaml | 62 ++- 25 files changed, 1222 insertions(+), 513 deletions(-) create mode 100644 modules/secret-manager/global.tf create mode 100644 modules/secret-manager/iam-regional.tf create mode 100644 modules/secret-manager/iam.tf create mode 100644 modules/secret-manager/regional.tf create mode 100644 tests/modules/secret_manager/examples/context.yaml delete mode 100644 tests/modules/secret_manager/examples/secret-cmek.yaml create mode 100644 tests/modules/secret_manager/examples/secret-regional.yaml diff --git a/fast/stages/3-secops-dev/secrets.tf b/fast/stages/3-secops-dev/secrets.tf index da0a676be..a89287d97 100644 --- a/fast/stages/3-secops-dev/secrets.tf +++ b/fast/stages/3-secops-dev/secrets.tf @@ -17,31 +17,32 @@ module "secops-tenant-secrets" { source = "../../../modules/secret-manager" project_id = module.project.project_id - secrets = merge({ - (local.secops_api_key_secret_key) = { - locations = [var.region] + secrets = merge( + { + (local.secops_api_key_secret_key) = { + global_replica_locations = { + (var.region) = null + } + labels = { scope = "secops" } + versions = { + latest = { + data = google_apikeys_key.feed_api_key.key_string + } + } + } + }, + !local.workspace_log_ingestion ? {} : { + (local.secops_workspace_int_sa_key) = { + global_replica_locations = { + (var.region) = null + } + labels = { scope = "secops" } + versions = { + latest = { + data = google_service_account_key.workspace_integration_key[0].private_key + } + } + } } - }, local.workspace_log_ingestion ? { - (local.secops_workspace_int_sa_key) = { - locations = [var.region] - } } : {} ) - versions = merge({ - (local.secops_api_key_secret_key) = { - latest = { - enabled = true, data = google_apikeys_key.feed_api_key.key_string - } - } - }, local.workspace_log_ingestion ? { - (local.secops_workspace_int_sa_key) = { - latest = { - enabled = true, data = google_service_account_key.workspace_integration_key[0].private_key - } - } - } : {}) - labels = merge({ - (local.secops_api_key_secret_key) = { scope = "secops" } - }, local.workspace_log_ingestion ? { - (local.secops_workspace_int_sa_key) = { scope = "secops" } - } : {}) } diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 6b427e654..551c798a6 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -280,7 +280,7 @@ module "cf-http" { project_id = var.project_number # use project_number to avoid perm-diff secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0] versions = [ - "${module.secret-manager.version_versions["credentials:v1"]}:/ver1" + "${module.secret-manager.version_versions["credentials/v1"]}:/ver1" ] } } @@ -288,7 +288,7 @@ module "cf-http" { google_project_iam_member.bucket_default_compute_account_grant, ] } -# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e +# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu ``` ### Using CMEK to encrypt function resources diff --git a/modules/cloud-function-v2/README.md b/modules/cloud-function-v2/README.md index dd81edb2a..0c0fa2c44 100644 --- a/modules/cloud-function-v2/README.md +++ b/modules/cloud-function-v2/README.md @@ -293,7 +293,7 @@ module "cf-http" { project_id = var.project_id secret = reverse(split("/", module.secret-manager.secrets["credentials"].name))[0] versions = [ - "${module.secret-manager.version_versions["credentials:v1"]}:ver1" + "${module.secret-manager.version_versions["credentials/v1"]}:ver1" ] } } @@ -302,7 +302,7 @@ module "cf-http" { ] } -# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e +# tftest fixtures=fixtures/secret-credentials.tf,fixtures/functions-default-sa-iam-grants.tf inventory=secrets.yaml e2e skip-tofu ``` ## Variables diff --git a/modules/cloud-run-v2/README.md b/modules/cloud-run-v2/README.md index 01a293b10..96141de7b 100644 --- a/modules/cloud-run-v2/README.md +++ b/modules/cloud-run-v2/README.md @@ -46,7 +46,7 @@ module "cloud_run" { env_from_key = { SECRET1 = { secret = module.secret-manager.secrets["credentials"].name - version = module.secret-manager.version_versions["credentials:v1"] + version = module.secret-manager.version_versions["credentials/v1"] } } } @@ -56,7 +56,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest fixtures=fixtures/secret-credentials.tf inventory=service-iam-env.yaml e2e +# tftest fixtures=fixtures/secret-credentials.tf inventory=service-iam-env.yaml e2e skip-tofu ``` ## Mounting secrets as volumes @@ -86,7 +86,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest fixtures=fixtures/secret-credentials.tf inventory=service-volume-secretes.yaml e2e +# tftest fixtures=fixtures/secret-credentials.tf inventory=service-volume-secretes.yaml e2e skip-tofu ``` ## Mounting GCS buckets @@ -491,18 +491,17 @@ module "secrets" { source = "./fabric/modules/secret-manager" project_id = var.project_id secrets = { - otel-config = {} - } - iam = { otel-config = { - "roles/secretmanager.secretAccessor" = [ - "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com", - ] - } - } - versions = { - otel-config = { - v1 = { enabled = true, data = file("${path.module}/config/otel-config.yaml") } + iam = { + "roles/secretmanager.secretAccessor" = [ + "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com" + ] + } + versions = { + v1 = { + data = file("${path.module}/config/otel-config.yaml") + } + } } } } @@ -555,7 +554,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest files=otel-config inventory=service-otel-sidecar.yaml e2e +# tftest files=otel-config inventory=service-otel-sidecar.yaml e2e skip-tofu ``` ## Eventarc triggers @@ -754,6 +753,7 @@ Unsupported variables / attributes: - containers.resources.startup_cpu_boost Additional configuration can be passwed as `job_config`: + - max_retries - maximum of retries per task - task_count - desired number of tasks - timeout - max allowed time per task, in seconds with up to nine fractional digits, ending with 's'. Example: `3.5s` diff --git a/modules/cloud-run/README.md b/modules/cloud-run/README.md index 9dbf641af..09d8c9556 100644 --- a/modules/cloud-run/README.md +++ b/modules/cloud-run/README.md @@ -29,11 +29,12 @@ module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = var.project_id secrets = { - credentials = {} - } - iam = { credentials = { - "roles/secretmanager.secretAccessor" = [module.cloud_run.service_account_iam_email] + iam = { + "roles/secretmanager.secretAccessor" = [ + module.cloud_run.service_account_iam_email + ] + } } } } @@ -63,7 +64,7 @@ module "cloud_run" { } service_account_create = true } -# tftest modules=2 resources=5 inventory=simple.yaml e2e +# tftest modules=2 resources=5 inventory=simple.yaml e2e skip-tofu ``` ## Mounting secrets as volumes @@ -73,16 +74,15 @@ module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = var.project_id secrets = { - credentials = {} - } - versions = { credentials = { - v1 = { enabled = true, data = "foo bar baz" } - } - } - iam = { - credentials = { - "roles/secretmanager.secretAccessor" = [module.cloud_run.service_account_iam_email] + iam = { + "roles/secretmanager.secretAccessor" = [ + module.cloud_run.service_account_iam_email + ] + } + versions = { + v1 = { data = "foo bar baz" } + } } } } @@ -112,7 +112,7 @@ module "cloud_run" { } } } -# tftest modules=2 resources=5 inventory=secrets.yaml e2e +# tftest modules=2 resources=5 inventory=secrets.yaml e2e skip-tofu ``` ## Revision annotations diff --git a/modules/dataform-repository/README.md b/modules/dataform-repository/README.md index 47699dc1b..f6e271791 100644 --- a/modules/dataform-repository/README.md +++ b/modules/dataform-repository/README.md @@ -2,8 +2,12 @@ This module allows managing a dataform repository, allows adding IAM permissions. Also enables attaching a remote repository. -## TODO -[] Add validation rules to variable. + +- [Examples](#examples) + - [Simple dataform repository with access configuration](#simple-dataform-repository-with-access-configuration) + - [Repository with an attached remote repository](#repository-with-an-attached-remote-repository) +- [Variables](#variables) + ## Examples @@ -34,11 +38,9 @@ module "secret" { project_id = "fast-bi-fabric" secrets = { my-secret = { - } - } - versions = { - my-secret = { - v1 = { enabled = true, data = "MYTOKEN" } + versions = { + v1 = { data = "MYTOKEN" } + } } } } @@ -51,10 +53,10 @@ module "dataform" { remote_repository_settings = { url = "my-url" secret_name = "my-secret" - token = module.secret.version_ids["my-secret:v1"] + token = module.secret.version_ids["my-secret/v1"] } } -# tftest modules=2 resources=3 +# tftest modules=2 resources=3 skip-tofu ``` ## Variables diff --git a/modules/secret-manager/README.md b/modules/secret-manager/README.md index 489517ad8..e2dcccae2 100644 --- a/modules/secret-manager/README.md +++ b/modules/secret-manager/README.md @@ -1,16 +1,13 @@ -# Google Secret Manager Module +# Google Secret Manager -Simple Secret Manager module that allows managing one or more secrets, their versions, and IAM bindings. - -Secret Manager locations are available via the `gcloud secrets locations list` command. - -**Warning:** managing versions will persist their data (the actual secret you want to protect) in the Terraform state in unencrypted form, accessible to any identity able to read or pull the state file. +This module allows managing one or more secrets with versions and IAM bindings. For global secrets, this module optionally supports [write-only attributes](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) for versions, which do not save data in state. -- [Secrets](#secrets) -- [Secret IAM bindings](#secret-iam-bindings) -- [Secret versions](#secret-versions) -- [Secret with customer managed encryption key](#secret-with-customer-managed-encryption-key) +- [Global Secrets](#global-secrets) +- [Regional Secrets](#regional-secrets) +- [IAM Bindings](#iam-bindings) +- [Secret Versions](#secret-versions) +- [Context Interpolations](#context-interpolations) - [Variables](#variables) - [Outputs](#outputs) - [Requirements](#requirements) @@ -18,9 +15,11 @@ Secret Manager locations are available via the `gcloud secrets locations list` c - [APIs](#apis) -## Secrets +## Global Secrets -The secret replication policy is automatically managed if no location is set, or manually managed if a list of locations is passed to the secret. +Secrets are created as global by default, with auto replication policy. For auto managed replication secrets the `kms_key` attribute can be used to configure CMEK via a global key. + +To configure a secret for user managed replication configure the `global_replica_locations` attribute. Non-auto secrets ignore the `kms_key` attribute, but use each element of the locations map to configure keys. ```hcl module "secret-manager" { @@ -28,183 +27,207 @@ module "secret-manager" { project_id = var.project_id secrets = { test-auto = {} - test-manual = { - expire_time = "2025-10-02T15:01:23Z" - locations = [var.regions.primary, var.regions.secondary] + test-auto-cmek = { + kms_key = "projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec" } - } -} -# tftest modules=1 resources=2 inventory=secret.yaml e2e -``` - -## Secret IAM bindings - -IAM bindings can be set per secret in the same way as for most other modules supporting IAM, using the `iam` variable. - -```hcl -module "secret-manager" { - source = "./fabric/modules/secret-manager" - project_id = var.project_id - secrets = { - test-auto = {} - test-manual = { - locations = [var.regions.primary, var.regions.secondary] - } - } - iam = { - test-auto = { - "roles/secretmanager.secretAccessor" = ["group:${var.group_email}"] - } - test-manual = { - "roles/secretmanager.secretAccessor" = ["group:${var.group_email}"] - } - } -} -# tftest modules=1 resources=4 inventory=iam.yaml e2e -``` - -## Secret versions - -As mentioned above, please be aware that **version data will be stored in state in unencrypted form**. - -```hcl -module "secret-manager" { - source = "./fabric/modules/secret-manager" - project_id = var.project_id - secrets = { - test-auto = {} - test-manual = { - locations = [var.regions.primary, var.regions.secondary] - } - } - versions = { - test-auto = { - v1 = { enabled = false, data = "auto foo bar baz" } - v2 = { enabled = true, data = "auto foo bar spam" } - }, - test-manual = { - v1 = { enabled = true, data = "manual foo bar spam" } - } - } -} -# tftest modules=1 resources=5 inventory=versions.yaml e2e -``` - -## Secret with customer managed encryption key - -CMEK will be used if an encryption key is set in the `keys` field of `secrets` object for the secret region. For secrets with auto-replication, a global key must be specified. - -```hcl -module "project" { - source = "./fabric/modules/project" - name = "sec-mgr" - billing_account = var.billing_account_id - prefix = var.prefix - parent = var.folder_id - services = [ - "cloudkms.googleapis.com", - "secretmanager.googleapis.com", - ] -} - -module "kms-global" { - source = "./fabric/modules/kms" - project_id = module.project.project_id - keyring = { - location = "global" - name = "${var.prefix}-keyring-global" - } - keys = { - "key-global" = { - } - } - iam = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - module.project.service_agents.secretmanager.iam_email - ] - } -} - - -module "kms-primary-region" { - source = "./fabric/modules/kms" - project_id = module.project.project_id - keyring = { - location = var.regions.primary - name = "${var.prefix}-keyring-regional" - } - keys = { - "key-regional" = { - } - } - iam = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - module.project.service_agents.secretmanager.iam_email - ] - } -} - -module "kms-secondary-region" { - source = "./fabric/modules/kms" - project_id = module.project.project_id - keyring = { - location = var.regions.secondary - name = "${var.prefix}-keyring-regional" - } - keys = { - "key-regional" = { - } - } - iam = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - module.project.service_agents.secretmanager.iam_email - ] - } -} - - -module "secret-manager" { - source = "./fabric/modules/secret-manager" - project_id = module.project.project_id - secrets = { - test-auto = { - keys = { - global = module.kms-global.keys.key-global.id + test-user = { + global_replica_locations = { + europe-west1 = null + europe-west3 = null } } - test-auto-nokeys = {} - test-manual = { - locations = [var.regions.primary, var.regions.secondary] - keys = { - "${var.regions.primary}" = module.kms-primary-region.keys.key-regional.id - "${var.regions.secondary}" = module.kms-secondary-region.keys.key-regional.id + test-user-cmek = { + global_replica_locations = { + europe-west1 = "projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1" + europe-west3 = "projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3" } } } } -# tftest inventory=secret-cmek.yaml e2e +# tftest modules=1 resources=4 inventory=secret.yaml skip-tofu +``` + +## Regional Secrets + +Regional secrets are identified by having the `location` attribute defined, and share the same interface with a few exceptions: the `global_replica_locations` is of course ignored, and versions only support a subset of attributes and can't use write-only attributes. + +```hcl +module "secret-manager" { + source = "./fabric/modules/secret-manager" + project_id = var.project_id + secrets = { + test = { + location = "europe-west1" + } + test-cmek = { + location = "europe-west1" + kms_key = "projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec" + } + } +} +# tftest modules=1 resources=2 inventory=secret-regional.yaml skip-tofu +``` + +## IAM Bindings + +This module supports the same IAM interface as all other modules in this repository. IAM bindings are defined per secret, if you need cross-secret IAM bindings use project-level ones. + +```hcl +module "secret-manager" { + source = "./fabric/modules/secret-manager" + project_id = var.project_id + secrets = { + test = { + iam = { + "roles/secretmanager.admin" = [ + "user:test-0@example.com" + ] + } + iam_bindings = { + test = { + role = "roles/secretmanager.secretAccessor" + members = [ + "user:test-1@example.com" + ] + condition = { + title = "Test." + expression = "resource.matchTag('1234567890/environment', 'test')" + } + } + } + iam_bindings_additive = { + test = { + role = "roles/secretmanager.viewer" + member = "user:test-2@example.com" + } + } + } + } +} +# tftest modules=1 resources=4 inventory=iam.yaml skip-tofu +``` + +## Secret Versions + +Versions are defined per secret via the `versions` attribute, and by default they accept string data which is stored in state. The `data_config` attributes allow configuring each secret: + +- `data_config.is_file` instructs the module to read version data from a file (`data` is then used as the file path) +- `data_config.is_base64` instructs the provider to treat data as Base64 +- `data_config.write_only_version` instructs the module to **use write-only attributes so that data is not set in state**, each time the write-only version is changed data is reuploaded to the secret version + +As mentioned before write-only attributes are only available for global secrets. Regional secrets still use the potentially insecure way of storing data. + +```hcl +module "secret-manager" { + source = "./fabric/modules/secret-manager" + project_id = var.project_id + secrets = { + test = { + versions = { + a = { + # potentially unsafe + data = "foo" + } + b = { + # potentially unsafe, reads from file + data = "test-data/secret-b.txt" + data_config = { + is_file = true + } + } + c = { + # uses safer write-only attribute + data = "bar" + data_config = { + # bump this version when data needs updating + write_only_version = 1 + } + } + } + } + } +} +# tftest files=0 modules=1 resources=4 inventory=versions.yaml skip-tofu +``` + +```txt +foo-secret +# tftest-file id=0 path=test-data/secret-b.txt +``` + +## Context Interpolations + +Similarly to other core modules in this repository, this module also supports context-based interpolations, which are populated via the `context` variable. + +This is a summary table of the available contexts, which can be used whenever an attribute expects the relevant information. Refer to the [project factory module](../project-factory/README.md#context-based-interpolation) for more details on context replacements. + +- `$custom_roles:my_role` +- `$iam_principals:my_principal` +- `$kms_keys:my_key` +- `$locations:my_location` +- `$project_ids:my_project` +- `$tag_keys:my_key` +- `$tag_values:my_value` +- custom template variables used in IAM conditions + +This is a simple example that uses context interpolation. + +```hcl +module "secret-manager" { + source = "./fabric/modules/secret-manager" + context = { + iam_principals = { + mysa = "serviceAccount:test@foo-prod-test-0.iam.gserviceaccount.com" + myuser = "user:test@example.com" + } + kms_keys = { + primary = "projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1" + secondary = "projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3" + } + locations = { + primary = "europe-west1" + secondary = "europe-west3" + } + project_ids = { + test = "foo-prod-test-0" + } + } + project_id = "$project_ids:test" + secrets = { + test-user-cmek = { + global_replica_locations = { + "$locations:primary" = "$kms_keys:primary" + "$locations:secondary" = "$kms_keys:secondary" + } + iam = { + "roles/secretmanager.viewer" = [ + "$iam_principals:mysa", "$iam_principals:myuser" + ] + } + } + } +} +# tftest modules=1 resources=2 inventory=context.yaml skip-tofu ``` ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L29) | Project id where the keyring will be created. | string | ✓ | | -| [iam](variables.tf#L17) | IAM bindings in {SECRET => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {} | -| [labels](variables.tf#L23) | Optional labels for each secret. | map(map(string)) | | {} | -| [project_number](variables.tf#L34) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | string | | null | -| [secrets](variables.tf#L40) | Map of secrets to manage, their optional expire time, version destroy ttl, locations and KMS keys in {LOCATION => KEY} format. {GLOBAL => KEY} format enables CMEK for automatic managed secrets. If locations is null, automatic management will be set. | map(object({…})) | | {} | -| [versions](variables.tf#L52) | Optional versions to manage for each secret. Version names are only used internally to track individual versions. | map(map(object({…}))) | | {} | +| [project_id](variables.tf#L40) | Project id where the keyring will be created. | string | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [project_number](variables.tf#L45) | Project number of var.project_id. Set this to avoid permadiffs when creating tag bindings. | string | | null | +| [secrets](variables.tf#L51) | Map of secrets to manage. Defaults to global secrets unless region is set. | map(object({…})) | | {} | ## Outputs | name | description | sensitive | |---|---|:---:| -| [ids](outputs.tf#L17) | Fully qualified secret ids. | | -| [secrets](outputs.tf#L27) | Secret resources. | | -| [version_ids](outputs.tf#L36) | Version ids keyed by secret name : version name. | | -| [version_versions](outputs.tf#L46) | Version versions keyed by secret name : version name. | | -| [versions](outputs.tf#L56) | Secret versions. | ✓ | +| [ids](outputs.tf#L28) | Fully qualified secret ids. | | +| [secrets](outputs.tf#L41) | Secret resources. | | +| [version_ids](outputs.tf#L54) | Fully qualified version ids. | | +| [version_versions](outputs.tf#L67) | Version versions. | | +| [versions](outputs.tf#L80) | Version resources. | ✓ | ## Requirements diff --git a/modules/secret-manager/global.tf b/modules/secret-manager/global.tf new file mode 100644 index 000000000..9c97bd0ac --- /dev/null +++ b/modules/secret-manager/global.tf @@ -0,0 +1,131 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + _tag_template_global = ( + "//secretmanager.googleapis.com/projects/%s/secrets/%s" + ) +} + +resource "google_secret_manager_secret" "default" { + for_each = { for k, v in var.secrets : k => v if v.location == null } + project = local.project_id + secret_id = each.key + labels = each.value.labels + annotations = each.value.annotations + version_aliases = try(each.value.version_config.aliases, null) + version_destroy_ttl = try(each.value.version_config.destroy_ttl, null) + expire_time = try(each.value.expiration_config.time, null) + ttl = try(each.value.expiration_config.ttl, null) + tags = { + for k, v in each.value.tags : + lookup(local.ctx.tag_keys, k, k) => lookup(local.ctx.tag_values, v, v) + } + deletion_protection = each.value.deletion_protection + dynamic "replication" { + for_each = try(each.value.global_replica_locations, null) == null ? [""] : [] + content { + auto { + dynamic "customer_managed_encryption" { + for_each = each.value.kms_key == null ? [] : [""] + content { + kms_key_name = lookup( + local.ctx.kms_keys, each.value.kms_key, each.value.kms_key + ) + } + } + } + } + } + dynamic "replication" { + for_each = try(each.value.global_replica_locations, null) != null ? [""] : [] + content { + user_managed { + dynamic "replicas" { + for_each = each.value.global_replica_locations + content { + location = lookup(local.ctx.locations, replicas.key, replicas.key) + dynamic "customer_managed_encryption" { + for_each = replicas.value == null ? [] : [""] + content { + kms_key_name = lookup( + local.ctx.kms_keys, replicas.value, replicas.value + ) + } + } + } + } + } + } + } + # dynamic "rotation" { + # for_each = try(each.value.rotation_config, null) == null ? [] : [""] + # content { + # next_rotation_time = each.value.rotation_config.next_time + # rotation_period = each.value.rotation_config.period + # } + # } + # topics + lifecycle { + ignore_changes = [ + rotation[0].next_rotation_time + ] + } +} + +resource "google_secret_manager_secret_version" "default" { + for_each = { + for v in local.versions : + "${v.secret}/${v.version}" => v if v.location == null + } + secret = google_secret_manager_secret.default[each.value.secret].id + deletion_policy = each.value.deletion_policy + enabled = each.value.enabled + is_secret_data_base64 = try( + each.value.data_config.is_base64, null + ) + secret_data_wo_version = try( + each.value.data_config.write_only_version, null + ) + secret_data = ( + try(each.value.data_config.write_only_version, null) != null + ? null + : ( + try(each.value.data_config.is_file, null) == true + ? file(each.value.data) + : each.value.data + ) + ) + secret_data_wo = ( + try(each.value.data_config.write_only_version, null) == null + ? null + : ( + try(each.value.data_config.is_file, null) == true + ? file(each.value.data) + : each.value.data + ) + ) +} + +resource "google_tags_tag_binding" "binding" { + for_each = { for k, v in local.tag_bindings : k => v if v.location == null } + parent = format( + local._tag_template_global, + local.tag_project, + google_secret_manager_secret.default[each.value.secret].secret_id + ) + tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag) +} diff --git a/modules/secret-manager/iam-regional.tf b/modules/secret-manager/iam-regional.tf new file mode 100644 index 000000000..a56c77e89 --- /dev/null +++ b/modules/secret-manager/iam-regional.tf @@ -0,0 +1,72 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_secret_manager_regional_secret_iam_binding" "authoritative" { + for_each = { + for binding in local.secret_iam : + "${binding.secret}.${binding.role}" => binding if !binding.global + } + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + secret_id = google_secret_manager_regional_secret.default[each.value.secret].id + location = google_secret_manager_regional_secret.default[each.value.secret].location + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] +} + +resource "google_secret_manager_regional_secret_iam_binding" "bindings" { + for_each = { + for k, v in local.secret_iam_bindings : k => v if !v.global + } + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + secret_id = google_secret_manager_regional_secret.default[each.value.secret].id + location = google_secret_manager_regional_secret.default[each.value.secret].location + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_secret_manager_regional_secret_iam_member" "members" { + for_each = { + for k, v in local.secret_iam_bindings_additive : k => v if !v.global + } + secret_id = google_secret_manager_regional_secret.default[each.value.secret].id + location = google_secret_manager_regional_secret.default[each.value.secret].location + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup(local.ctx.iam_principals, each.value.member, each.value.member) + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) + title = each.value.condition.title + description = each.value.condition.description + } + } +} diff --git a/modules/secret-manager/iam.tf b/modules/secret-manager/iam.tf new file mode 100644 index 000000000..661a6cf8a --- /dev/null +++ b/modules/secret-manager/iam.tf @@ -0,0 +1,106 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + secret_iam = flatten([ + for k, v in var.secrets : [ + for role, members in v.iam : { + secret = k + role = role + members = members + global = v.location == null + } + ] + ]) + secret_iam_bindings = merge([ + for k, v in var.secrets : { + for binding_key, data in v.iam_bindings : + "${k}-${binding_key}" => { + secret = k + role = data.role + members = data.members + condition = data.condition + global = v.location == null + } + } + ]...) + secret_iam_bindings_additive = merge([ + for k, v in var.secrets : { + for binding_key, data in v.iam_bindings_additive : + "${k}-${binding_key}" => { + secret = k + role = data.role + member = data.member + condition = data.condition + global = v.location == null + } + } + ]...) +} + +resource "google_secret_manager_secret_iam_binding" "authoritative" { + for_each = { + for binding in local.secret_iam : + "${binding.secret}.${binding.role}" => binding if binding.global + } + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + secret_id = google_secret_manager_secret.default[each.value.secret].id + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] +} + +resource "google_secret_manager_secret_iam_binding" "bindings" { + for_each = { + for k, v in local.secret_iam_bindings : k => v if v.global + } + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + secret_id = google_secret_manager_secret.default[each.value.secret].id + members = [ + for v in each.value.members : + lookup(local.ctx.iam_principals, v, v) + ] + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_secret_manager_secret_iam_member" "members" { + for_each = { + for k, v in local.secret_iam_bindings_additive : k => v if v.global + } + secret_id = google_secret_manager_secret.default[each.value.secret].id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup(local.ctx.iam_principals, each.value.member, each.value.member) + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) + title = each.value.condition.title + description = each.value.condition.description + } + } +} diff --git a/modules/secret-manager/main.tf b/modules/secret-manager/main.tf index a81d9ba53..03e854d87 100644 --- a/modules/secret-manager/main.tf +++ b/modules/secret-manager/main.tf @@ -15,99 +15,40 @@ */ locals { - # distinct is needed to make the expanding function argument work - iam = flatten([ - for secret, roles in var.iam : [ - for role, members in roles : { - secret = secret - role = role - members = members - } - ] - ]) + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } + ctx_p = "$" + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) + tag_project = coalesce(var.project_number, var.project_id) tag_bindings = merge([ for k, v in var.secrets : { for kk, vv in v.tag_bindings : "${k}/${kk}" => { - parent = "//secretmanager.googleapis.com/projects/${coalesce(var.project_number, var.project_id)}/secrets/${google_secret_manager_secret.default[k].secret_id}" - tag_value = vv + location = v.location + secret = k + tag = vv } } if v.tag_bindings != null ]...) - version_pairs = flatten([ - for secret, versions in var.versions : [ - for name, attrs in versions : merge(attrs, { name = name, secret = secret }) + versions = flatten([ + for k, v in var.secrets : [ + for sk, sv in v.versions : merge(sv, { + secret = k + version = sk + location = v.location + }) ] ]) - version_keypairs = { - for pair in local.version_pairs : "${pair.secret}:${pair.name}" => pair - } } -resource "google_secret_manager_secret" "default" { - for_each = var.secrets - project = var.project_id - secret_id = each.key - labels = lookup(var.labels, each.key, null) - expire_time = each.value.expire_time - version_destroy_ttl = each.value.version_destroy_ttl - - dynamic "replication" { - for_each = each.value.locations == null ? [""] : [] - content { - auto { - dynamic "customer_managed_encryption" { - for_each = try(lookup(each.value.keys, "global", null) == null ? [] : [""], []) - content { - kms_key_name = each.value.keys["global"] - } - } - } - } - } - - dynamic "replication" { - for_each = each.value.locations == null ? [] : [""] - content { - user_managed { - dynamic "replicas" { - for_each = each.value.locations - iterator = location - content { - location = location.value - dynamic "customer_managed_encryption" { - for_each = try(lookup(each.value.keys, location.value, null) == null ? [] : [""], []) - content { - kms_key_name = each.value.keys[location.value] - } - } - } - } - } - } - } -} - -resource "google_secret_manager_secret_version" "default" { - provider = google-beta - for_each = local.version_keypairs - secret = google_secret_manager_secret.default[each.value.secret].id - enabled = each.value.enabled - secret_data = each.value.data -} - -resource "google_secret_manager_secret_iam_binding" "default" { - provider = google-beta - for_each = { - for binding in local.iam : "${binding.secret}.${binding.role}" => binding - } - role = each.value.role - secret_id = google_secret_manager_secret.default[each.value.secret].id - members = each.value.members -} - -resource "google_tags_tag_binding" "binding" { - for_each = local.tag_bindings - parent = each.value.parent - tag_value = each.value.tag_value -} +# resource "google_kms_key_handle" "my_key_handle" { +# provider = google-beta +# for_each = var.kms_autokey_config +# project = var.project_id +# name = each.key +# location = each.value +# resource_type_selector = "secretmanager.googleapis.com/Secret" +# } diff --git a/modules/secret-manager/outputs.tf b/modules/secret-manager/outputs.tf index 89215567b..b6f304dec 100644 --- a/modules/secret-manager/outputs.tf +++ b/modules/secret-manager/outputs.tf @@ -14,50 +14,79 @@ * limitations under the License. */ +locals { + o_secrets = merge( + google_secret_manager_secret.default, + google_secret_manager_regional_secret.default + ) + o_versions = merge( + google_secret_manager_secret_version.default, + google_secret_manager_regional_secret_version.default + ) +} + output "ids" { description = "Fully qualified secret ids." - value = { - for k, v in google_secret_manager_secret.default : v.secret_id => v.id - } + value = { for k, v in local.o_secrets : k => v.id } depends_on = [ - google_secret_manager_secret_iam_binding.default + google_secret_manager_secret_iam_binding.authoritative, + google_secret_manager_secret_iam_binding.bindings, + google_secret_manager_secret_iam_member.members, + google_secret_manager_regional_secret_iam_binding.authoritative, + google_secret_manager_regional_secret_iam_binding.bindings, + google_secret_manager_regional_secret_iam_member.members ] } output "secrets" { description = "Secret resources." - value = google_secret_manager_secret.default + value = local.o_secrets depends_on = [ - google_secret_manager_secret_iam_binding.default + google_secret_manager_secret_iam_binding.authoritative, + google_secret_manager_secret_iam_binding.bindings, + google_secret_manager_secret_iam_member.members, + google_secret_manager_regional_secret_iam_binding.authoritative, + google_secret_manager_regional_secret_iam_binding.bindings, + google_secret_manager_regional_secret_iam_member.members ] - } output "version_ids" { - description = "Version ids keyed by secret name : version name." - value = { - for k, v in google_secret_manager_secret_version.default : k => v.id - } + description = "Fully qualified version ids." + value = { for k, v in local.o_versions : k => v.id } depends_on = [ - google_secret_manager_secret_iam_binding.default + google_secret_manager_secret_iam_binding.authoritative, + google_secret_manager_secret_iam_binding.bindings, + google_secret_manager_secret_iam_member.members, + google_secret_manager_regional_secret_iam_binding.authoritative, + google_secret_manager_regional_secret_iam_binding.bindings, + google_secret_manager_regional_secret_iam_member.members ] } output "version_versions" { - description = "Version versions keyed by secret name : version name." - value = { - for k, v in google_secret_manager_secret_version.default : k => v.version - } + description = "Version versions." + value = { for k, v in local.o_versions : k => v.version } depends_on = [ - google_secret_manager_secret_iam_binding.default + google_secret_manager_secret_iam_binding.authoritative, + google_secret_manager_secret_iam_binding.bindings, + google_secret_manager_secret_iam_member.members, + google_secret_manager_regional_secret_iam_binding.authoritative, + google_secret_manager_regional_secret_iam_binding.bindings, + google_secret_manager_regional_secret_iam_member.members ] } output "versions" { - description = "Secret versions." - value = google_secret_manager_secret_version.default + description = "Version resources." + value = local.o_versions sensitive = true depends_on = [ - google_secret_manager_secret_iam_binding.default + google_secret_manager_secret_iam_binding.authoritative, + google_secret_manager_secret_iam_binding.bindings, + google_secret_manager_secret_iam_member.members, + google_secret_manager_regional_secret_iam_binding.authoritative, + google_secret_manager_regional_secret_iam_binding.bindings, + google_secret_manager_regional_secret_iam_member.members ] } diff --git a/modules/secret-manager/regional.tf b/modules/secret-manager/regional.tf new file mode 100644 index 000000000..9122dba65 --- /dev/null +++ b/modules/secret-manager/regional.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + _tag_template_regional = ( + "//secretmanager.googleapis.com/projects/%s/locations/%s/secrets/%s" + ) +} + +resource "google_secret_manager_regional_secret" "default" { + for_each = { for k, v in var.secrets : k => v if v.location != null } + project = local.project_id + location = lookup( + local.ctx.locations, each.value.location, each.value.location + ) + secret_id = each.key + labels = each.value.labels + annotations = each.value.annotations + version_aliases = try(each.value.version_config.aliases, null) + version_destroy_ttl = try(each.value.version_config.destroy_ttl, null) + expire_time = try(each.value.expiration_config.time, null) + ttl = try(each.value.expiration_config.ttl, null) + tags = each.value.tags + deletion_protection = each.value.deletion_protection + dynamic "customer_managed_encryption" { + for_each = each.value.kms_key == null ? [] : [""] + content { + kms_key_name = lookup( + local.ctx.kms_keys, each.value.kms_key, each.value.kms_key + ) + } + } + # dynamic "rotation" { + # for_each = try(each.value.rotation_config, null) == null ? [] : [""] + # content { + # next_rotation_time = each.value.rotation_config.next_time + # rotation_period = each.value.rotation_config.period + # } + # } + # topics + lifecycle { + ignore_changes = [ + rotation[0].next_rotation_time + ] + } +} + +resource "google_secret_manager_regional_secret_version" "default" { + for_each = { + for v in local.versions : + "${v.secret}/${v.version}" => v if v.location != null + } + secret = google_secret_manager_regional_secret.default[each.value.secret].id + deletion_policy = each.value.deletion_policy + enabled = each.value.enabled + is_secret_data_base64 = try( + each.value.data_config.is_base64, null + ) + secret_data = ( + try(each.value.data_config.is_file, null) == true + ? file(each.value.data) + : each.value.data + ) +} + + +resource "google_tags_location_tag_binding" "binding" { + for_each = { for k, v in local.tag_bindings : k => v if v.location != null } + parent = format( + local._tag_template_regional, + local.tag_project, + lookup(local.ctx.locations, each.value.location, each.value.location), + google_secret_manager_regional_secret.default[each.value.secret].secret_id + ) + location = lookup(local.ctx.locations, each.value.location, each.value.location) + tag_value = lookup(local.ctx.tag_values, each.value.tag, each.value.tag) +} + diff --git a/modules/secret-manager/variables.tf b/modules/secret-manager/variables.tf index 3ed8d5761..5b1731211 100644 --- a/modules/secret-manager/variables.tf +++ b/modules/secret-manager/variables.tf @@ -14,17 +14,28 @@ * limitations under the License. */ -variable "iam" { - description = "IAM bindings in {SECRET => {ROLE => [MEMBERS]}} format." - type = map(map(list(string))) - default = {} +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + kms_keys = optional(map(string), {}) + locations = optional(map(string), {}) + project_ids = optional(map(string), {}) + tag_keys = optional(map(string), {}) + tag_values = optional(map(string), {}) + }) + default = {} + nullable = false } -variable "labels" { - description = "Optional labels for each secret." - type = map(map(string)) - default = {} -} +# variable "kms_autokey_config" { +# description = "Key handle definitions for KMS autokey, in name => location format. Injected in the context $kms_keys:autokey/ namespace." +# type = map(string) +# nullable = false +# default = {} +# } variable "project_id" { description = "Project id where the keyring will be created." @@ -38,22 +49,83 @@ variable "project_number" { } variable "secrets" { - description = "Map of secrets to manage, their optional expire time, version destroy ttl, locations and KMS keys in {LOCATION => KEY} format. {GLOBAL => KEY} format enables CMEK for automatic managed secrets. If locations is null, automatic management will be set." + description = "Map of secrets to manage. Defaults to global secrets unless region is set." type = map(object({ - expire_time = optional(string) - locations = optional(list(string)) - keys = optional(map(string)) - tag_bindings = optional(map(string)) - version_destroy_ttl = optional(string) + annotations = optional(map(string), {}) + deletion_protection = optional(bool) + kms_key = optional(string) + labels = optional(map(string), {}) + global_replica_locations = optional(map(string)) + location = optional(string) + tag_bindings = optional(map(string)) + tags = optional(map(string), {}) + expiration_config = optional(object({ + time = optional(string) + ttl = optional(string) + })) + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + version_config = optional(object({ + aliases = optional(map(number)) + destroy_ttl = optional(string) + }), {}) + versions = optional(map(object({ + data = string + deletion_policy = optional(string) + enabled = optional(bool) + data_config = optional(object({ + is_base64 = optional(bool, false) + is_file = optional(bool, false) + write_only_version = optional(number) + })) + })), {}) + # rotation_config = optional(object({ + # next_time = string + # period = number + # })) + # topics })) default = {} -} - -variable "versions" { - description = "Optional versions to manage for each secret. Version names are only used internally to track individual versions." - type = map(map(object({ - enabled = bool - data = string - }))) - default = {} + validation { + condition = alltrue([ + for k, v in var.secrets : + try(v.expiration_config.time, null) == null || + try(v.expiration_config.ttl, null) == null + ]) + error_message = "Only one of time and ttl can be set in expiration config." + } + validation { + condition = alltrue([ + for k, v in var.secrets : + v.location == null || v.global_replica_locations == null + ]) + error_message = "Global replication cannot be configured on regional secrets." + } + validation { + condition = alltrue(flatten([ + for k, v in var.secrets : [ + for sk, sv in v.versions : contains( + ["DELETE", "DISABLE", "ABANDON"], coalesce(sv.deletion_policy, "DELETE") + ) + ] + ])) + error_message = "Invalid version deletion policy." + } } diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 8f4cf785e..c5fdeb150 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -14,6 +14,8 @@ """Pytest configuration for testing code examples.""" import collections +import os + from pathlib import Path import marko @@ -68,6 +70,9 @@ def pytest_generate_tests(metafunc, test_group='example', continue if directive and not filter_tests(directive.args): continue + if os.environ.get( + 'TERRAFORM') == 'tofu' and 'skip-tofu' in directive.args: + continue if child.lang in ('hcl', 'tfvars'): path = module.relative_to(FABRIC_ROOT) name = f'{path}:{last_header}' diff --git a/tests/fixtures/secret-credentials.tf b/tests/fixtures/secret-credentials.tf index 7e584041a..a1c41b46b 100644 --- a/tests/fixtures/secret-credentials.tf +++ b/tests/fixtures/secret-credentials.tf @@ -18,19 +18,16 @@ module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = var.project_id secrets = { - credentials = {} - } - iam = { credentials = { - "roles/secretmanager.secretAccessor" = [ - "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com", - "serviceAccount:${var.project_id}@appspot.gserviceaccount.com", - ] - } - } - versions = { - credentials = { - v1 = { enabled = true, data = "manual foo bar spam" } + iam = { + "roles/secretmanager.secretAccessor" = [ + "serviceAccount:${var.project_number}-compute@developer.gserviceaccount.com", + "serviceAccount:${var.project_id}@appspot.gserviceaccount.com", + ] + } + versions = { + v1 = { data = "manual foo bar spam" } + } } } } diff --git a/tests/modules/cloud_run/examples/secrets.yaml b/tests/modules/cloud_run/examples/secrets.yaml index d537863c8..1907ca728 100644 --- a/tests/modules/cloud_run/examples/secrets.yaml +++ b/tests/modules/cloud_run/examples/secrets.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,13 @@ values: autogenerate_revision_name: false location: europe-west8 metadata: - - {} + - annotations: null + effective_labels: + goog-terraform-provisioned: 'true' + generation: 0 + labels: null + terraform_labels: + goog-terraform-provisioned: 'true' name: hello project: project-id template: @@ -35,28 +41,65 @@ values: - mount_path: /credentials name: credentials working_dir: null + node_selector: null + service_account_name: tf-cr-hello@project-id.iam.gserviceaccount.com volumes: - - empty_dir: [] + - csi: [] + empty_dir: [] name: credentials + nfs: [] secret: - default_mode: null items: - key: latest mode: null path: v1.txt - + secret_name: credentials + timeouts: null module.cloud_run.google_service_account.service_account[0]: account_id: tf-cr-hello + create_ignore_already_exists: null + description: null + disabled: false + display_name: Terraform Cloud Run hello. + email: tf-cr-hello@project-id.iam.gserviceaccount.com + member: serviceAccount:tf-cr-hello@project-id.iam.gserviceaccount.com project: project-id - + timeouts: null module.secret-manager.google_secret_manager_secret.default["credentials"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null project: project-id + replication: + - auto: + - customer_managed_encryption: [] + user_managed: [] + rotation: [] secret_id: credentials - - module.secret-manager.google_secret_manager_secret_iam_binding.default["credentials.roles/secretmanager.secretAccessor"]: - condition: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + ? module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["credentials.roles/secretmanager.secretAccessor"] + : condition: [] + members: + - serviceAccount:tf-cr-hello@project-id.iam.gserviceaccount.com role: roles/secretmanager.secretAccessor - + module.secret-manager.google_secret_manager_secret_version.default["credentials/v1"]: + deletion_policy: DELETE + enabled: true + is_secret_data_base64: false + secret_data: foo bar baz + secret_data_wo: null + secret_data_wo_version: 0 + timeouts: null counts: google_cloud_run_service: 1 google_secret_manager_secret: 1 diff --git a/tests/modules/cloud_run/examples/simple.yaml b/tests/modules/cloud_run/examples/simple.yaml index 5001c3867..9b96e4aba 100644 --- a/tests/modules/cloud_run/examples/simple.yaml +++ b/tests/modules/cloud_run/examples/simple.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,13 @@ values: autogenerate_revision_name: false location: europe-west8 metadata: - - {} + - annotations: null + effective_labels: + goog-terraform-provisioned: 'true' + generation: 0 + labels: null + terraform_labels: + goog-terraform-provisioned: 'true' name: hello project: project-id template: @@ -39,7 +45,10 @@ values: liveness_probe: [] volume_mounts: [] working_dir: null + node_selector: null + service_account_name: tf-cr-hello@project-id.iam.gserviceaccount.com volumes: [] + timeouts: null module.cloud_run.google_cloud_run_service_iam_binding.binding["roles/run.invoker"]: condition: [] location: europe-west8 @@ -50,17 +59,40 @@ values: service: hello module.cloud_run.google_service_account.service_account[0]: account_id: tf-cr-hello + create_ignore_already_exists: null + description: null disabled: false display_name: Terraform Cloud Run hello. + email: tf-cr-hello@project-id.iam.gserviceaccount.com + member: serviceAccount:tf-cr-hello@project-id.iam.gserviceaccount.com project: project-id + timeouts: null module.secret-manager.google_secret_manager_secret.default["credentials"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null project: project-id + replication: + - auto: + - customer_managed_encryption: [] + user_managed: [] + rotation: [] secret_id: credentials - module.secret-manager.google_secret_manager_secret_iam_binding.default["credentials.roles/secretmanager.secretAccessor"]: - condition: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + ? module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["credentials.roles/secretmanager.secretAccessor"] + : condition: [] + members: + - serviceAccount:tf-cr-hello@project-id.iam.gserviceaccount.com role: roles/secretmanager.secretAccessor - - counts: google_cloud_run_service: 1 google_cloud_run_service_iam_binding: 1 diff --git a/tests/modules/cloud_run_v2/examples/service-iam-env.yaml b/tests/modules/cloud_run_v2/examples/service-iam-env.yaml index 2bb377a28..09cc44d54 100644 --- a/tests/modules/cloud_run_v2/examples/service-iam-env.yaml +++ b/tests/modules/cloud_run_v2/examples/service-iam-env.yaml @@ -16,6 +16,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: annotations: null binary_authorization: [] + build_config: [] client: null client_version: null custom_audiences: null @@ -24,6 +25,8 @@ values: description: null effective_labels: goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false labels: null location: europe-west8 name: hello @@ -33,6 +36,7 @@ values: - annotations: null containers: - args: null + base_image_uri: null command: null depends_on: null env: @@ -54,8 +58,11 @@ values: working_dir: null encryption_key: null execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null labels: null + node_selector: [] revision: null + service_mesh: [] session_affinity: null volumes: [] vpc_access: [] @@ -69,6 +76,7 @@ values: role: roles/run.invoker module.secret-manager.google_secret_manager_secret.default["credentials"]: annotations: null + deletion_protection: false effective_labels: goog-terraform-provisioned: 'true' labels: null @@ -79,6 +87,7 @@ values: user_managed: [] rotation: [] secret_id: credentials + tags: null terraform_labels: goog-terraform-provisioned: 'true' timeouts: null @@ -86,19 +95,20 @@ values: ttl: null version_aliases: null version_destroy_ttl: null - module.secret-manager.google_secret_manager_secret_iam_binding.default["credentials.roles/secretmanager.secretAccessor"]: - condition: [] + ? module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["credentials.roles/secretmanager.secretAccessor"] + : condition: [] members: - serviceAccount:123-compute@developer.gserviceaccount.com - serviceAccount:project-id@appspot.gserviceaccount.com role: roles/secretmanager.secretAccessor - module.secret-manager.google_secret_manager_secret_version.default["credentials:v1"]: + module.secret-manager.google_secret_manager_secret_version.default["credentials/v1"]: deletion_policy: DELETE enabled: true is_secret_data_base64: false secret_data: manual foo bar spam + secret_data_wo: null + secret_data_wo_version: 0 timeouts: null - counts: google_cloud_run_v2_service: 1 google_cloud_run_v2_service_iam_binding: 1 diff --git a/tests/modules/secret_manager/examples/context.yaml b/tests/modules/secret_manager/examples/context.yaml new file mode 100644 index 000000000..9ffdcfd37 --- /dev/null +++ b/tests/modules/secret_manager/examples/context.yaml @@ -0,0 +1,54 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.secret-manager.google_secret_manager_secret.default["test-user-cmek"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + project: foo-prod-test-0 + replication: + - auto: [] + user_managed: + - replicas: + - customer_managed_encryption: + - kms_key_name: projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1 + location: europe-west1 + - customer_managed_encryption: + - kms_key_name: projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3 + location: europe-west3 + rotation: [] + secret_id: test-user-cmek + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["test-user-cmek.roles/secretmanager.viewer"]: + condition: [] + members: + - serviceAccount:test@foo-prod-test-0.iam.gserviceaccount.com + - user:test@example.com + role: roles/secretmanager.viewer + +counts: + google_secret_manager_secret: 1 + google_secret_manager_secret_iam_binding: 1 + modules: 1 + resources: 2 diff --git a/tests/modules/secret_manager/examples/iam.yaml b/tests/modules/secret_manager/examples/iam.yaml index 6732708a8..2d635ade4 100644 --- a/tests/modules/secret_manager/examples/iam.yaml +++ b/tests/modules/secret_manager/examples/iam.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,37 +13,48 @@ # limitations under the License. values: - module.secret-manager.google_secret_manager_secret.default["test-auto"]: + module.secret-manager.google_secret_manager_secret.default["test"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null project: project-id replication: - auto: - customer_managed_encryption: [] user_managed: [] - secret_id: test-auto - module.secret-manager.google_secret_manager_secret.default["test-manual"]: - project: project-id - replication: - - auto: [] - user_managed: - - replicas: - - customer_managed_encryption: [] - location: europe-west8 - - customer_managed_encryption: [] - location: europe-west9 - secret_id: test-manual - module.secret-manager.google_secret_manager_secret_iam_binding.default["test-auto.roles/secretmanager.secretAccessor"]: + rotation: [] + secret_id: test + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret_iam_binding.authoritative["test.roles/secretmanager.admin"]: condition: [] members: - - group:organization-admins@example.org - role: roles/secretmanager.secretAccessor - module.secret-manager.google_secret_manager_secret_iam_binding.default["test-manual.roles/secretmanager.secretAccessor"]: - condition: [] + - user:test-0@example.com + role: roles/secretmanager.admin + module.secret-manager.google_secret_manager_secret_iam_binding.bindings["test-test"]: + condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'test') + title: Test. members: - - group:organization-admins@example.org + - user:test-1@example.com role: roles/secretmanager.secretAccessor + module.secret-manager.google_secret_manager_secret_iam_member.members["test-test"]: + condition: [] + member: user:test-2@example.com + role: roles/secretmanager.viewer counts: - google_secret_manager_secret: 2 + google_secret_manager_secret: 1 google_secret_manager_secret_iam_binding: 2 - -outputs: {} \ No newline at end of file + google_secret_manager_secret_iam_member: 1 + modules: 1 + resources: 4 diff --git a/tests/modules/secret_manager/examples/secret-cmek.yaml b/tests/modules/secret_manager/examples/secret-cmek.yaml deleted file mode 100644 index 6cced3147..000000000 --- a/tests/modules/secret_manager/examples/secret-cmek.yaml +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - module.secret-manager.google_secret_manager_secret.default["test-auto"]: - annotations: null - labels: null - project: test-sec-mgr - replication: - - auto: - - {} - user_managed: [] - rotation: [] - secret_id: test-auto - timeouts: null - topics: [] - ttl: null - version_aliases: null - version_destroy_ttl: null - module.secret-manager.google_secret_manager_secret.default["test-auto-nokeys"]: - annotations: null - labels: null - project: test-sec-mgr - replication: - - auto: - - customer_managed_encryption: [] - user_managed: [] - rotation: [] - secret_id: test-auto-nokeys - timeouts: null - topics: [] - ttl: null - version_aliases: null - version_destroy_ttl: null - module.secret-manager.google_secret_manager_secret.default["test-manual"]: - annotations: null - labels: null - project: test-sec-mgr - replication: - - auto: [] - user_managed: - - replicas: - - location: europe-west8 - - location: europe-west9 - rotation: [] - secret_id: test-manual - timeouts: null - topics: [] - ttl: null - version_aliases: null - version_destroy_ttl: null - -counts: - google_kms_crypto_key: 3 - google_kms_key_ring: 3 - google_secret_manager_secret: 3 - modules: 5 - resources: 18 - -outputs: {} diff --git a/tests/modules/secret_manager/examples/secret-regional.yaml b/tests/modules/secret_manager/examples/secret-regional.yaml new file mode 100644 index 000000000..fcfbef3dd --- /dev/null +++ b/tests/modules/secret_manager/examples/secret-regional.yaml @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.secret-manager.google_secret_manager_regional_secret.default["test"]: + annotations: null + customer_managed_encryption: [] + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + location: europe-west1 + project: project-id + rotation: [] + secret_id: test + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_regional_secret.default["test-cmek"]: + annotations: null + customer_managed_encryption: + - kms_key_name: projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + location: europe-west1 + project: project-id + rotation: [] + secret_id: test-cmek + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + +counts: + google_secret_manager_regional_secret: 2 + modules: 1 + resources: 2 diff --git a/tests/modules/secret_manager/examples/secret.yaml b/tests/modules/secret_manager/examples/secret.yaml index 5761fc708..751f2701f 100644 --- a/tests/modules/secret_manager/examples/secret.yaml +++ b/tests/modules/secret_manager/examples/secret.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ values: module.secret-manager.google_secret_manager_secret.default["test-auto"]: annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' labels: null project: project-id replication: @@ -23,12 +26,41 @@ values: user_managed: [] rotation: [] secret_id: test-auto + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' timeouts: null topics: [] ttl: null version_aliases: null - module.secret-manager.google_secret_manager_secret.default["test-manual"]: + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret.default["test-auto-cmek"]: annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + project: project-id + replication: + - auto: + - customer_managed_encryption: + - kms_key_name: projects/test-0/locations/global/keyRings/test-g/cryptoKeys/sec + user_managed: [] + rotation: [] + secret_id: test-auto-cmek + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret.default["test-user"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' labels: null project: project-id replication: @@ -36,19 +68,48 @@ values: user_managed: - replicas: - customer_managed_encryption: [] - location: europe-west8 + location: europe-west1 - customer_managed_encryption: [] - location: europe-west9 + location: europe-west3 rotation: [] - secret_id: test-manual + secret_id: test-user + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' timeouts: null topics: [] ttl: null version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret.default["test-user-cmek"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + project: project-id + replication: + - auto: [] + user_managed: + - replicas: + - customer_managed_encryption: + - kms_key_name: projects/test-0/locations/europe-west1/keyRings/test-g/cryptoKeys/sec-ew1 + location: europe-west1 + - customer_managed_encryption: + - kms_key_name: projects/test-0/locations/europe-west3/keyRings/test-g/cryptoKeys/sec-ew3 + location: europe-west3 + rotation: [] + secret_id: test-user-cmek + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null counts: - google_secret_manager_secret: 2 + google_secret_manager_secret: 4 modules: 1 - resources: 2 - -outputs: {} \ No newline at end of file + resources: 4 diff --git a/tests/modules/secret_manager/examples/versions.yaml b/tests/modules/secret_manager/examples/versions.yaml index f65f5e482..92a3d3374 100644 --- a/tests/modules/secret_manager/examples/versions.yaml +++ b/tests/modules/secret_manager/examples/versions.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,18 +13,58 @@ # limitations under the License. values: - module.secret-manager.google_secret_manager_secret_version.default["test-auto:v1"]: - enabled: false - secret_data: auto foo bar baz - module.secret-manager.google_secret_manager_secret_version.default["test-auto:v2"]: + module.secret-manager.google_secret_manager_secret.default["test"]: + annotations: null + deletion_protection: false + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + project: project-id + replication: + - auto: + - customer_managed_encryption: [] + user_managed: [] + rotation: [] + secret_id: test + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + topics: [] + ttl: null + version_aliases: null + version_destroy_ttl: null + module.secret-manager.google_secret_manager_secret_version.default["test/a"]: + deletion_policy: DELETE enabled: true - secret_data: auto foo bar spam - module.secret-manager.google_secret_manager_secret_version.default["test-manual:v1"]: + is_secret_data_base64: false + secret_data: foo + secret_data_wo: null + secret_data_wo_version: 0 + timeouts: null + module.secret-manager.google_secret_manager_secret_version.default["test/b"]: + deletion_policy: DELETE enabled: true - secret_data: manual foo bar spam + is_secret_data_base64: false + secret_data: 'foo-secret + + # tftest-file id=0 path=test-data/secret-b.txt + + ' + secret_data_wo: null + secret_data_wo_version: 0 + timeouts: null + module.secret-manager.google_secret_manager_secret_version.default["test/c"]: + deletion_policy: DELETE + enabled: true + is_secret_data_base64: false + secret_data: null + secret_data_wo: null + secret_data_wo_version: 1 + timeouts: null counts: - google_secret_manager_secret: 2 + google_secret_manager_secret: 1 google_secret_manager_secret_version: 3 - -outputs: {} \ No newline at end of file + modules: 1 + resources: 4 From 461a10a669c466a0ef79c6f86001f12544f33f75 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 10 Sep 2025 13:49:33 +0200 Subject: [PATCH 4/5] Update README.md --- modules/secret-manager/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/secret-manager/README.md b/modules/secret-manager/README.md index e2dcccae2..ab231726e 100644 --- a/modules/secret-manager/README.md +++ b/modules/secret-manager/README.md @@ -1,6 +1,6 @@ # Google Secret Manager -This module allows managing one or more secrets with versions and IAM bindings. For global secrets, this module optionally supports [write-only attributes](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) for versions, which do not save data in state. +This module allows managing one or more secrets with versions and IAM bindings. For global secrets, this module optionally supports [write-only attributes](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) for versions, which do not save data in state. Write-only attributes are not yet supported in OpenTofu, so this module is only compatible with Terraform until [OpenTofu support](https://github.com/opentofu/opentofu/issues/2834) has been released. - [Global Secrets](#global-secrets) From 01e5e6d3e8e994726abc7f024c404fea8b30838c Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 10 Sep 2025 11:51:31 +0000 Subject: [PATCH 5/5] changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce7776d4..f4e6f7c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,28 @@ All notable changes to this project will be documented in this file. -## [Unreleased] +## [Unreleased] + +### BREAKING CHANGES + +- `modules/secret-manager`: the module interface has changed and been brought up to date with our current modules' shared interfaces; please test and refactor appropriately before using it in existing installations. This new version is **incompatible with OpenTofu** as it lacks support for write-once attributes. [[#3315](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3315)] +- `modules/secure-source-manager-instance`: Changed interface to declare private instances. [[#3310](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3310)] + + +### FAST + +- [[#3315](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3315)] Refactor secret manager module ([ludoo](https://github.com/ludoo)) +- [[#3305](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3305)] Improve context support in vpc-sc module and stage / new FAST stages small fixes ([ludoo](https://github.com/ludoo)) + +### MODULES + +- [[#3315](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3315)] Refactor secret manager module ([ludoo](https://github.com/ludoo)) +- [[#3313](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3313)] Add support for startup script to compute-vm module ([ludoo](https://github.com/ludoo)) +- [[#3286](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3286)] Added support for cross-project NEGs in net-lb-app-int module ([avh01](https://github.com/avh01)) +- [[#3310](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3310)] [secure-source-manager-instance] Allow provisioning of instances with managed certificates ([LucaPrete](https://github.com/LucaPrete)) +- [[#3308](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3308)] Add validation to kms key variables ([ludoo](https://github.com/ludoo)) +- [[#3307](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3307)] Add support for context in kms module ([ludoo](https://github.com/ludoo)) +- [[#3305](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3305)] Improve context support in vpc-sc module and stage / new FAST stages small fixes ([ludoo](https://github.com/ludoo)) ## [44.1.0] - 2025-09-06 @@ -1580,4 +1601,4 @@ Project templates are still following the old project factory schemas, and will [32.0.1]: [32.0.0]: [31.1.0]: -[31.0.0]: +[31.0.0]: \ No newline at end of file