From 6045cf6ae2b0a4bb3bac4379d903f745116b5e97 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 13 Oct 2025 16:54:13 +0200 Subject: [PATCH 01/13] Add support for contexts to compute-vm module (#3406) --- modules/compute-vm/README.md | 61 +++---- modules/compute-vm/main.tf | 112 ++++++++---- modules/compute-vm/outputs.tf | 2 +- modules/compute-vm/resource-policies.tf | 18 +- modules/compute-vm/tags.tf | 32 ++-- modules/compute-vm/template.tf | 66 +++++-- modules/compute-vm/variables.tf | 17 ++ .../context-template-regional.tfvars | 62 +++++++ .../compute_vm/context-template-regional.yaml | 122 +++++++++++++ .../compute_vm/context-template.tfvars | 60 +++++++ .../modules/compute_vm/context-template.yaml | 123 +++++++++++++ tests/modules/compute_vm/context-vm.tfvars | 59 +++++++ tests/modules/compute_vm/context-vm.yaml | 164 ++++++++++++++++++ tests/modules/compute_vm/tftest.yaml | 19 ++ 14 files changed, 815 insertions(+), 102 deletions(-) create mode 100644 tests/modules/compute_vm/context-template-regional.tfvars create mode 100644 tests/modules/compute_vm/context-template-regional.yaml create mode 100644 tests/modules/compute_vm/context-template.tfvars create mode 100644 tests/modules/compute_vm/context-template.yaml create mode 100644 tests/modules/compute_vm/context-vm.tfvars create mode 100644 tests/modules/compute_vm/context-vm.yaml create mode 100644 tests/modules/compute_vm/tftest.yaml diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md index 0844c5997..b2f627fe5 100644 --- a/modules/compute-vm/README.md +++ b/modules/compute-vm/README.md @@ -937,41 +937,42 @@ module "sole-tenancy" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L266) | Instance name. | string | ✓ | | -| [network_interfaces](variables.tf#L278) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | -| [project_id](variables.tf#L363) | Project id. | string | ✓ | | -| [zone](variables.tf#L483) | Compute zone. | string | ✓ | | +| [name](variables.tf#L283) | Instance name. | string | ✓ | | +| [network_interfaces](variables.tf#L295) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | +| [project_id](variables.tf#L380) | Project id. | string | ✓ | | +| [zone](variables.tf#L500) | 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({…}) | | {…} | | [can_ip_forward](variables.tf#L113) | Enable IP forwarding. | bool | | false | | [confidential_compute](variables.tf#L119) | Enable Confidential Compute for these instances. | bool | | false | -| [create_template](variables.tf#L125) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | -| [description](variables.tf#L134) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | -| [enable_display](variables.tf#L140) | Enable virtual display on the instances. | bool | | false | -| [encryption](variables.tf#L146) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | -| [gpu](variables.tf#L156) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | -| [group](variables.tf#L191) | Define this variable to create an instance group for instances. Disabled for template use. | object({…}) | | null | -| [hostname](variables.tf#L199) | Instance FQDN name. | string | | null | -| [iam](variables.tf#L205) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [instance_schedule](variables.tf#L211) | Assign or create and assign an instance schedule policy. Either resource policy id or create_config must be specified if not null. Set active to null to dtach a policy from vm before destroying. | object({…}) | | null | -| [instance_type](variables.tf#L235) | Instance type. | string | | "f1-micro" | -| [labels](variables.tf#L241) | Instance labels. | map(string) | | {} | -| [metadata](variables.tf#L247) | Instance metadata. | map(string) | | {} | -| [metadata_startup_script](variables.tf#L253) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | -| [min_cpu_platform](variables.tf#L260) | Minimum CPU platform. | string | | null | -| [network_attached_interfaces](variables.tf#L271) | Network interfaces using network attachments. | list(string) | | [] | -| [network_tag_bindings](variables.tf#L299) | 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#L306) | Instance options. | object({…}) | | {…} | -| [project_number](variables.tf#L368) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | -| [resource_policies](variables.tf#L374) | Resource policies to attach to the instance or template. | list(string) | | null | -| [scratch_disks](variables.tf#L381) | Scratch disks configuration. | object({…}) | | {…} | -| [service_account](variables.tf#L393) | 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#L403) | Shielded VM configuration of the instances. | object({…}) | | null | -| [snapshot_schedules](variables.tf#L413) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L456) | 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#L463) | 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#L477) | Instance network tags for firewall rule targets. | list(string) | | [] | +| [context](variables.tf#L125) | Context-specific interpolations. | object({…}) | | {} | +| [create_template](variables.tf#L142) | Create instance template instead of instances. Defaults to a global template. | object({…}) | | null | +| [description](variables.tf#L151) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | +| [enable_display](variables.tf#L157) | Enable virtual display on the instances. | bool | | false | +| [encryption](variables.tf#L163) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | +| [gpu](variables.tf#L173) | GPU information. Based on https://cloud.google.com/compute/docs/gpus. | object({…}) | | null | +| [group](variables.tf#L208) | Define this variable to create an instance group for instances. Disabled for template use. | object({…}) | | null | +| [hostname](variables.tf#L216) | Instance FQDN name. | string | | null | +| [iam](variables.tf#L222) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [instance_schedule](variables.tf#L228) | Assign or create and assign an instance schedule policy. Either resource policy id or create_config must be specified if not null. Set active to null to dtach a policy from vm before destroying. | object({…}) | | null | +| [instance_type](variables.tf#L252) | Instance type. | string | | "f1-micro" | +| [labels](variables.tf#L258) | Instance labels. | map(string) | | {} | +| [metadata](variables.tf#L264) | Instance metadata. | map(string) | | {} | +| [metadata_startup_script](variables.tf#L270) | Instance startup script. Will trigger recreation on change, even after importing. | string | | null | +| [min_cpu_platform](variables.tf#L277) | Minimum CPU platform. | string | | null | +| [network_attached_interfaces](variables.tf#L288) | Network interfaces using network attachments. | list(string) | | [] | +| [network_tag_bindings](variables.tf#L316) | 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#L323) | Instance options. | object({…}) | | {…} | +| [project_number](variables.tf#L385) | Project number. Used in tag bindings to avoid a permadiff. | string | | null | +| [resource_policies](variables.tf#L391) | Resource policies to attach to the instance or template. | list(string) | | null | +| [scratch_disks](variables.tf#L398) | Scratch disks configuration. | object({…}) | | {…} | +| [service_account](variables.tf#L410) | 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#L420) | Shielded VM configuration of the instances. | object({…}) | | null | +| [snapshot_schedules](variables.tf#L430) | Snapshot schedule resource policies that can be attached to disks. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L473) | 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#L480) | 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#L494) | 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 2880e47b9..4535cce8f 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -15,6 +15,7 @@ */ locals { + _region = join("-", slice(split("-", local.zone), 0, 2)) advanced_mf = var.options.advanced_machine_features attached_disks = { for i, disk in var.attached_disks : @@ -30,18 +31,27 @@ locals { for k, v in local.attached_disks : k => v if try(v.options.replica_zone, null) == null } + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" + gpu = var.gpu != null on_host_maintenance = ( var.options.spot || var.confidential_compute || local.gpu ? "TERMINATE" : "MIGRATE" ) - region = join("-", slice(split("-", var.zone), 0, 2)) - gpu = var.gpu != null + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) + region = lookup(local.ctx.locations, local._region, local._region) service_account = var.service_account == null ? null : { - email = ( - var.service_account.auto_create + email = (var.service_account.auto_create ? google_service_account.service_account[0].email - : var.service_account.email + : try( + local.ctx.iam_principals[var.service_account.email], + var.service_account.email + ) ) scopes = ( var.service_account.scopes != null ? var.service_account.scopes : ( @@ -63,12 +73,13 @@ locals { termination_action = ( var.options.spot || var.options.max_run_duration != null ? coalesce(var.options.termination_action, "STOP") : null ) + zone = lookup(local.ctx.locations, var.zone, var.zone) } resource "google_compute_disk" "boot" { count = !local.template_create && var.boot_disk.use_independent_disk ? 1 : 0 - project = var.project_id - zone = var.zone + project = local.project_id + zone = local.zone # by default, GCP creates boot disks with the same name as instance, the deviation here is kept for backwards # compatibility name = "${var.name}-boot" @@ -82,8 +93,12 @@ resource "google_compute_disk" "boot" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - raw_key = var.encryption.disk_encryption_key_raw - kms_key_self_link = var.encryption.kms_key_self_link + raw_key = var.encryption.disk_encryption_key_raw + kms_key_self_link = lookup( + local.ctx.kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) } } } @@ -93,8 +108,8 @@ resource "google_compute_disk" "disks" { for k, v in local.attached_disks_zonal : k => v if v.source_type != "attach" } - project = var.project_id - zone = var.zone + project = local.project_id + zone = local.zone name = "${var.name}-${each.key}" type = each.value.options.type size = each.value.size @@ -107,8 +122,12 @@ resource "google_compute_disk" "disks" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - raw_key = var.encryption.disk_encryption_key_raw - kms_key_self_link = var.encryption.kms_key_self_link + raw_key = var.encryption.disk_encryption_key_raw + kms_key_self_link = lookup( + local.ctx.kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) } } } @@ -119,9 +138,9 @@ resource "google_compute_region_disk" "disks" { for k, v in local.attached_disks_regional : k => v if v.source_type != "attach" } - project = var.project_id + project = local.project_id region = local.region - replica_zones = [var.zone, each.value.options.replica_zone] + replica_zones = [local.zone, each.value.options.replica_zone] name = "${var.name}-${each.key}" type = each.value.options.type size = each.value.size @@ -136,7 +155,12 @@ resource "google_compute_region_disk" "disks" { content { raw_key = var.encryption.disk_encryption_key_raw # TODO: check if self link works here - kms_key_name = var.encryption.kms_key_self_link + kms_key_name = lookup( + local.ctx.kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) + } } } @@ -144,8 +168,8 @@ resource "google_compute_region_disk" "disks" { resource "google_compute_instance" "default" { provider = google-beta count = local.template_create ? 0 : 1 - project = var.project_id - zone = var.zone + project = local.project_id + zone = local.zone name = var.name hostname = var.hostname description = var.description @@ -228,10 +252,20 @@ resource "google_compute_instance" "default" { : var.boot_disk.source ) disk_encryption_key_raw = ( - var.encryption != null ? var.encryption.disk_encryption_key_raw : null + var.encryption != null ? + try( + local.ctx.kms_keys[var.encryption.disk_encryption_key_raw], + var.encryption.disk_encryption_key_raw + ) + : null ) kms_key_self_link = ( - var.encryption != null ? var.encryption.kms_key_self_link : null + var.encryption != null + ? try( + local.ctx.kms_keys[var.encryption.kms_key_self_link], + var.encryption.kms_key_self_link + ) + : null ) dynamic "initialize_params" { for_each = ( @@ -263,15 +297,27 @@ resource "google_compute_instance" "default" { for_each = var.network_interfaces iterator = config content { - network = config.value.network - subnetwork = config.value.subnetwork - network_ip = try(config.value.addresses.internal, null) + network = lookup( + local.ctx.networks, config.value.network, config.value.network + ) + subnetwork = lookup( + local.ctx.subnets, config.value.subnetwork, config.value.subnetwork + ) + network_ip = try( + local.ctx.addresses[config.value.addresses.internal], + config.value.addresses.internal, + null + ) nic_type = config.value.nic_type stack_type = config.value.stack_type dynamic "access_config" { for_each = config.value.nat || config.value.network_tier != null ? [""] : [] content { - nat_ip = try(config.value.addresses.external, null) + nat_ip = try( + local.ctx.addresses[config.value.addresses.external], + config.value.addresses.external, + null + ) network_tier = try(config.value.network_tier, null) } } @@ -378,24 +424,26 @@ resource "google_compute_instance" "default" { } resource "google_compute_instance_iam_binding" "default" { - project = var.project_id + project = local.project_id for_each = var.iam - zone = var.zone + zone = local.zone instance_name = var.name - role = each.key - members = each.value - depends_on = [google_compute_instance.default] + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [ + for m in each.value : lookup(local.ctx.iam_principals, m, m) + ] + depends_on = [google_compute_instance.default] } resource "google_compute_instance_group" "unmanaged" { count = var.group != null && !local.template_create ? 1 : 0 - project = var.project_id + project = local.project_id network = ( length(var.network_interfaces) > 0 ? var.network_interfaces[0].network : "" ) - zone = var.zone + zone = local.zone name = var.name description = var.description instances = [google_compute_instance.default[0].self_link] @@ -411,7 +459,7 @@ resource "google_compute_instance_group" "unmanaged" { resource "google_service_account" "service_account" { count = try(var.service_account.auto_create, null) == true ? 1 : 0 - project = var.project_id + project = local.project_id account_id = "tf-vm-${var.name}" display_name = "Terraform VM ${var.name}." } diff --git a/modules/compute-vm/outputs.tf b/modules/compute-vm/outputs.tf index 79551b84a..7e3b97e88 100644 --- a/modules/compute-vm/outputs.tf +++ b/modules/compute-vm/outputs.tf @@ -57,7 +57,7 @@ output "internal_ips" { output "login_command" { description = "Command to SSH into the machine." - value = "gcloud compute ssh --project ${var.project_id} --zone ${var.zone} ${var.name}" + value = "gcloud compute ssh --project ${local.project_id} --zone ${local.zone} ${var.name}" } output "self_link" { diff --git a/modules/compute-vm/resource-policies.tf b/modules/compute-vm/resource-policies.tf index fbabd667f..e717ecd74 100644 --- a/modules/compute-vm/resource-policies.tf +++ b/modules/compute-vm/resource-policies.tf @@ -46,8 +46,8 @@ locals { resource "google_compute_resource_policy" "schedule" { count = var.instance_schedule != null ? 1 : 0 - project = var.project_id - region = substr(var.zone, 0, length(var.zone) - 2) + project = local.project_id + region = substr(local.zone, 0, length(local.zone) - 2) name = var.name description = coalesce( var.instance_schedule.description, "Schedule policy for ${var.name}." @@ -73,8 +73,8 @@ resource "google_compute_resource_policy" "schedule" { resource "google_compute_resource_policy" "snapshot" { for_each = var.snapshot_schedules - project = var.project_id - region = substr(var.zone, 0, length(var.zone) - 2) + project = local.project_id + region = substr(local.zone, 0, length(local.zone) - 2) name = "${var.name}-${each.key}" description = coalesce( each.value.description, "Schedule policy ${each.key} for ${var.name}." @@ -132,8 +132,8 @@ resource "google_compute_resource_policy" "snapshot" { resource "google_compute_disk_resource_policy_attachment" "boot" { for_each = var.boot_disk.snapshot_schedule != null ? toset(var.boot_disk.snapshot_schedule) : [] - project = var.project_id - zone = var.zone + project = local.project_id + zone = local.zone name = try( google_compute_resource_policy.snapshot[each.value].name, each.value @@ -153,8 +153,8 @@ resource "google_compute_disk_resource_policy_attachment" "attached" { "${attachment.disk_key}-${attachment.snapshot_schedule}" => attachment } - project = var.project_id - zone = var.zone + project = local.project_id + zone = local.zone name = try( google_compute_resource_policy.snapshot[each.value.snapshot_schedule].name, each.value.snapshot_schedule @@ -176,7 +176,7 @@ resource "google_compute_region_disk_resource_policy_attachment" "attached" { "${attachment.disk_key}-${attachment.snapshot_schedule}" => attachment } - project = var.project_id + project = local.project_id name = try( google_compute_resource_policy.snapshot[each.value.snapshot_schedule].name, each.value.snapshot_schedule diff --git a/modules/compute-vm/tags.tf b/modules/compute-vm/tags.tf index e06c9f8c0..d5a6a4fb7 100644 --- a/modules/compute-vm/tags.tf +++ b/modules/compute-vm/tags.tf @@ -46,7 +46,7 @@ locals { ]) tag_parent_base = format( "//compute.googleapis.com/projects/%s", - coalesce(var.project_number, var.project_id) + coalesce(var.project_number, local.project_id) ) } @@ -55,19 +55,19 @@ locals { resource "google_tags_location_tag_binding" "network" { for_each = local.template_create ? {} : var.network_tag_bindings parent = ( - "${local.tag_parent_base}/zones/${var.zone}/instances/${google_compute_instance.default[0].instance_id}" + "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) - tag_value = each.value - location = var.zone + tag_value = lookup(local.ctx.tag_values, each.value, each.value) + location = local.zone } resource "google_tags_location_tag_binding" "instance" { for_each = local.template_create ? {} : var.tag_bindings parent = ( - "${local.tag_parent_base}/zones/${var.zone}/instances/${google_compute_instance.default[0].instance_id}" + "${local.tag_parent_base}/zones/${local.zone}/instances/${google_compute_instance.default[0].instance_id}" ) - tag_value = each.value - location = var.zone + tag_value = lookup(local.ctx.tag_values, each.value, each.value) + location = local.zone } resource "google_tags_location_tag_binding" "boot_disks" { @@ -75,10 +75,10 @@ resource "google_tags_location_tag_binding" "boot_disks" { local.template_create ? {} : { for v in local.boot_disk_tags : v.key => v } ) parent = ( - "${local.tag_parent_base}/zones/${var.zone}/disks/${each.value.disk_id}" + "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" ) - tag_value = each.value.tag_value - location = var.zone + tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) + location = local.zone } resource "google_tags_location_tag_binding" "disks" { @@ -86,10 +86,10 @@ resource "google_tags_location_tag_binding" "disks" { local.template_create ? {} : { for v in local.disk_tags : v.key => v } ) parent = ( - "${local.tag_parent_base}/zones/${var.zone}/disks/${each.value.disk_id}" + "${local.tag_parent_base}/zones/${local.zone}/disks/${each.value.disk_id}" ) - tag_value = each.value.tag_value - location = var.zone + tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) + location = local.zone } resource "google_tags_location_tag_binding" "disks_regional" { @@ -99,7 +99,7 @@ resource "google_tags_location_tag_binding" "disks_regional" { parent = ( "${local.tag_parent_base}/regions/${local.region}/disks/${each.value.disk_id}" ) - tag_value = each.value.tag_value + tag_value = lookup(local.ctx.tag_values, each.value.tag_value, each.value.tag_value) location = local.region } @@ -108,8 +108,8 @@ resource "google_tags_location_tag_binding" "disks_regional" { # resource "google_tags_location_tag_binding" "template" { # for_each = local.template_create ? var.tag_bindings : {} # parent = ( -# "${local.tag_parent_base}/regions/${local.region}/instanceTemplates/${google_compute_instance.default[0].instance_id}" +# "${local.tag_parent_base}/regions/${local.region}/instanceTemplates/${google_compute_instance_template.default[0].instance_id}" # ) -# tag_value = each.value +# tag_value = lookup(local.ctx.tag_values, each.value, each.value) # location = local.region # } diff --git a/modules/compute-vm/template.tf b/modules/compute-vm/template.tf index 4e06f9be5..4001d3e65 100644 --- a/modules/compute-vm/template.tf +++ b/modules/compute-vm/template.tf @@ -22,7 +22,7 @@ locals { resource "google_compute_instance_template" "default" { provider = google-beta count = local.template_create && !local.template_regional ? 1 : 0 - project = var.project_id + project = local.project_id region = local.region name_prefix = "${var.name}-" description = var.description @@ -67,7 +67,11 @@ resource "google_compute_instance_template" "default" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = var.encryption.kms_key_self_link + kms_key_self_link = lookup( + local.ctx.kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) } } } @@ -117,7 +121,11 @@ resource "google_compute_instance_template" "default" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = var.encryption.kms_key_self_link + kms_key_self_link = lookup( + local.ctx.kms_keys, + var.encryption.kms_key_self_link, + var.encryption.kms_key_self_link + ) } } } @@ -127,15 +135,27 @@ resource "google_compute_instance_template" "default" { for_each = var.network_interfaces iterator = config content { - network = config.value.network - subnetwork = config.value.subnetwork - network_ip = try(config.value.addresses.internal, null) + network = lookup( + local.ctx.networks, config.value.network, config.value.network + ) + subnetwork = lookup( + local.ctx.subnets, config.value.subnetwork, config.value.subnetwork + ) + network_ip = try( + local.ctx.addresses[config.value.addresses.internal], + config.value.addresses.internal, + null + ) nic_type = config.value.nic_type stack_type = config.value.stack_type dynamic "access_config" { for_each = config.value.nat ? [""] : [] content { - nat_ip = try(config.value.addresses.external, null) + nat_ip = try( + local.ctx.addresses[config.value.addresses.external], + config.value.addresses.external, + null + ) } } dynamic "alias_ip_range" { @@ -221,7 +241,7 @@ 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 + project = local.project_id region = local.region name_prefix = "${var.name}-" description = var.description @@ -266,7 +286,10 @@ resource "google_compute_region_instance_template" "default" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = var.encryption.kms_key_self_link + kms_key_self_link = try( + local.ctx.kms_keys[var.encryption.kms_key_self_link], + var.encryption.kms_key_self_link + ) } } } @@ -316,7 +339,10 @@ resource "google_compute_region_instance_template" "default" { dynamic "disk_encryption_key" { for_each = var.encryption != null ? [""] : [] content { - kms_key_self_link = var.encryption.kms_key_self_link + kms_key_self_link = try( + local.ctx.kms_keys[var.encryption.kms_key_self_link], + var.encryption.kms_key_self_link + ) } } } @@ -326,15 +352,27 @@ resource "google_compute_region_instance_template" "default" { for_each = var.network_interfaces iterator = config content { - network = config.value.network - subnetwork = config.value.subnetwork - network_ip = try(config.value.addresses.internal, null) + network = lookup( + local.ctx.networks, config.value.network, config.value.network + ) + subnetwork = lookup( + local.ctx.subnets, config.value.subnetwork, config.value.subnetwork + ) + network_ip = try( + local.ctx.addresses[config.value.addresses.internal], + config.value.addresses.internal, + null + ) nic_type = config.value.nic_type stack_type = config.value.stack_type dynamic "access_config" { for_each = config.value.nat ? [""] : [] content { - nat_ip = try(config.value.addresses.external, null) + nat_ip = try( + local.ctx.addresses[config.value.addresses.external], + config.value.addresses.external, + null + ) } } dynamic "alias_ip_range" { diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf index 2d502af73..6e6dfa9c9 100644 --- a/modules/compute-vm/variables.tf +++ b/modules/compute-vm/variables.tf @@ -122,6 +122,23 @@ variable "confidential_compute" { default = false } +variable "context" { + description = "Context-specific interpolations." + type = object({ + addresses = optional(map(string), {}) + custom_roles = optional(map(string), {}) + kms_keys = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + tag_values = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "create_template" { description = "Create instance template instead of instances. Defaults to a global template." type = object({ diff --git a/tests/modules/compute_vm/context-template-regional.tfvars b/tests/modules/compute_vm/context-template-regional.tfvars new file mode 100644 index 000000000..fc11cce4f --- /dev/null +++ b/tests/modules/compute_vm/context-template-regional.tfvars @@ -0,0 +1,62 @@ +attached_disks = [{ + name = "data-0" + size = 10 + } +] +context = { + addresses = { + ext-test-0 = "35.10.10.10" + int-test-0 = "10.0.0.10" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8a = "europe-west8-a" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } + tag_values = { + "test/one" = "tagValues/1234567890" + } +} +create_template = { + regional = true +} +encryption = { + encrypt_boot = true + kms_key_self_link = "$kms_keys:test" +} +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +name = "test" +network_interfaces = [{ + network = "$networks:test" + subnetwork = "$subnets:test" + nat = true + addresses = { + external = "$addresses:ext-test-0" + internal = "$addresses:int-test-0" + } +}] +project_id = "$project_ids:test" +tag_bindings = { + foo = "$tag_values:test/one" +} +zone = "$locations:ew8a" diff --git a/tests/modules/compute_vm/context-template-regional.yaml b/tests/modules/compute_vm/context-template-regional.yaml new file mode 100644 index 000000000..073ca8c5c --- /dev/null +++ b/tests/modules/compute_vm/context-template-regional.yaml @@ -0,0 +1,122 @@ +# 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: + google_compute_instance_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + instance_name: test + members: + - group:test-group@example.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + zone: europe-west8-a + google_compute_region_instance_template.default[0]: + advanced_machine_features: [] + can_ip_forward: false + description: Managed by the compute-vm Terraform module. + disk: + - auto_delete: true + boot: true + disk_encryption_key: + - kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + kms_key_service_account: null + disk_name: null + disk_size_gb: 10 + disk_type: pd-balanced + guest_os_features: null + labels: null + resource_manager_tags: null + resource_policies: null + source: null + source_image: projects/debian-cloud/global/images/family/debian-11 + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + - auto_delete: true + device_name: data-0 + disk_encryption_key: + - kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + kms_key_service_account: null + disk_name: data-0 + disk_size_gb: 10 + disk_type: pd-balanced + guest_os_features: null + labels: null + mode: READ_WRITE + resource_manager_tags: null + resource_policies: null + source: null + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + type: PERSISTENT + effective_labels: + goog-terraform-provisioned: 'true' + enable_display: null + guest_accelerator: [] + instance_description: null + key_revocation_action_type: null + labels: null + machine_type: f1-micro + metadata: null + metadata_startup_script: null + min_cpu_platform: null + name_prefix: test- + network_interface: + - access_config: + - nat_ip: 35.10.10.10 + alias_ip_range: [] + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + network_ip: 10.0.0.10 + nic_type: null + queue_count: null + subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce + network_performance_config: [] + partner_metadata: null + project: foo-test-0 + region: europe-west8 + reservation_affinity: [] + resource_manager_tags: null + resource_policies: null + scheduling: + - automatic_restart: true + availability_domain: null + graceful_shutdown: [] + host_error_timeout_seconds: null + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + on_instance_stop_action: [] + preemptible: false + provisioning_model: STANDARD + termination_time: null + service_account: + - scopes: + - https://www.googleapis.com/auth/devstorage.read_only + - https://www.googleapis.com/auth/logging.write + - https://www.googleapis.com/auth/monitoring.write + shielded_instance_config: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null +counts: + google_compute_instance_iam_binding: 1 + google_compute_region_instance_template: 1 + modules: 0 + resources: 2 diff --git a/tests/modules/compute_vm/context-template.tfvars b/tests/modules/compute_vm/context-template.tfvars new file mode 100644 index 000000000..3ee07484a --- /dev/null +++ b/tests/modules/compute_vm/context-template.tfvars @@ -0,0 +1,60 @@ +attached_disks = [{ + name = "data-0" + size = 10 + } +] +context = { + addresses = { + ext-test-0 = "35.10.10.10" + int-test-0 = "10.0.0.10" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8a = "europe-west8-a" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } + tag_values = { + "test/one" = "tagValues/1234567890" + } +} +create_template = {} +encryption = { + encrypt_boot = true + kms_key_self_link = "$kms_keys:test" +} +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +name = "test" +network_interfaces = [{ + network = "$networks:test" + subnetwork = "$subnets:test" + nat = true + addresses = { + external = "$addresses:ext-test-0" + internal = "$addresses:int-test-0" + } +}] +project_id = "$project_ids:test" +tag_bindings = { + foo = "$tag_values:test/one" +} +zone = "$locations:ew8a" diff --git a/tests/modules/compute_vm/context-template.yaml b/tests/modules/compute_vm/context-template.yaml new file mode 100644 index 000000000..edd9a7003 --- /dev/null +++ b/tests/modules/compute_vm/context-template.yaml @@ -0,0 +1,123 @@ +# 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: + google_compute_instance_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + instance_name: test + members: + - group:test-group@example.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + zone: europe-west8-a + google_compute_instance_template.default[0]: + advanced_machine_features: [] + can_ip_forward: false + description: Managed by the compute-vm Terraform module. + disk: + - auto_delete: true + boot: true + disk_encryption_key: + - kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + kms_key_service_account: null + disk_name: null + disk_size_gb: 10 + disk_type: pd-balanced + guest_os_features: null + labels: null + resource_manager_tags: null + resource_policies: null + source: null + source_image: projects/debian-cloud/global/images/family/debian-11 + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + - auto_delete: true + device_name: data-0 + disk_encryption_key: + - kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + kms_key_service_account: null + disk_name: data-0 + disk_size_gb: 10 + disk_type: pd-balanced + guest_os_features: null + labels: null + mode: READ_WRITE + resource_manager_tags: null + resource_policies: null + source: null + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + type: PERSISTENT + effective_labels: + goog-terraform-provisioned: 'true' + enable_display: null + guest_accelerator: [] + instance_description: null + key_revocation_action_type: null + labels: null + machine_type: f1-micro + metadata: null + metadata_startup_script: null + min_cpu_platform: null + name_prefix: test- + network_interface: + - access_config: + - nat_ip: 35.10.10.10 + alias_ip_range: [] + ipv6_access_config: [] + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + network_ip: 10.0.0.10 + nic_type: null + queue_count: null + subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce + network_performance_config: [] + partner_metadata: null + project: foo-test-0 + region: europe-west8 + reservation_affinity: [] + resource_manager_tags: null + resource_policies: null + scheduling: + - automatic_restart: true + availability_domain: null + graceful_shutdown: [] + host_error_timeout_seconds: null + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + on_instance_stop_action: [] + preemptible: false + provisioning_model: STANDARD + termination_time: null + service_account: + - scopes: + - https://www.googleapis.com/auth/devstorage.read_only + - https://www.googleapis.com/auth/logging.write + - https://www.googleapis.com/auth/monitoring.write + shielded_instance_config: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null +counts: + google_compute_instance_iam_binding: 1 + google_compute_instance_template: 1 + modules: 0 + resources: 2 diff --git a/tests/modules/compute_vm/context-vm.tfvars b/tests/modules/compute_vm/context-vm.tfvars new file mode 100644 index 000000000..91295c4d8 --- /dev/null +++ b/tests/modules/compute_vm/context-vm.tfvars @@ -0,0 +1,59 @@ +attached_disks = [{ + name = "data-0" + size = 10 + } +] +context = { + addresses = { + ext-test-0 = "35.10.10.10" + int-test-0 = "10.0.0.10" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8a = "europe-west8-a" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } + tag_values = { + "test/one" = "tagValues/1234567890" + } +} +encryption = { + encrypt_boot = true + kms_key_self_link = "$kms_keys:test" +} +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +name = "test" +network_interfaces = [{ + network = "$networks:test" + subnetwork = "$subnets:test" + nat = true + addresses = { + external = "$addresses:ext-test-0" + internal = "$addresses:int-test-0" + } +}] +project_id = "$project_ids:test" +tag_bindings = { + foo = "$tag_values:test/one" +} +zone = "$locations:ew8a" diff --git a/tests/modules/compute_vm/context-vm.yaml b/tests/modules/compute_vm/context-vm.yaml new file mode 100644 index 000000000..252e886cf --- /dev/null +++ b/tests/modules/compute_vm/context-vm.yaml @@ -0,0 +1,164 @@ +# 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: + google_compute_disk.disks["data-0"]: + architecture: null + async_primary_disk: [] + create_snapshot_before_destroy: false + create_snapshot_before_destroy_prefix: null + description: null + disk_encryption_key: + - kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + kms_key_service_account: null + raw_key: null + rsa_encrypted_key: null + effective_labels: + disk_name: data-0 + disk_type: pd-balanced + goog-terraform-provisioned: 'true' + image: null + labels: + disk_name: data-0 + disk_type: pd-balanced + name: test-data-0 + params: [] + project: foo-test-0 + size: 10 + snapshot: null + source_disk: null + source_image_encryption_key: [] + source_instant_snapshot: null + source_snapshot_encryption_key: [] + source_storage_object: null + storage_pool: null + terraform_labels: + disk_name: data-0 + disk_type: pd-balanced + goog-terraform-provisioned: 'true' + timeouts: null + type: pd-balanced + zone: europe-west8-a + google_compute_instance.default[0]: + advanced_machine_features: [] + allow_stopping_for_update: true + attached_disk: + - device_name: data-0 + disk_encryption_key_raw: null + disk_encryption_key_rsa: null + disk_encryption_service_account: null + force_attach: null + mode: READ_WRITE + source: test-data-0 + boot_disk: + - auto_delete: true + disk_encryption_key_raw: null + disk_encryption_key_rsa: null + disk_encryption_service_account: null + force_attach: null + initialize_params: + - enable_confidential_compute: null + image: projects/debian-cloud/global/images/family/debian-11 + resource_manager_tags: null + size: 10 + source_image_encryption_key: [] + source_snapshot_encryption_key: [] + storage_pool: null + type: pd-balanced + interface: null + kms_key_self_link: projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute + mode: READ_WRITE + can_ip_forward: false + deletion_protection: false + description: Managed by the compute-vm Terraform module. + desired_status: null + effective_labels: + goog-terraform-provisioned: 'true' + enable_display: false + hostname: null + instance_encryption_key: [] + key_revocation_action_type: null + labels: null + machine_type: f1-micro + metadata: null + metadata_startup_script: null + name: test + network_interface: + - access_config: + - nat_ip: 35.10.10.10 + public_ptr_domain_name: null + alias_ip_range: [] + ipv6_access_config: [] + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + network_ip: 10.0.0.10 + nic_type: null + queue_count: null + security_policy: null + subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce + network_performance_config: [] + params: [] + partner_metadata: null + project: foo-test-0 + resource_policies: null + scheduling: + - automatic_restart: true + availability_domain: null + graceful_shutdown: [] + host_error_timeout_seconds: null + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + on_instance_stop_action: [] + preemptible: false + provisioning_model: STANDARD + termination_time: null + scratch_disk: [] + service_account: + - scopes: + - https://www.googleapis.com/auth/devstorage.read_only + - https://www.googleapis.com/auth/logging.write + - https://www.googleapis.com/auth/monitoring.write + shielded_instance_config: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + zone: europe-west8-a + google_compute_instance_iam_binding.default["$custom_roles:myrole_one"]: + condition: [] + instance_name: test + members: + - group:test-group@example.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + zone: europe-west8-a + google_tags_location_tag_binding.disks["data-0/foo"]: + location: europe-west8-a + tag_value: tagValues/1234567890 + timeouts: null + google_tags_location_tag_binding.instance["foo"]: + location: europe-west8-a + tag_value: tagValues/1234567890 + timeouts: null +counts: + google_compute_disk: 1 + google_compute_instance: 1 + google_compute_instance_iam_binding: 1 + google_tags_location_tag_binding: 2 + modules: 0 + resources: 5 diff --git a/tests/modules/compute_vm/tftest.yaml b/tests/modules/compute_vm/tftest.yaml new file mode 100644 index 000000000..61f5f1dd4 --- /dev/null +++ b/tests/modules/compute_vm/tftest.yaml @@ -0,0 +1,19 @@ +# 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. + +module: modules/compute-vm +tests: + context-template: + context-template-regional: + context-vm: From 23dd44ce62287ff5d8e6a261bacf63938f37c19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 13 Oct 2025 15:01:57 +0000 Subject: [PATCH 02/13] remove tf version from matrix, to keep workflow names stable across upgrades --- .github/workflows/tests.yml | 83 ++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1aaa749de..a18d5ae76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,40 +41,36 @@ env: DEFAULT_TOFU_VERSION: "1.10.0" jobs: - compute-matrix: - runs-on: ubuntu-latest - outputs: - DEFAULT_TERRAFORM_FLAVOUR: ${{ env.DEFAULT_TERRAFORM_FLAVOUR }} - DEFAULT_TERRAFORM_VERSION: ${{ env.DEFAULT_TERRAFORM_VERSION }} - DEFAULT_TOFU_VERSION: ${{ env.DEFAULT_TOFU_VERSION }} - steps: - - name: dummy - run: echo - setup-tf-providers: runs-on: ubuntu-latest - needs: - - compute-matrix strategy: matrix: include: - flavour: terraform - version: "${{ needs.compute-matrix.outputs.DEFAULT_TERRAFORM_VERSION }}" - flavour: tofu - version: "${{ needs.compute-matrix.outputs.DEFAULT_TOFU_VERSION }}" steps: - uses: actions/checkout@v4 + - name: Set Terraform versions + run: | + set -e -o xtrace + if [[ ${{ matrix.flavour }} == 'terraform' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TERRAFORM_VERSION }} | tee -a ${GITHUB_ENV} + elif [[ ${{ matrix.flavour }} == 'tofu' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TOFU_VERSION }} | tee -a ${GITHUB_ENV} + else + echo TERRAFORM_VERSION=unkown_flavor | tee -a ${GITHUB_ENV} + fi - uses: hashicorp/setup-terraform@v3 if: ${{ matrix.flavour == 'terraform' }} with: - terraform_version: ${{ matrix.version }} + terraform_version: ${{ env.TERRAFORM_VERSION }} terraform_wrapper: false - uses: opentofu/setup-opentofu@v1 if: ${{ matrix.flavour == 'tofu' }} with: - tofu_version: ${{ matrix.version }} + tofu_version: ${{ env.TERRAFORM_VERSION }} tofu_wrapper: false - name: Build lockfile and fetch providers @@ -86,7 +82,7 @@ jobs: sed -i -e 's/>=\(.*# tftest\)/=\1/g' tools/lockfile/versions.tf tools/lockfile/versions.tofu # change terraform version to the one that is running - sed -i 's/required_version = .*$/required_version = ">= ${{ matrix.version }}"/g' tools/lockfile/versions.tf + sed -i 's/required_version = .*$/required_version = ">= ${{ env.TERRAFORM_VERSION }}"/g' tools/lockfile/versions.tf cd tools/lockfile ${{ matrix.flavour }} init -upgrade=true @@ -95,12 +91,12 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.TF_PLUGIN_CACHE_DIR }} - key: ${{ runner.os }}-${{ matrix.flavour }}-${{ matrix.version }}-${{ hashFiles('tools/lockfile/.terraform.lock.hcl') }} + key: ${{ runner.os }}-${{ matrix.flavour }}-${{ env.TERRAFORM_VERSION }}-${{ hashFiles('tools/lockfile/.terraform.lock.hcl') }} - name: Upload lockfile uses: actions/upload-artifact@v4 with: - name: lockfile-${{ runner.os }}-${{ matrix.flavour }}-${{ matrix.version }} + name: lockfile-${{ runner.os }}-${{ matrix.flavour }}-${{ env.TERRAFORM_VERSION }} path: tools/lockfile/.terraform.lock.hcl overwrite: true include-hidden-files: true @@ -131,23 +127,30 @@ jobs: runs-on: ubuntu-latest needs: - setup-tf-providers - - compute-matrix strategy: matrix: include: - flavour: terraform - version: "${{ needs.compute-matrix.outputs.DEFAULT_TERRAFORM_VERSION }}" - flavour: tofu - version: "${{ needs.compute-matrix.outputs.DEFAULT_TOFU_VERSION }}" steps: - uses: actions/checkout@v4 + - name: Set Terraform versions + run: | + set -e -o xtrace + if [[ ${{ matrix.flavour }} == 'terraform' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TERRAFORM_VERSION }} | tee -a ${GITHUB_ENV} + elif [[ ${{ matrix.flavour }} == 'tofu' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TOFU_VERSION }} | tee -a ${GITHUB_ENV} + else + echo TERRAFORM_VERSION=unkown_flavor | tee -a ${GITHUB_ENV} + fi - name: Call composite action fabric-tests uses: ./.github/actions/fabric-tests with: PYTHON_VERSION: ${{ env.PYTHON_VERSION }} - TERRAFORM_VERSION: ${{ matrix.version }} + TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }} TERRAFORM_FLAVOUR: ${{ matrix.flavour }} - name: Run tests on documentation examples @@ -165,17 +168,24 @@ jobs: runs-on: ubuntu-latest needs: - setup-tf-providers - - compute-matrix strategy: matrix: include: - flavour: terraform - version: "${{ needs.compute-matrix.outputs.DEFAULT_TERRAFORM_VERSION }}" - flavour: tofu - version: "${{ needs.compute-matrix.outputs.DEFAULT_TOFU_VERSION }}" steps: - uses: actions/checkout@v4 + - name: Set Terraform versions + run: | + set -e -o xtrace + if [[ ${{ matrix.flavour }} == 'terraform' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TERRAFORM_VERSION }} | tee -a ${GITHUB_ENV} + elif [[ ${{ matrix.flavour }} == 'tofu' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TOFU_VERSION }} | tee -a ${GITHUB_ENV} + else + echo TERRAFORM_VERSION=unkown_flavor | tee -a ${GITHUB_ENV} + fi - name: Call composite action fabric-tests uses: ./.github/actions/fabric-tests @@ -183,7 +193,7 @@ jobs: TERRAFORM: ${{ matrix.flavour }} with: PYTHON_VERSION: ${{ env.PYTHON_VERSION }} - TERRAFORM_VERSION: ${{ matrix.version }} + TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }} TERRAFORM_FLAVOUR: ${{ matrix.flavour }} - name: Run tests modules @@ -200,15 +210,30 @@ jobs: fast: runs-on: ubuntu-latest needs: setup-tf-providers + strategy: + matrix: + include: + - flavour: terraform + # - flavour: tofu # tofu fails to find the terraform binary for FAST tests steps: - uses: actions/checkout@v4 + - name: Set Terraform versions + run: | + set -e -o xtrace + if [[ ${{ matrix.flavour }} == 'terraform' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TERRAFORM_VERSION }} | tee -a ${GITHUB_ENV} + elif [[ ${{ matrix.flavour }} == 'tofu' ]] ; then + echo TERRAFORM_VERSION=${{ env.DEFAULT_TOFU_VERSION }} | tee -a ${GITHUB_ENV} + else + echo TERRAFORM_VERSION=unkown_flavor | tee -a ${GITHUB_ENV} + fi - name: Call composite action fabric-tests uses: ./.github/actions/fabric-tests with: PYTHON_VERSION: ${{ env.PYTHON_VERSION }} - TERRAFORM_VERSION: ${{ env.DEFAULT_TERRAFORM_VERSION }} - TERRAFORM_FLAVOUR: ${{ env.DEFAULT_TERRAFORM_FLAVOUR }} + TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }} + TERRAFORM_FLAVOUR: ${{ matrix.flavour }} - name: Run tests on FAST stages run: pytest -vv -n4 --tb=line --junit-xml=test-results-raw.xml tests/fast From ad9d52a7da9e6d6c189e2d0a36cf090e46e0f7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 Oct 2025 09:53:47 +0000 Subject: [PATCH 03/13] Use context prefixes for auto service grants --- tests/fixtures/shared-vpc.tf | 2 +- ...service-vpc-access-connector-create-sharedvpc.yaml | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/shared-vpc.tf b/tests/fixtures/shared-vpc.tf index 2fc18972d..ae995b372 100644 --- a/tests/fixtures/shared-vpc.tf +++ b/tests/fixtures/shared-vpc.tf @@ -56,7 +56,7 @@ module "project-service" { shared_vpc_service_config = { host_project = module.project-host.project_id # reuse the list of services from the module's outputs - service_iam_grants = module.project-service.services + service_iam_grants = [for service in module.project-service.services : "$service_agents:${service}"] } } diff --git a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml index 480c4433a..16efec979 100644 --- a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml +++ b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml @@ -39,11 +39,20 @@ values: subnet: - name: fixture-subnet-28 project_id: test-host + # those IAM grants from shared-vpc.tf fixture are necessary for successful connector deployment + module.project-service.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:cloudservices"]: + condition: [] + project: test-host + role: roles/compute.networkUser + module.project-service.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:vpcaccess"]: + condition: [] + project: test-host + role: roles/compute.networkUser counts: google_cloud_run_v2_service: 1 google_vpc_access_connector: 1 modules: 4 - resources: 55 + resources: 59 outputs: {} From b10d31ef9868ca4f534307082f7310acb112284c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 Oct 2025 09:54:09 +0000 Subject: [PATCH 04/13] Add unique suffix to custom role --- modules/organization/README.md | 4 ++-- tests/modules/organization/examples/roles.yaml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/organization/README.md b/modules/organization/README.md index cb44aec9f..90159be08 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -384,12 +384,12 @@ module "org" { source = "./fabric/modules/organization" organization_id = var.organization_id custom_roles = { - "myRole" = [ + "myRole${replace(var.prefix, "/[^a-zA-Z0-9_\\.]/", "")}" = [ "compute.instances.list", ] } iam = { - (module.org.custom_role_id.myRole) = ["group:${var.group_email}"] + (module.org.custom_role_id["myRole${replace(var.prefix, "/[^a-zA-Z0-9_\\.]/", "")}"]) = ["group:${var.group_email}"] } } # tftest modules=1 resources=2 inventory=roles.yaml e2e serial diff --git a/tests/modules/organization/examples/roles.yaml b/tests/modules/organization/examples/roles.yaml index a6ca7851b..c4c8ba5ed 100644 --- a/tests/modules/organization/examples/roles.yaml +++ b/tests/modules/organization/examples/roles.yaml @@ -12,21 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -values: - module.org.google_organization_iam_binding.authoritative["organizations/1122334455/roles/myRole"]: +values: + module.org.google_organization_iam_binding.authoritative["organizations/1122334455/roles/myRoletest"]: condition: [] members: - group:organization-admins@example.org org_id: '1122334455' - role: organizations/1122334455/roles/myRole - module.org.google_organization_iam_custom_role.roles["myRole"]: + role: organizations/1122334455/roles/myRoletest + module.org.google_organization_iam_custom_role.roles["myRoletest"]: description: Terraform-managed. org_id: '1122334455' permissions: - compute.instances.list - role_id: myRole + role_id: myRoletest stage: GA - title: Custom role myRole + title: Custom role myRoletest counts: google_organization_iam_binding: 1 From 6673cba7739b3b344b4086271b62ffd2118788ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 Oct 2025 10:01:22 +0000 Subject: [PATCH 05/13] Fix provider failure when appling example --- modules/gcs/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/gcs/README.md b/modules/gcs/README.md index 8c7d2104c..d787566bf 100644 --- a/modules/gcs/README.md +++ b/modules/gcs/README.md @@ -149,12 +149,11 @@ module "bucket-gcs-notification" { name = "my-bucket" location = "EU" notification_config = { - enabled = true - payload_format = "JSON_API_V1" - sa_email = module.project.service_agents.storage.email - topic_name = "gcs-notification-topic" - event_types = ["OBJECT_FINALIZE"] - custom_attributes = {} + enabled = true + payload_format = "JSON_API_V1" + sa_email = module.project.service_agents.storage.email + topic_name = "gcs-notification-topic" + event_types = ["OBJECT_FINALIZE"] } } # tftest e2e From 849e85643793a043beb0db9c293cc5ba46610ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 Oct 2025 10:02:20 +0000 Subject: [PATCH 06/13] Use email from variables in tests --- modules/project/README.md | 8 ++++---- tests/modules/project/examples/iam-authoritative.yaml | 2 +- tests/modules/project/examples/iam-bindings-additive.yaml | 2 +- tests/modules/project/examples/iam-bindings.yaml | 1 - tests/modules/project/examples/iam-group.yaml | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/project/README.md b/modules/project/README.md index 8bba98c4f..c0f773546 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -96,7 +96,7 @@ module "project" { my_role = "organizations/1234567890/roles/myRole" } iam_principals = { - org_admins = "group:gcp-organization-admins@example.com" + org_admins = "group:${var.group_email}" } } iam = { @@ -125,7 +125,7 @@ module "project" { my_role = "organizations/1234567890/roles/myRole" } iam_principals = { - org_admins = "group:gcp-organization-admins@example.com" + org_admins = "group:${var.group_email}" } } iam_by_principals = { @@ -161,7 +161,7 @@ module "project" { my_role = "organizations/1234567890/roles/myRole" } iam_principals = { - org_admins = "group:gcp-organization-admins@example.com" + org_admins = "group:${var.group_email}" } } iam_bindings = { @@ -205,7 +205,7 @@ module "project" { ] context = { iam_principals = { - org_admins = "group:gcp-organization-admins@example.com" + org_admins = "group:${var.group_email}" } } iam_bindings_additive = { diff --git a/tests/modules/project/examples/iam-authoritative.yaml b/tests/modules/project/examples/iam-authoritative.yaml index 0f7e9ec7e..608d5448e 100644 --- a/tests/modules/project/examples/iam-authoritative.yaml +++ b/tests/modules/project/examples/iam-authoritative.yaml @@ -31,7 +31,7 @@ values: module.project.google_project_iam_binding.authoritative["$custom_roles:my_role"]: condition: [] members: - - group:gcp-organization-admins@example.com + - group:organization-admins@example.org project: test-project role: organizations/1234567890/roles/myRole module.project.google_project_iam_binding.authoritative["roles/container.hostServiceAgentUser"]: diff --git a/tests/modules/project/examples/iam-bindings-additive.yaml b/tests/modules/project/examples/iam-bindings-additive.yaml index 05c06dca1..3753799de 100644 --- a/tests/modules/project/examples/iam-bindings-additive.yaml +++ b/tests/modules/project/examples/iam-bindings-additive.yaml @@ -35,7 +35,7 @@ values: role: roles/owner module.project.google_project_iam_member.bindings["org-admins-viewer"]: condition: [] - member: group:gcp-organization-admins@example.com + member: group:organization-admins@example.org project: test-project role: roles/viewer diff --git a/tests/modules/project/examples/iam-bindings.yaml b/tests/modules/project/examples/iam-bindings.yaml index c1dc8d53a..3a061bdc6 100644 --- a/tests/modules/project/examples/iam-bindings.yaml +++ b/tests/modules/project/examples/iam-bindings.yaml @@ -35,7 +35,6 @@ values: \ []\n).hasOnly([\n 'roles/compute.networkAdmin'\n])\n" title: delegated_network_user_one members: - - group:gcp-organization-admins@example.com - group:organization-admins@example.org project: test-project role: roles/resourcemanager.projectIamAdmin diff --git a/tests/modules/project/examples/iam-group.yaml b/tests/modules/project/examples/iam-group.yaml index c61c44fcb..6bd0f4352 100644 --- a/tests/modules/project/examples/iam-group.yaml +++ b/tests/modules/project/examples/iam-group.yaml @@ -46,7 +46,7 @@ values: module.project.google_project_iam_binding.authoritative["roles/owner"]: condition: [] members: - - group:gcp-organization-admins@example.com + - group:organization-admins@example.org project: test-project role: roles/owner From 9f2beb1ee6c517eb3ac05be5bcf0604788463047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 Oct 2025 16:23:52 +0000 Subject: [PATCH 07/13] Use fixture custom role for modules/project tests --- modules/project/README.md | 60 ++++++++++++----- tests/fixtures/organization-custom-role.tf | 20 ++++++ .../project/examples/custom-role-iam.yaml | 64 +++++++++++++++++++ .../project/examples/iam-authoritative.yaml | 9 +-- .../project/examples/iam-bindings.yaml | 6 +- tests/modules/project/examples/iam-group.yaml | 3 +- 6 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 tests/fixtures/organization-custom-role.tf create mode 100644 tests/modules/project/examples/custom-role-iam.yaml diff --git a/modules/project/README.md b/modules/project/README.md index c0f773546..21171b03a 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -39,6 +39,7 @@ This module implements the creation and management of one GCP project including - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) +- [Fixtures](#fixtures) ## Basic Project Creation @@ -77,10 +78,6 @@ IAM also supports variable interpolation for both roles and principals, via the The `iam` variable is based on role keys and is typically used for service accounts, or where member values can be dynamic and would create potential problems in the underlying `for_each` cycle. ```hcl -locals { - gke_service_account = "my_gke_service_account" -} - module "project" { source = "./fabric/modules/project" billing_account = var.billing_account_id @@ -93,22 +90,19 @@ module "project" { ] context = { custom_roles = { - my_role = "organizations/1234567890/roles/myRole" + my_role = google_organization_iam_custom_role.custom_role.id # or module.organization.custom_roles["my_role"].id } iam_principals = { org_admins = "group:${var.group_email}" } } iam = { - "roles/container.hostServiceAgentUser" = [ - "serviceAccount:${local.gke_service_account}" - ] "$custom_roles:my_role" = [ "$iam_principals:org_admins" ] } } -# tftest modules=1 resources=8 inventory=iam-authoritative.yaml +# tftest fixtures=fixtures/organization-custom-role.tf inventory=iam-authoritative.yaml e2e ``` The `iam_by_principals` variable uses [principals](https://cloud.google.com/iam/docs/principal-identifiers) as keys and is a convenient way to assign roles to humans following Google's best practices. The end result is readable code that also serves as documentation. @@ -122,7 +116,7 @@ module "project" { prefix = var.prefix context = { custom_roles = { - my_role = "organizations/1234567890/roles/myRole" + my_role = google_organization_iam_custom_role.custom_role.id # or module.organization.custom_roles["my_role"].id } iam_principals = { org_admins = "group:${var.group_email}" @@ -141,7 +135,7 @@ module "project" { ] } } -# tftest modules=1 resources=7 inventory=iam-group.yaml e2e +# tftest fixtures=fixtures/organization-custom-role.tf inventory=iam-group.yaml e2e ``` The `iam_bindings` variable behaves like a more verbose version of `iam`, and allows setting binding-level IAM conditions. @@ -157,8 +151,10 @@ module "project" { "stackdriver.googleapis.com" ] context = { - custom_roles = { - my_role = "organizations/1234567890/roles/myRole" + condition_vars = { + custom_roles = { + my_role = google_organization_iam_custom_role.custom_role.id # or module.organization.custom_roles["my_role"].id + } } iam_principals = { org_admins = "group:${var.group_email}" @@ -172,19 +168,19 @@ module "project" { ] role = "roles/resourcemanager.projectIamAdmin" condition = { - title = "delegated_network_user_one" + title = "delegated_custom_role" expression = <<-END api.getAttribute( 'iam.googleapis.com/modifiedGrantsByRole', [] ).hasOnly([ - 'roles/compute.networkAdmin' + '$${custom_roles.my_role}' ]) END } } } } -# tftest modules=1 resources=3 inventory=iam-bindings.yaml e2e +# tftest fixtures=fixtures/organization-custom-role.tf inventory=iam-bindings.yaml e2e ``` ### Additive IAM @@ -1247,11 +1243,37 @@ module "project" { "compute.instances.list", ] } + context = { + condition_vars = { + custom_roles = { + my_role = "organizations/1234567890/roles/myRole" + } + } + } iam = { (module.project.custom_role_id.myRole) = ["group:${var.group_email}"] } + iam_bindings = { + iam_admin_conditional = { + members = [ + "group:${var.group_email}", + "$iam_principals:org_admins" + ] + role = "roles/resourcemanager.projectIamAdmin" + condition = { + title = "delegated_custom_role" + expression = <<-END + api.getAttribute( + 'iam.googleapis.com/modifiedGrantsByRole', [] + ).hasOnly([ + '$${custom_roles.my_role}' + ]) + END + } + } + } } -# tftest modules=1 resources=3 +# tftest inventory=custom-role-iam.yaml ``` ### Custom Roles Factory @@ -1938,4 +1960,8 @@ alerts: | [sink_writer_identities](outputs.tf#L182) | Writer identities created for each sink. | | | [tag_keys](outputs.tf#L189) | Tag key resources. | | | [tag_values](outputs.tf#L198) | Tag value resources. | | + +## Fixtures + +- [organization-custom-role.tf](../../tests/fixtures/organization-custom-role.tf) diff --git a/tests/fixtures/organization-custom-role.tf b/tests/fixtures/organization-custom-role.tf new file mode 100644 index 000000000..f2beb11ab --- /dev/null +++ b/tests/fixtures/organization-custom-role.tf @@ -0,0 +1,20 @@ +# 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. + +resource "google_organization_iam_custom_role" "custom_role" { + role_id = "fixtureRole${replace(var.prefix, "/[^a-zA-Z0-9_\\.]/", "")}" + org_id = trimprefix(var.organization_id, "organizations/") + title = "fixtureRole" + permissions = ["compute.instances.list"] +} diff --git a/tests/modules/project/examples/custom-role-iam.yaml b/tests/modules/project/examples/custom-role-iam.yaml new file mode 100644 index 000000000..26b7645af --- /dev/null +++ b/tests/modules/project/examples/custom-role-iam.yaml @@ -0,0 +1,64 @@ +# 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.project.google_project.project[0]: + auto_create_network: false + billing_account: null + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' + folder_id: null + labels: null + name: project + org_id: null + project_id: project + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.project.google_project_iam_binding.authoritative["projects/project/roles/myRole"]: + condition: [] + members: + - group:organization-admins@example.org + project: project + role: projects/project/roles/myRole + module.project.google_project_iam_binding.bindings["iam_admin_conditional"]: + condition: + - description: null + expression: "api.getAttribute(\n 'iam.googleapis.com/modifiedGrantsByRole',\ + \ []\n).hasOnly([\n 'organizations/1234567890/roles/myRole'\n])\n" + title: delegated_custom_role + members: + - $iam_principals:org_admins + - group:organization-admins@example.org + project: project + role: roles/resourcemanager.projectIamAdmin + module.project.google_project_iam_custom_role.roles["myRole"]: + description: Terraform-managed. + permissions: + - compute.instances.list + project: project + role_id: myRole + stage: GA + title: Custom role myRole + +counts: + google_project: 1 + google_project_iam_binding: 2 + google_project_iam_custom_role: 1 + modules: 1 + resources: 4 + +outputs: {} diff --git a/tests/modules/project/examples/iam-authoritative.yaml b/tests/modules/project/examples/iam-authoritative.yaml index 608d5448e..b45207006 100644 --- a/tests/modules/project/examples/iam-authoritative.yaml +++ b/tests/modules/project/examples/iam-authoritative.yaml @@ -33,17 +33,10 @@ values: members: - group:organization-admins@example.org project: test-project - role: organizations/1234567890/roles/myRole - module.project.google_project_iam_binding.authoritative["roles/container.hostServiceAgentUser"]: - condition: [] - members: - - serviceAccount:my_gke_service_account - project: test-project - role: roles/container.hostServiceAgentUser counts: google_project: 1 - google_project_iam_binding: 2 + google_project_iam_binding: 1 google_project_iam_member: 2 google_project_service: 2 google_project_service_identity: 1 diff --git a/tests/modules/project/examples/iam-bindings.yaml b/tests/modules/project/examples/iam-bindings.yaml index 3a061bdc6..c924441a9 100644 --- a/tests/modules/project/examples/iam-bindings.yaml +++ b/tests/modules/project/examples/iam-bindings.yaml @@ -31,9 +31,7 @@ values: module.project.google_project_iam_binding.bindings["iam_admin_conditional"]: condition: - description: null - expression: "api.getAttribute(\n 'iam.googleapis.com/modifiedGrantsByRole',\ - \ []\n).hasOnly([\n 'roles/compute.networkAdmin'\n])\n" - title: delegated_network_user_one + title: delegated_custom_role members: - group:organization-admins@example.org project: test-project @@ -44,6 +42,6 @@ counts: google_project_iam_binding: 1 google_project_service: 1 modules: 1 - resources: 3 + resources: 4 outputs: {} diff --git a/tests/modules/project/examples/iam-group.yaml b/tests/modules/project/examples/iam-group.yaml index 6bd0f4352..a9c11b916 100644 --- a/tests/modules/project/examples/iam-group.yaml +++ b/tests/modules/project/examples/iam-group.yaml @@ -18,7 +18,6 @@ values: members: - group:organization-admins@example.org project: test-project - role: organizations/1234567890/roles/myRole module.project.google_project_iam_binding.authoritative["roles/cloudasset.owner"]: condition: [] members: @@ -54,4 +53,4 @@ counts: google_project: 1 google_project_iam_binding: 6 modules: 1 - resources: 7 + resources: 8 From 5bc9d831befc420e16330561399ca3de2d4f30cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Mon, 13 Oct 2025 08:03:00 +0000 Subject: [PATCH 08/13] Pass deletion_protection to secondary instance --- modules/alloydb/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/alloydb/main.tf b/modules/alloydb/main.tf index 1b04f6161..3de00cbb5 100644 --- a/modules/alloydb/main.tf +++ b/modules/alloydb/main.tf @@ -254,6 +254,7 @@ resource "google_alloydb_cluster" "secondary" { cluster_type = var.cross_region_replication.promote_secondary || var.cross_region_replication.switchover_mode ? "PRIMARY" : "SECONDARY" database_version = var.database_version deletion_policy = "FORCE" + deletion_protection = var.deletion_protection display_name = coalesce(var.cross_region_replication.secondary_cluster_display_name, local.secondary_cluster_name) labels = var.labels location = var.cross_region_replication.region From 6e02fd113607871e1b92034cae90cc1a8f3c0cad Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 14 Oct 2025 18:07:29 +0200 Subject: [PATCH 09/13] Add support for context to DNS modules (#3412) * dns-response-policy * add context to dns modules --- modules/dns-response-policy/README.md | 15 ++-- modules/dns-response-policy/main.tf | 13 +++- modules/dns-response-policy/variables.tf | 10 +++ modules/dns/README.md | 15 ++-- modules/dns/main.tf | 45 +++++++----- modules/dns/variables.tf | 12 ++++ tests/modules/dns/context.tfvars | 30 ++++++++ tests/modules/dns/context.yaml | 69 +++++++++++++++++++ tests/modules/dns/tftest.yaml | 17 +++++ .../dns_response_policy/context.tfvars | 23 +++++++ .../modules/dns_response_policy/context.yaml | 44 ++++++++++++ tests/modules/dns_response_policy/tftest.yaml | 17 +++++ 12 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 tests/modules/dns/context.tfvars create mode 100644 tests/modules/dns/context.yaml create mode 100644 tests/modules/dns/tftest.yaml create mode 100644 tests/modules/dns_response_policy/context.tfvars create mode 100644 tests/modules/dns_response_policy/context.yaml create mode 100644 tests/modules/dns_response_policy/tftest.yaml diff --git a/modules/dns-response-policy/README.md b/modules/dns-response-policy/README.md index 010c09c5a..0cce4c060 100644 --- a/modules/dns-response-policy/README.md +++ b/modules/dns-response-policy/README.md @@ -145,14 +145,15 @@ restricted: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L39) | Policy name. | string | ✓ | | -| [project_id](variables.tf#L58) | Project id for the zone. | string | ✓ | | +| [name](variables.tf#L49) | Policy name. | string | ✓ | | +| [project_id](variables.tf#L68) | Project id for the zone. | string | ✓ | | | [clusters](variables.tf#L17) | Map of GKE clusters to which this policy is applied in name => id format. | map(string) | | {} | -| [description](variables.tf#L24) | Policy description. | string | | "Terraform managed." | -| [factories_config](variables.tf#L30) | Path to folder containing rules data files for the optional factory. | object({…}) | | {} | -| [networks](variables.tf#L44) | Map of VPC self links to which this policy is applied in name => self link format. | map(string) | | {} | -| [policy_create](variables.tf#L51) | Set to false to use the existing policy matching name and only manage rules. | bool | | true | -| [rules](variables.tf#L63) | Map of policy rules in name => rule format. Local data takes precedence over behavior and is in the form record type => attributes. | map(object({…})) | | {} | +| [context](variables.tf#L24) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L34) | Policy description. | string | | "Terraform managed." | +| [factories_config](variables.tf#L40) | Path to folder containing rules data files for the optional factory. | object({…}) | | {} | +| [networks](variables.tf#L54) | Map of VPC self links to which this policy is applied in name => self link format. | map(string) | | {} | +| [policy_create](variables.tf#L61) | Set to false to use the existing policy matching name and only manage rules. | bool | | true | +| [rules](variables.tf#L73) | Map of policy rules in name => rule format. Local data takes precedence over behavior and is in the form record type => attributes. | map(object({…})) | | {} | ## Outputs diff --git a/modules/dns-response-policy/main.tf b/modules/dns-response-policy/main.tf index 0cee3ca14..9e2e06cc9 100644 --- a/modules/dns-response-policy/main.tf +++ b/modules/dns-response-policy/main.tf @@ -21,6 +21,12 @@ locals { : "{}" ) _factory_rules = yamldecode(local._factory_data) + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" factory_rules = { for k, v in local._factory_rules : k => { dns_name = v.dns_name @@ -36,18 +42,19 @@ locals { ? google_dns_response_policy.default[0].response_policy_name : var.name ) + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) } resource "google_dns_response_policy" "default" { provider = google-beta count = var.policy_create ? 1 : 0 - project = var.project_id + project = local.project_id description = var.description response_policy_name = var.name dynamic "networks" { for_each = var.networks content { - network_url = networks.value + network_url = lookup(local.ctx.networks, networks.value, networks.value) } } dynamic "gke_clusters" { @@ -61,7 +68,7 @@ resource "google_dns_response_policy" "default" { resource "google_dns_response_policy_rule" "default" { provider = google-beta for_each = merge(local.factory_rules, var.rules) - project = var.project_id + project = local.project_id response_policy = local.policy_name rule_name = each.key dns_name = each.value.dns_name diff --git a/modules/dns-response-policy/variables.tf b/modules/dns-response-policy/variables.tf index 1c39260a9..01392e4ff 100644 --- a/modules/dns-response-policy/variables.tf +++ b/modules/dns-response-policy/variables.tf @@ -21,6 +21,16 @@ variable "clusters" { nullable = false } +variable "context" { + description = "Context-specific interpolations." + type = object({ + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "description" { description = "Policy description." type = string diff --git a/modules/dns/README.md b/modules/dns/README.md index fe2fdb90c..c1a68f4b2 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -190,13 +190,14 @@ module "public-dns" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L35) | Zone name, must be unique within the project. | string | ✓ | | -| [project_id](variables.tf#L40) | Project id for the zone. | string | ✓ | | -| [description](variables.tf#L17) | Domain description. | string | | "Terraform managed." | -| [force_destroy](variables.tf#L23) | Set this to true to delete all records in the zone upon zone destruction. | bool | | null | -| [iam](variables.tf#L29) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | null | -| [recordsets](variables.tf#L45) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | -| [zone_config](variables.tf#L102) | DNS zone configuration. | object({…}) | | null | +| [name](variables.tf#L47) | Zone name, must be unique within the project. | string | ✓ | | +| [project_id](variables.tf#L52) | Project id for the zone. | string | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L29) | Domain description. | string | | "Terraform managed." | +| [force_destroy](variables.tf#L35) | Set this to true to delete all records in the zone upon zone destruction. | bool | | null | +| [iam](variables.tf#L41) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | null | +| [recordsets](variables.tf#L57) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | +| [zone_config](variables.tf#L114) | DNS zone configuration. | object({…}) | | null | ## Outputs diff --git a/modules/dns/main.tf b/modules/dns/main.tf index a13ea40f1..ae8c21678 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -15,15 +15,27 @@ */ locals { - managed_zone = (var.zone_config == null ? - data.google_dns_managed_zone.dns_managed_zone[0] - : google_dns_managed_zone.dns_managed_zone[0] - ) # split record name and type and set as keys in a map _recordsets_0 = { for key, attrs in var.recordsets : key => merge(attrs, zipmap(["type", "name"], split(" ", key))) } + client_networks = concat( + coalesce(try(var.zone_config.forwarding.client_networks, null), []), + coalesce(try(var.zone_config.peering.client_networks, null), []), + coalesce(try(var.zone_config.private.client_networks, null), []) + ) + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" + managed_zone = (var.zone_config == null ? + data.google_dns_managed_zone.dns_managed_zone[0] + : google_dns_managed_zone.dns_managed_zone[0] + ) + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) # compute the final resource name for the recordset recordsets = { for key, attrs in local._recordsets_0 : @@ -39,11 +51,6 @@ locals { ) }) } - client_networks = concat( - coalesce(try(var.zone_config.forwarding.client_networks, null), []), - coalesce(try(var.zone_config.peering.client_networks, null), []), - coalesce(try(var.zone_config.private.client_networks, null), []) - ) visibility = (var.zone_config == null ? null : (var.zone_config.forwarding != null || @@ -57,7 +64,7 @@ locals { resource "google_dns_managed_zone" "dns_managed_zone" { count = (var.zone_config == null) ? 0 : 1 provider = google-beta - project = var.project_id + project = local.project_id name = var.name dns_name = var.zone_config.domain description = var.description @@ -122,7 +129,9 @@ resource "google_dns_managed_zone" "dns_managed_zone" { for_each = local.client_networks iterator = network content { - network_url = network.value + network_url = lookup( + local.ctx.networks, network.value, network.value + ) } } } @@ -146,22 +155,24 @@ resource "google_dns_managed_zone" "dns_managed_zone" { data "google_dns_managed_zone" "dns_managed_zone" { count = var.zone_config == null ? 1 : 0 - project = var.project_id + project = local.project_id name = var.name } resource "google_dns_managed_zone_iam_binding" "iam_bindings" { for_each = coalesce(var.iam, {}) - project = var.project_id + project = local.project_id managed_zone = local.managed_zone.id - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [ + for m in each.value : lookup(local.ctx.iam_principals, m, m) + ] } data "google_dns_keys" "dns_keys" { count = try(var.zone_config.public.dnssec_config.state, "off") != "off" ? 1 : 0 managed_zone = local.managed_zone.id - project = var.project_id + project = local.project_id depends_on = [ google_dns_managed_zone.dns_managed_zone ] @@ -169,7 +180,7 @@ data "google_dns_keys" "dns_keys" { resource "google_dns_record_set" "dns_record_set" { for_each = local.recordsets - project = var.project_id + project = local.project_id managed_zone = var.name name = each.value.resource_name type = each.value.type diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf index 4dc10e2c5..b80188608 100644 --- a/modules/dns/variables.tf +++ b/modules/dns/variables.tf @@ -14,6 +14,18 @@ * limitations under the License. */ +variable "context" { + description = "Context-specific interpolations." + type = object({ + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "description" { description = "Domain description." type = string diff --git a/tests/modules/dns/context.tfvars b/tests/modules/dns/context.tfvars new file mode 100644 index 000000000..26a3575b2 --- /dev/null +++ b/tests/modules/dns/context.tfvars @@ -0,0 +1,30 @@ +context = { + custom_roles = { + myrole = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } +} +project_id = "$project_ids:test" +name = "test-example" +zone_config = { + domain = "test.example." + private = { + client_networks = ["$networks:test"] + } +} +recordsets = { + "A localhost" = { records = ["127.0.0.1"] } + "A myhost" = { ttl = 600, records = ["10.0.0.120"] } +} +iam = { + "$custom_roles:myrole" = ["$iam_principals:mygroup"] +} + diff --git a/tests/modules/dns/context.yaml b/tests/modules/dns/context.yaml new file mode 100644 index 000000000..6c986b75b --- /dev/null +++ b/tests/modules/dns/context.yaml @@ -0,0 +1,69 @@ +# 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: + google_dns_managed_zone.dns_managed_zone[0]: + cloud_logging_config: + - enable_logging: false + description: Terraform managed. + dns_name: test.example. + effective_labels: + goog-terraform-provisioned: 'true' + force_destroy: false + forwarding_config: [] + labels: null + name: test-example + peering_config: [] + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + reverse_lookup: false + service_directory_config: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + visibility: private + google_dns_managed_zone_iam_binding.iam_bindings["$custom_roles:myrole"]: + condition: [] + members: + - group:test-group@example.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_dns_record_set.dns_record_set["A localhost"]: + managed_zone: test-example + name: localhost.test.example. + project: foo-test-0 + routing_policy: [] + rrdatas: + - 127.0.0.1 + ttl: 300 + type: A + google_dns_record_set.dns_record_set["A myhost"]: + managed_zone: test-example + name: myhost.test.example. + project: foo-test-0 + routing_policy: [] + rrdatas: + - 10.0.0.120 + ttl: 600 + type: A + +counts: + google_dns_managed_zone: 1 + google_dns_managed_zone_iam_binding: 1 + google_dns_record_set: 2 + modules: 0 + resources: 4 diff --git a/tests/modules/dns/tftest.yaml b/tests/modules/dns/tftest.yaml new file mode 100644 index 000000000..afeb43bda --- /dev/null +++ b/tests/modules/dns/tftest.yaml @@ -0,0 +1,17 @@ +# 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. + +module: modules/dns +tests: + context: diff --git a/tests/modules/dns_response_policy/context.tfvars b/tests/modules/dns_response_policy/context.tfvars new file mode 100644 index 000000000..d9440a11f --- /dev/null +++ b/tests/modules/dns_response_policy/context.tfvars @@ -0,0 +1,23 @@ +context = { + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } +} +project_id = "$project_ids:test" +name = "googleapis" +networks = { + landing = "$networks:test" +} +rules = { + pubsub = { + dns_name = "pubsub.googleapis.com." + local_data = { + A = { + rrdatas = ["199.36.153.4", "199.36.153.5"] + } + } + } +} diff --git a/tests/modules/dns_response_policy/context.yaml b/tests/modules/dns_response_policy/context.yaml new file mode 100644 index 000000000..44f708cc9 --- /dev/null +++ b/tests/modules/dns_response_policy/context.yaml @@ -0,0 +1,44 @@ +# 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: + google_dns_response_policy.default[0]: + description: Terraform managed. + gke_clusters: [] + networks: + - network_url: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + response_policy_name: googleapis + timeouts: null + google_dns_response_policy_rule.default["pubsub"]: + behavior: null + dns_name: pubsub.googleapis.com. + local_data: + - local_datas: + - name: pubsub.googleapis.com. + rrdatas: + - 199.36.153.4 + - 199.36.153.5 + ttl: null + type: A + project: foo-test-0 + response_policy: googleapis + rule_name: pubsub + timeouts: null + +counts: + google_dns_response_policy: 1 + google_dns_response_policy_rule: 1 + modules: 0 + resources: 2 diff --git a/tests/modules/dns_response_policy/tftest.yaml b/tests/modules/dns_response_policy/tftest.yaml new file mode 100644 index 000000000..185bfb83f --- /dev/null +++ b/tests/modules/dns_response_policy/tftest.yaml @@ -0,0 +1,17 @@ +# 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. + +module: modules/dns-response-policy +tests: + context: From 9c61b1c30c8fcc7c85f99645f0b50d31f2ea4648 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Wed, 15 Oct 2025 11:35:44 +0200 Subject: [PATCH 10/13] Make SSM gitignores a list (#3413) Fixes #3395 --- .../secure-source-manager-instance/README.md | 23 +++++++- .../variables.tf | 2 +- .../examples/initial-config.yaml | 52 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/modules/secure_source_manager_instance/examples/initial-config.yaml diff --git a/modules/secure-source-manager-instance/README.md b/modules/secure-source-manager-instance/README.md index 42f0e52d2..73b102fd6 100644 --- a/modules/secure-source-manager-instance/README.md +++ b/modules/secure-source-manager-instance/README.md @@ -11,6 +11,7 @@ This module allows to create a Secure Source Manager instance and repositories i - [Private instance](#private-instance) - [IAM](#iam) - [Branch Protection Rules](#branch-protection-rules) + - [Initial Configuration](#initial-configuration) - [Variables](#variables) - [Outputs](#outputs) @@ -195,6 +196,26 @@ module "ssm_instance" { } # tftest modules=1 resources=3 inventory=branch-protection-rules.yaml ``` + +### Initial Configuration + +```hcl +module "ssm_instance" { + source = "./fabric/modules/secure-source-manager-instance" + project_id = var.project_id + instance_id = "my-instance" + location = var.region + repositories = { + my-repository = { + initial_config = { + default_branch = "main" + gitignores = ["terraform.tfstate"] + } + } + } +} +# tftest inventory=initial-config.yaml +``` ## Variables @@ -203,7 +224,7 @@ module "ssm_instance" { | [instance_id](variables.tf#L23) | Instance ID. | string | ✓ | | | [location](variables.tf#L40) | Location. | string | ✓ | | | [project_id](variables.tf#L55) | Project ID. | string | ✓ | | -| [repositories](variables.tf#L60) | Repositories. | map(object({…})) | ✓ | | +| [repositories](variables.tf#L60) | Repositories. | map(object({…})) | ✓ | | | [iam](variables-iam.tf#L17) | IAM bindings. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L23) | IAM bindings. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L32) | IAM bindings. | map(object({…})) | | {} | diff --git a/modules/secure-source-manager-instance/variables.tf b/modules/secure-source-manager-instance/variables.tf index 8a2b097e3..49cd81393 100644 --- a/modules/secure-source-manager-instance/variables.tf +++ b/modules/secure-source-manager-instance/variables.tf @@ -72,7 +72,7 @@ variable "repositories" { })), {}) initial_config = optional(object({ default_branch = optional(string) - gitignores = optional(string) + gitignores = optional(list(string)) license = optional(string) readme = optional(string) })) diff --git a/tests/modules/secure_source_manager_instance/examples/initial-config.yaml b/tests/modules/secure_source_manager_instance/examples/initial-config.yaml new file mode 100644 index 000000000..5ed7b5961 --- /dev/null +++ b/tests/modules/secure_source_manager_instance/examples/initial-config.yaml @@ -0,0 +1,52 @@ +# 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.ssm_instance.google_secure_source_manager_instance.instance[0]: + deletion_policy: PREVENT + effective_labels: + goog-terraform-provisioned: 'true' + instance_id: my-instance + kms_key: null + labels: null + location: europe-west8 + private_config: + - ca_pool: null + is_private: true + project: project-id + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + workforce_identity_federation_config: [] + module.ssm_instance.google_secure_source_manager_repository.repositories["my-repository"]: + deletion_policy: PREVENT + description: null + initial_config: + - default_branch: main + gitignores: + - terraform.tfstate + license: null + readme: null + location: europe-west8 + project: project-id + repository_id: my-repository + timeouts: null + +counts: + google_secure_source_manager_instance: 1 + google_secure_source_manager_repository: 1 + modules: 1 + resources: 2 + +outputs: {} From ecdc248f3fcc98125537875c19098ebcdbd6aabd Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 15 Oct 2025 13:59:51 +0200 Subject: [PATCH 11/13] Add support for context to net-cloudnat, net-firewall-policy modules (#3414) * net-cloudnat * net firewall policy --- modules/net-cloudnat/README.md | 29 ++--- modules/net-cloudnat/main.tf | 42 ++++--- modules/net-cloudnat/variables.tf | 14 +++ modules/net-firewall-policy/README.md | 31 +++--- modules/net-firewall-policy/hierarchical.tf | 48 +++++--- modules/net-firewall-policy/main.tf | 6 + modules/net-firewall-policy/net-global.tf | 55 +++++++--- modules/net-firewall-policy/net-regional.tf | 59 ++++++---- modules/net-firewall-policy/variables.tf | 15 +++ tests/modules/net_cloudnat/context.tfvars | 28 +++++ tests/modules/net_cloudnat/context.yaml | 61 +++++++++++ tests/modules/net_cloudnat/tftest.yaml | 17 +++ .../net_firewall_policy/context-g.tfvars | 52 +++++++++ .../net_firewall_policy/context-g.yaml | 99 +++++++++++++++++ .../net_firewall_policy/context-h.tfvars | 49 +++++++++ .../net_firewall_policy/context-h.yaml | 92 ++++++++++++++++ .../net_firewall_policy/context-r.tfvars | 52 +++++++++ .../net_firewall_policy/context-r.yaml | 103 ++++++++++++++++++ tests/modules/net_firewall_policy/tftest.yaml | 19 ++++ 19 files changed, 774 insertions(+), 97 deletions(-) create mode 100644 tests/modules/net_cloudnat/context.tfvars create mode 100644 tests/modules/net_cloudnat/context.yaml create mode 100644 tests/modules/net_cloudnat/tftest.yaml create mode 100644 tests/modules/net_firewall_policy/context-g.tfvars create mode 100644 tests/modules/net_firewall_policy/context-g.yaml create mode 100644 tests/modules/net_firewall_policy/context-h.tfvars create mode 100644 tests/modules/net_firewall_policy/context-h.yaml create mode 100644 tests/modules/net_firewall_policy/context-r.tfvars create mode 100644 tests/modules/net_firewall_policy/context-r.yaml create mode 100644 tests/modules/net_firewall_policy/tftest.yaml diff --git a/modules/net-cloudnat/README.md b/modules/net-cloudnat/README.md index b8b8b614e..dd1e88922 100644 --- a/modules/net-cloudnat/README.md +++ b/modules/net-cloudnat/README.md @@ -189,21 +189,22 @@ module "nat" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L107) | Name of the Cloud NAT resource. | string | ✓ | | -| [project_id](variables.tf#L112) | Project where resources will be created. | string | ✓ | | -| [region](variables.tf#L117) | Region where resources will be created. | string | ✓ | | +| [name](variables.tf#L121) | Name of the Cloud NAT resource. | string | ✓ | | +| [project_id](variables.tf#L126) | Project where resources will be created. | string | ✓ | | +| [region](variables.tf#L131) | Region where resources will be created. | string | ✓ | | | [addresses](variables.tf#L17) | Optional list of external address self links. | list(string) | | [] | -| [config_port_allocation](variables.tf#L23) | Configuration for how to assign ports to virtual machines. min_ports_per_vm and max_ports_per_vm have no effect unless enable_dynamic_port_allocation is set to 'true'. | object({…}) | | {} | -| [config_source_subnetworks](variables.tf#L39) | Subnetwork configuration. | object({…}) | | {} | -| [config_timeouts](variables.tf#L69) | Timeout configurations. | object({…}) | | {} | -| [endpoint_types](variables.tf#L82) | Specifies the endpoint Types supported by the NAT Gateway. Supported values include: ENDPOINT_TYPE_VM, ENDPOINT_TYPE_SWG, ENDPOINT_TYPE_MANAGED_PROXY_LB. | list(string) | | null | -| [logging_filter](variables.tf#L101) | Enables logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string | | null | -| [router_asn](variables.tf#L122) | Router ASN used for auto-created router. | number | | null | -| [router_create](variables.tf#L128) | Create router. | bool | | true | -| [router_name](variables.tf#L134) | Router name, leave blank if router will be created to use auto generated name. | string | | null | -| [router_network](variables.tf#L140) | Name of the VPC used for auto-created router. | string | | null | -| [rules](variables.tf#L146) | List of rules associated with this NAT. | list(object({…})) | | [] | -| [type](variables.tf#L166) | Whether this Cloud NAT is used for public or private IP translation. One of 'PUBLIC' or 'PRIVATE'. | string | | "PUBLIC" | +| [config_port_allocation](variables.tf#L24) | Configuration for how to assign ports to virtual machines. min_ports_per_vm and max_ports_per_vm have no effect unless enable_dynamic_port_allocation is set to 'true'. | object({…}) | | {} | +| [config_source_subnetworks](variables.tf#L40) | Subnetwork configuration. | object({…}) | | {} | +| [config_timeouts](variables.tf#L70) | Timeout configurations. | object({…}) | | {} | +| [context](variables.tf#L83) | Context-specific interpolations. | object({…}) | | {} | +| [endpoint_types](variables.tf#L96) | Specifies the endpoint Types supported by the NAT Gateway. Supported values include: ENDPOINT_TYPE_VM, ENDPOINT_TYPE_SWG, ENDPOINT_TYPE_MANAGED_PROXY_LB. | list(string) | | null | +| [logging_filter](variables.tf#L115) | Enables logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string | | null | +| [router_asn](variables.tf#L136) | Router ASN used for auto-created router. | number | | null | +| [router_create](variables.tf#L142) | Create router. | bool | | true | +| [router_name](variables.tf#L148) | Router name, leave blank if router will be created to use auto generated name. | string | | null | +| [router_network](variables.tf#L154) | Name of the VPC used for auto-created router. | string | | null | +| [rules](variables.tf#L160) | List of rules associated with this NAT. | list(object({…})) | | [] | +| [type](variables.tf#L180) | Whether this Cloud NAT is used for public or private IP translation. One of 'PUBLIC' or 'PRIVATE'. | string | | "PUBLIC" | ## Outputs diff --git a/modules/net-cloudnat/main.tf b/modules/net-cloudnat/main.tf index 2c6d8eb67..60a4f72d0 100644 --- a/modules/net-cloudnat/main.tf +++ b/modules/net-cloudnat/main.tf @@ -15,11 +15,19 @@ */ locals { + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" router_name = ( var.router_create ? try(google_compute_router.router[0].name, null) : var.router_name ) + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) + region = lookup(local.ctx.locations, var.region, var.region) subnet_config = ( var.config_source_subnetworks.all != true ? "LIST_OF_SUBNETWORKS" @@ -34,10 +42,9 @@ locals { resource "google_compute_router" "router" { count = var.router_create ? 1 : 0 name = var.router_name == null ? "${var.name}-nat" : var.router_name - project = var.project_id - region = var.region - network = var.router_network - + project = local.project_id + region = local.region + network = lookup(local.ctx.networks, var.router_network, var.router_network) dynamic "bgp" { for_each = var.router_asn == null ? [] : [1] content { @@ -48,13 +55,15 @@ resource "google_compute_router" "router" { resource "google_compute_router_nat" "nat" { provider = google-beta - project = var.project_id - region = var.region + project = local.project_id + region = local.region name = var.name endpoint_types = var.endpoint_types type = var.type router = local.router_name - nat_ips = var.addresses + nat_ips = [ + for a in var.addresses : lookup(local.ctx.addresses, a, a) + ] nat_ip_allocate_option = ( var.type == "PRIVATE" ? null @@ -64,30 +73,28 @@ resource "google_compute_router_nat" "nat" { : "AUTO_ONLY" ) ) - source_subnetwork_ip_ranges_to_nat = local.subnet_config icmp_idle_timeout_sec = var.config_timeouts.icmp - udp_idle_timeout_sec = var.config_timeouts.udp + source_subnetwork_ip_ranges_to_nat = local.subnet_config tcp_established_idle_timeout_sec = var.config_timeouts.tcp_established tcp_time_wait_timeout_sec = var.config_timeouts.tcp_time_wait tcp_transitory_idle_timeout_sec = var.config_timeouts.tcp_transitory + udp_idle_timeout_sec = var.config_timeouts.udp enable_endpoint_independent_mapping = ( var.config_port_allocation.enable_endpoint_independent_mapping ) enable_dynamic_port_allocation = ( var.config_port_allocation.enable_dynamic_port_allocation ) + log_config { + enable = var.logging_filter == null ? false : true + filter = var.logging_filter == null ? "ALL" : var.logging_filter + } min_ports_per_vm = ( var.config_port_allocation.min_ports_per_vm ) max_ports_per_vm = ( var.config_port_allocation.max_ports_per_vm ) - - log_config { - enable = var.logging_filter == null ? false : true - filter = var.logging_filter == null ? "ALL" : var.logging_filter - } - dynamic "subnetwork" { for_each = toset( local.subnet_config == "LIST_OF_SUBNETWORKS" @@ -95,7 +102,9 @@ resource "google_compute_router_nat" "nat" { : [] ) content { - name = subnetwork.value.self_link + name = lookup( + local.ctx.subnets, subnetwork.value.self_link, subnetwork.value.self_link + ) source_ip_ranges_to_nat = ( subnetwork.value.all_ranges == true ? ["ALL_IP_RANGES"] @@ -120,7 +129,6 @@ resource "google_compute_router_nat" "nat" { ) } } - dynamic "rules" { for_each = { for i, r in var.rules : i => r } content { diff --git a/modules/net-cloudnat/variables.tf b/modules/net-cloudnat/variables.tf index 88fe1cb83..369a3d94f 100644 --- a/modules/net-cloudnat/variables.tf +++ b/modules/net-cloudnat/variables.tf @@ -17,6 +17,7 @@ variable "addresses" { description = "Optional list of external address self links." type = list(string) + nullable = false default = [] } @@ -79,6 +80,19 @@ variable "config_timeouts" { nullable = false } +variable "context" { + description = "Context-specific interpolations." + type = object({ + addresses = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "endpoint_types" { description = "Specifies the endpoint Types supported by the NAT Gateway. Supported values include: ENDPOINT_TYPE_VM, ENDPOINT_TYPE_SWG, ENDPOINT_TYPE_MANAGED_PROXY_LB." type = list(string) diff --git a/modules/net-firewall-policy/README.md b/modules/net-firewall-policy/README.md index ca3aecf2f..fe899cc03 100644 --- a/modules/net-firewall-policy/README.md +++ b/modules/net-firewall-policy/README.md @@ -17,8 +17,8 @@ The module also makes fewer assumptions about implicit defaults, only using one - [Factory](#factory) - [Firewall Rule Factory Schema](#firewall-rule-factory-schema) - [Dynamic Rule Matching](#dynamic-rule-matching) - - [Ingress Rules (](#ingress-rules-) - - [Egress Rules (](#egress-rules-) + - [Ingress Rules](#ingress-rules) + - [Egress Rules](#egress-rules) - [Rule-Level Mappings](#rule-level-mappings) - [Variables](#variables) - [Outputs](#outputs) @@ -351,7 +351,9 @@ This module simplifies firewall rule creation by using generic, context-aware va The tables below provide a complete reference for these dynamic mappings. -#### Ingress Rules (`direction = "INGRESS"`) +#### Ingress Rules + +`direction = "INGRESS"` | Module Variable (`match.*`) | Mapped Resource Attribute | | :--- | :--- | @@ -361,7 +363,9 @@ The tables below provide a complete reference for these dynamic mappings. | `source_tags` | `src_secure_tags` | | `threat_intelligences` | `src_threat_intelligences` | -#### Egress Rules (`direction = "EGRESS"`) +#### Egress Rules + +`direction = "EGRESS"` | Module Variable (`match.*`) | Mapped Resource Attribute | | :--- | :--- | @@ -377,22 +381,21 @@ The following variable is defined at the top level of the rule (not within the ` | Module Variable | Mapped Resource Attribute | | :--- | :--- | | `target_tags` | `target_secure_tags` | - - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L117) | Policy name. | string | ✓ | | -| [parent_id](variables.tf#L123) | Parent node where the policy will be created, `folders/nnn` or `organizations/nnn` for hierarchical policy, project id for a network policy. | string | ✓ | | +| [name](variables.tf#L132) | Policy name. | string | ✓ | | +| [parent_id](variables.tf#L138) | Parent node where the policy will be created, `folders/nnn` or `organizations/nnn` for hierarchical policy, project id for a network policy. | string | ✓ | | | [attachments](variables.tf#L17) | Ids of the resources to which this policy will be attached, in descriptive name => self link format. Specify folders or organization for hierarchical policy, VPCs for network policy. | map(string) | | {} | -| [description](variables.tf#L24) | Policy description. | string | | null | -| [egress_rules](variables.tf#L30) | List of egress rule definitions, action can be 'allow', 'deny', 'goto_next' or 'apply_security_profile_group'. The match.layer4configs map is in protocol => optional [ports] format. | map(object({…})) | | {} | -| [factories_config](variables.tf#L68) | Paths to folders for the optional factories. | object({…}) | | {} | -| [ingress_rules](variables.tf#L79) | List of ingress rule definitions, action can be 'allow', 'deny', 'goto_next' or 'apply_security_profile_group'. | map(object({…})) | | {} | -| [region](variables.tf#L129) | Policy region. Leave null for hierarchical policy, set to 'global' for a global network policy. | string | | null | -| [security_profile_group_ids](variables.tf#L135) | The optional security groups ids to be referenced in factories. | map(string) | | {} | +| [context](variables.tf#L24) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L39) | Policy description. | string | | null | +| [egress_rules](variables.tf#L45) | List of egress rule definitions, action can be 'allow', 'deny', 'goto_next' or 'apply_security_profile_group'. The match.layer4configs map is in protocol => optional [ports] format. | map(object({…})) | | {} | +| [factories_config](variables.tf#L83) | Paths to folders for the optional factories. | object({…}) | | {} | +| [ingress_rules](variables.tf#L94) | List of ingress rule definitions, action can be 'allow', 'deny', 'goto_next' or 'apply_security_profile_group'. | map(object({…})) | | {} | +| [region](variables.tf#L144) | Policy region. Leave null for hierarchical policy, set to 'global' for a global network policy. | string | | null | +| [security_profile_group_ids](variables.tf#L150) | The optional security groups ids to be referenced in factories. | map(string) | | {} | ## Outputs diff --git a/modules/net-firewall-policy/hierarchical.tf b/modules/net-firewall-policy/hierarchical.tf index 7969a559d..dffbb7805 100644 --- a/modules/net-firewall-policy/hierarchical.tf +++ b/modules/net-firewall-policy/hierarchical.tf @@ -16,7 +16,7 @@ resource "google_compute_firewall_policy" "hierarchical" { count = local.use_hierarchical ? 1 : 0 - parent = var.parent_id + parent = lookup(local.ctx.folder_ids, var.parent_id, var.parent_id) short_name = var.name description = var.description } @@ -24,7 +24,7 @@ resource "google_compute_firewall_policy" "hierarchical" { resource "google_compute_firewall_policy_association" "hierarchical" { for_each = local.use_hierarchical ? var.attachments : {} name = "${var.name}-${each.key}" - attachment_target = each.value + attachment_target = lookup(local.ctx.folder_ids, each.value, each.value) firewall_policy = google_compute_firewall_policy.hierarchical[0].name } @@ -33,23 +33,43 @@ resource "google_compute_firewall_policy_rule" "hierarchical" { for_each = toset( local.use_hierarchical ? keys(local.rules) : [] ) - firewall_policy = google_compute_firewall_policy.hierarchical[0].name - action = local.rules[each.key].action - description = local.rules[each.key].description - direction = local.rules[each.key].direction - disabled = local.rules[each.key].disabled - enable_logging = local.rules[each.key].enable_logging - priority = local.rules[each.key].priority - target_resources = local.rules[each.key].target_resources - target_service_accounts = local.rules[each.key].target_service_accounts - tls_inspect = local.rules[each.key].tls_inspect + firewall_policy = google_compute_firewall_policy.hierarchical[0].name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_resources = ( + local.rules[each.key].target_resources == null ? null : [ + for n in local.rules[each.key].target_resources : + lookup(local.ctx.networks, n, n) + ] + ) + target_service_accounts = ( + local.rules[each.key].target_service_accounts == null ? null : [ + for n in local.rules[each.key].target_service_accounts : + lookup(local.ctx.iam_principals, n, n) + ] + ) + tls_inspect = local.rules[each.key].tls_inspect security_profile_group = try( var.security_profile_group_ids[local.rules[each.key].security_profile_group], local.rules[each.key].security_profile_group ) match { - dest_ip_ranges = local.rules[each.key].match.destination_ranges - src_ip_ranges = local.rules[each.key].match.source_ranges + dest_ip_ranges = ( + local.rules[each.key].match.destination_ranges == null ? null : [ + for r in local.rules[each.key].match.destination_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) + src_ip_ranges = ( + local.rules[each.key].match.source_ranges == null ? null : [ + for r in local.rules[each.key].match.source_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) dest_address_groups = ( local.rules[each.key].direction == "EGRESS" ? local.rules[each.key].match.address_groups diff --git a/modules/net-firewall-policy/main.tf b/modules/net-firewall-policy/main.tf index 093c8bf23..b59a92126 100644 --- a/modules/net-firewall-policy/main.tf +++ b/modules/net-firewall-policy/main.tf @@ -23,6 +23,12 @@ locals { for name, rule in merge(var.ingress_rules) : "ingress/${name}" => merge(rule, { name = name, direction = "INGRESS" }) } + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" rules = merge( local.factory_egress_rules, local.factory_ingress_rules, local._rules_egress, local._rules_ingress diff --git a/modules/net-firewall-policy/net-global.tf b/modules/net-firewall-policy/net-global.tf index cf837c861..8d9bc4049 100644 --- a/modules/net-firewall-policy/net-global.tf +++ b/modules/net-firewall-policy/net-global.tf @@ -16,7 +16,7 @@ resource "google_compute_network_firewall_policy" "net-global" { count = !local.use_hierarchical && !local.use_regional ? 1 : 0 - project = var.parent_id + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) name = var.name description = var.description } @@ -25,9 +25,9 @@ resource "google_compute_network_firewall_policy_association" "net-global" { for_each = ( !local.use_hierarchical && !local.use_regional ? var.attachments : {} ) - project = var.parent_id + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) name = "${var.name}-${each.key}" - attachment_target = each.value + attachment_target = lookup(local.ctx.networks, each.value, each.value) firewall_policy = google_compute_network_firewall_policy.net-global[0].name } @@ -38,24 +38,39 @@ resource "google_compute_network_firewall_policy_rule" "net-global" { ? keys(local.rules) : [] ) - project = var.parent_id - firewall_policy = google_compute_network_firewall_policy.net-global[0].name - rule_name = local.rules[each.key].name - action = local.rules[each.key].action - description = local.rules[each.key].description - direction = local.rules[each.key].direction - disabled = local.rules[each.key].disabled - enable_logging = local.rules[each.key].enable_logging - priority = local.rules[each.key].priority - target_service_accounts = local.rules[each.key].target_service_accounts - tls_inspect = local.rules[each.key].tls_inspect + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) + firewall_policy = google_compute_network_firewall_policy.net-global[0].name + rule_name = local.rules[each.key].name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_service_accounts = ( + local.rules[each.key].target_service_accounts == null ? null : [ + for n in local.rules[each.key].target_service_accounts : + lookup(local.ctx.iam_principals, n, n) + ] + ) + tls_inspect = local.rules[each.key].tls_inspect security_profile_group = try( var.security_profile_group_ids[local.rules[each.key].security_profile_group], local.rules[each.key].security_profile_group ) match { - dest_ip_ranges = local.rules[each.key].match.destination_ranges - src_ip_ranges = local.rules[each.key].match.source_ranges + dest_ip_ranges = ( + local.rules[each.key].match.destination_ranges == null ? null : [ + for r in local.rules[each.key].match.destination_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) + src_ip_ranges = ( + local.rules[each.key].match.source_ranges == null ? null : [ + for r in local.rules[each.key].match.source_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) dest_address_groups = ( local.rules[each.key].direction == "EGRESS" ? local.rules[each.key].match.address_groups @@ -106,7 +121,9 @@ resource "google_compute_network_firewall_policy_rule" "net-global" { dynamic "src_secure_tags" { for_each = toset(coalesce(local.rules[each.key].match.source_tags, [])) content { - name = src_secure_tags.key + name = lookup( + local.ctx.tag_values, src_secure_tags.key, src_secure_tags.key + ) } } } @@ -117,7 +134,9 @@ resource "google_compute_network_firewall_policy_rule" "net-global" { : local.rules[each.key].target_tags ) content { - name = target_secure_tags.value + name = lookup( + local.ctx.tag_values, target_secure_tags.value, target_secure_tags.value + ) } } } diff --git a/modules/net-firewall-policy/net-regional.tf b/modules/net-firewall-policy/net-regional.tf index 281cbc4f9..9a83a5ef6 100644 --- a/modules/net-firewall-policy/net-regional.tf +++ b/modules/net-firewall-policy/net-regional.tf @@ -16,20 +16,20 @@ resource "google_compute_region_network_firewall_policy" "net-regional" { count = !local.use_hierarchical && local.use_regional ? 1 : 0 - project = var.parent_id + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) name = var.name description = var.description - region = var.region + region = lookup(local.ctx.locations, var.region, var.region) } resource "google_compute_region_network_firewall_policy_association" "net-regional" { for_each = ( !local.use_hierarchical && local.use_regional ? var.attachments : {} ) - project = var.parent_id - region = var.region + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) + region = lookup(local.ctx.locations, var.region, var.region) name = "${var.name}-${each.key}" - attachment_target = each.value + attachment_target = lookup(local.ctx.networks, each.value, each.value) firewall_policy = google_compute_region_network_firewall_policy.net-regional[0].name } @@ -40,20 +40,35 @@ resource "google_compute_region_network_firewall_policy_rule" "net-regional" { ? keys(local.rules) : [] ) - project = var.parent_id - region = var.region - firewall_policy = google_compute_region_network_firewall_policy.net-regional[0].name - rule_name = local.rules[each.key].name - action = local.rules[each.key].action - description = local.rules[each.key].description - direction = local.rules[each.key].direction - disabled = local.rules[each.key].disabled - enable_logging = local.rules[each.key].enable_logging - priority = local.rules[each.key].priority - target_service_accounts = local.rules[each.key].target_service_accounts + project = lookup(local.ctx.project_ids, var.parent_id, var.parent_id) + region = lookup(local.ctx.locations, var.region, var.region) + firewall_policy = google_compute_region_network_firewall_policy.net-regional[0].name + rule_name = local.rules[each.key].name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_service_accounts = ( + local.rules[each.key].target_service_accounts == null ? null : [ + for n in local.rules[each.key].target_service_accounts : + lookup(local.ctx.iam_principals, n, n) + ] + ) match { - dest_ip_ranges = local.rules[each.key].match.destination_ranges - src_ip_ranges = local.rules[each.key].match.source_ranges + dest_ip_ranges = ( + local.rules[each.key].match.destination_ranges == null ? null : [ + for r in local.rules[each.key].match.destination_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) + src_ip_ranges = ( + local.rules[each.key].match.source_ranges == null ? null : [ + for r in local.rules[each.key].match.source_ranges : + lookup(local.ctx.cidr_ranges, r, r) + ] + ) dest_address_groups = ( local.rules[each.key].direction == "EGRESS" ? local.rules[each.key].match.address_groups @@ -104,7 +119,9 @@ resource "google_compute_region_network_firewall_policy_rule" "net-regional" { dynamic "src_secure_tags" { for_each = toset(coalesce(local.rules[each.key].match.source_tags, [])) content { - name = src_secure_tags.key + name = lookup( + local.ctx.tag_values, src_secure_tags.key, src_secure_tags.key + ) } } } @@ -115,7 +132,9 @@ resource "google_compute_region_network_firewall_policy_rule" "net-regional" { : local.rules[each.key].target_tags ) content { - name = target_secure_tags.value + name = lookup( + local.ctx.tag_values, target_secure_tags.value, target_secure_tags.value + ) } } } diff --git a/modules/net-firewall-policy/variables.tf b/modules/net-firewall-policy/variables.tf index bab3ee853..9ccc91783 100644 --- a/modules/net-firewall-policy/variables.tf +++ b/modules/net-firewall-policy/variables.tf @@ -21,6 +21,21 @@ variable "attachments" { nullable = false } +variable "context" { + description = "Context-specific interpolations." + type = object({ + cidr_ranges = optional(map(string), {}) + folder_ids = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + tag_values = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "description" { description = "Policy description." type = string diff --git a/tests/modules/net_cloudnat/context.tfvars b/tests/modules/net_cloudnat/context.tfvars new file mode 100644 index 000000000..78b8af838 --- /dev/null +++ b/tests/modules/net_cloudnat/context.tfvars @@ -0,0 +1,28 @@ +context = { + addresses = { + test = "35.10.10.10" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +addresses = ["$addresses:test"] +config_source_subnetworks = { + all = false + subnetworks = [{ + self_link = "$subnets:test" + }] +} +name = "test" +project_id = "$project_ids:test" +region = "$locations:ew8" +router_network = "$networks:test" diff --git a/tests/modules/net_cloudnat/context.yaml b/tests/modules/net_cloudnat/context.yaml new file mode 100644 index 000000000..3faec0cad --- /dev/null +++ b/tests/modules/net_cloudnat/context.yaml @@ -0,0 +1,61 @@ +# 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: + google_compute_router.router[0]: + bgp: [] + description: null + encrypted_interconnect_router: null + md5_authentication_keys: [] + name: test-nat + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + region: europe-west8 + timeouts: null + google_compute_router_nat.nat: + enable_dynamic_port_allocation: false + enable_endpoint_independent_mapping: true + icmp_idle_timeout_sec: 30 + initial_nat_ips: null + log_config: + - enable: false + filter: ALL + max_ports_per_vm: 65536 + name: test + nat64_subnetwork: [] + nat_ip_allocate_option: MANUAL_ONLY + nat_ips: + - 35.10.10.10 + project: foo-test-0 + region: europe-west8 + router: test-nat + rules: [] + source_subnetwork_ip_ranges_to_nat: LIST_OF_SUBNETWORKS + source_subnetwork_ip_ranges_to_nat64: null + subnetwork: + - name: projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce + secondary_ip_range_names: [] + source_ip_ranges_to_nat: + - ALL_IP_RANGES + tcp_established_idle_timeout_sec: 1200 + tcp_time_wait_timeout_sec: 120 + tcp_transitory_idle_timeout_sec: 30 + timeouts: null + type: PUBLIC + udp_idle_timeout_sec: 30 +counts: + google_compute_router: 1 + google_compute_router_nat: 1 + modules: 0 + resources: 2 diff --git a/tests/modules/net_cloudnat/tftest.yaml b/tests/modules/net_cloudnat/tftest.yaml new file mode 100644 index 000000000..8f75c0ecf --- /dev/null +++ b/tests/modules/net_cloudnat/tftest.yaml @@ -0,0 +1,17 @@ +# 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. + +module: modules/net-cloudnat +tests: + context: diff --git a/tests/modules/net_firewall_policy/context-g.tfvars b/tests/modules/net_firewall_policy/context-g.tfvars new file mode 100644 index 000000000..7acda3c67 --- /dev/null +++ b/tests/modules/net_firewall_policy/context-g.tfvars @@ -0,0 +1,52 @@ +context = { + cidr_ranges = { + rfc1918-10 = "10.0.0.0/8" + } + folder_ids = { + test = "folders/1234567890" + } + iam_principals = { + test = "serviceAccount:test@test-project.iam.gserviceaccount.com" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + tag_values = { + "test" = "tagValues/1234567890" + } +} +name = "test-1" +parent_id = "$project_ids:test" +region = "global" +attachments = { + test = "$networks:test" +} +egress_rules = { + smtp = { + priority = 900 + target_service_accounts = ["$iam_principals:test"] + match = { + destination_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "tcp", ports = ["25"] }] + source_tags = ["$tag_values:test"] + } + } +} +ingress_rules = { + icmp = { + priority = 1000 + enable_logging = true + target_resources = ["$networks:test"] + target_tags = ["$tag_values:test"] + match = { + source_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "icmp" }] + } + } +} diff --git a/tests/modules/net_firewall_policy/context-g.yaml b/tests/modules/net_firewall_policy/context-g.yaml new file mode 100644 index 000000000..2a7569356 --- /dev/null +++ b/tests/modules/net_firewall_policy/context-g.yaml @@ -0,0 +1,99 @@ +# 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: + google_compute_network_firewall_policy.net-global[0]: + description: null + name: test-1 + project: foo-test-0 + timeouts: null + google_compute_network_firewall_policy_association.net-global["test"]: + attachment_target: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + firewall_policy: test-1 + name: test-1-test + project: foo-test-0 + timeouts: null + google_compute_network_firewall_policy_rule.net-global["egress/smtp"]: + action: deny + description: null + direction: EGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 10.0.0.0/8 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_secure_tags: + - name: tagValues/1234567890 + src_threat_intelligences: null + priority: 900 + project: foo-test-0 + rule_name: smtp + security_profile_group: null + target_secure_tags: [] + target_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + timeouts: null + tls_inspect: null + google_compute_network_firewall_policy_rule.net-global["ingress/icmp"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: true + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1000 + project: foo-test-0 + rule_name: icmp + security_profile_group: null + target_secure_tags: + - name: tagValues/1234567890 + target_service_accounts: null + timeouts: null + tls_inspect: null + +counts: + google_compute_network_firewall_policy: 1 + google_compute_network_firewall_policy_association: 1 + google_compute_network_firewall_policy_rule: 2 + modules: 0 + resources: 4 diff --git a/tests/modules/net_firewall_policy/context-h.tfvars b/tests/modules/net_firewall_policy/context-h.tfvars new file mode 100644 index 000000000..80eb44543 --- /dev/null +++ b/tests/modules/net_firewall_policy/context-h.tfvars @@ -0,0 +1,49 @@ +context = { + cidr_ranges = { + rfc1918-10 = "10.0.0.0/8" + } + folder_ids = { + test = "folders/1234567890" + } + iam_principals = { + test = "serviceAccount:test@test-project.iam.gserviceaccount.com" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + tag_values = { + "test/one" = "tagValues/1234567890" + } +} +name = "test-1" +parent_id = "$folder_ids:test" +attachments = { + test = "$folder_ids:test" +} +egress_rules = { + smtp = { + priority = 900 + match = { + destination_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "tcp", ports = ["25"] }] + } + } +} +ingress_rules = { + icmp = { + priority = 1000 + enable_logging = true + target_resources = ["$networks:test"] + target_service_accounts = ["$iam_principals:test"] + match = { + source_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "icmp" }] + } + } +} diff --git a/tests/modules/net_firewall_policy/context-h.yaml b/tests/modules/net_firewall_policy/context-h.yaml new file mode 100644 index 000000000..ebcbcc539 --- /dev/null +++ b/tests/modules/net_firewall_policy/context-h.yaml @@ -0,0 +1,92 @@ +# 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: + google_compute_firewall_policy.hierarchical[0]: + description: null + parent: folders/1234567890 + short_name: test-1 + timeouts: null + google_compute_firewall_policy_association.hierarchical["test"]: + attachment_target: folders/1234567890 + name: test-1-test + timeouts: null + google_compute_firewall_policy_rule.hierarchical["egress/smtp"]: + action: deny + description: null + direction: EGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 10.0.0.0/8 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 900 + security_profile_group: null + target_resources: null + target_secure_tags: [] + target_service_accounts: null + timeouts: null + tls_inspect: null + google_compute_firewall_policy_rule.hierarchical["ingress/icmp"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: true + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1000 + security_profile_group: null + target_resources: + - projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + target_secure_tags: [] + target_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + timeouts: null + tls_inspect: null + +counts: + google_compute_firewall_policy: 1 + google_compute_firewall_policy_association: 1 + google_compute_firewall_policy_rule: 2 + modules: 0 + resources: 4 diff --git a/tests/modules/net_firewall_policy/context-r.tfvars b/tests/modules/net_firewall_policy/context-r.tfvars new file mode 100644 index 000000000..9630d37be --- /dev/null +++ b/tests/modules/net_firewall_policy/context-r.tfvars @@ -0,0 +1,52 @@ +context = { + cidr_ranges = { + rfc1918-10 = "10.0.0.0/8" + } + folder_ids = { + test = "folders/1234567890" + } + iam_principals = { + test = "serviceAccount:test@test-project.iam.gserviceaccount.com" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + tag_values = { + "test" = "tagValues/1234567890" + } +} +name = "test-1" +parent_id = "$project_ids:test" +region = "$locations:ew8" +attachments = { + test = "$networks:test" +} +egress_rules = { + smtp = { + priority = 900 + target_service_accounts = ["$iam_principals:test"] + match = { + destination_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "tcp", ports = ["25"] }] + source_tags = ["$tag_values:test"] + } + } +} +ingress_rules = { + icmp = { + priority = 1000 + enable_logging = true + target_resources = ["$networks:test"] + target_tags = ["$tag_values:test"] + match = { + source_ranges = ["$cidr_ranges:rfc1918-10"] + layer4_configs = [{ protocol = "icmp" }] + } + } +} diff --git a/tests/modules/net_firewall_policy/context-r.yaml b/tests/modules/net_firewall_policy/context-r.yaml new file mode 100644 index 000000000..d68a05777 --- /dev/null +++ b/tests/modules/net_firewall_policy/context-r.yaml @@ -0,0 +1,103 @@ +# 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: + google_compute_region_network_firewall_policy.net-regional[0]: + description: null + name: test-1 + project: foo-test-0 + region: europe-west8 + timeouts: null + google_compute_region_network_firewall_policy_association.net-regional["test"]: + attachment_target: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + firewall_policy: test-1 + name: test-1-test + project: foo-test-0 + region: europe-west8 + timeouts: null + google_compute_region_network_firewall_policy_rule.net-regional["egress/smtp"]: + action: deny + description: null + direction: EGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 10.0.0.0/8 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_secure_tags: + - name: tagValues/1234567890 + src_threat_intelligences: null + priority: 900 + project: foo-test-0 + region: europe-west8 + rule_name: smtp + security_profile_group: null + target_secure_tags: [] + target_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + timeouts: null + tls_inspect: null + google_compute_region_network_firewall_policy_rule.net-regional["ingress/icmp"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: true + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1000 + project: foo-test-0 + region: europe-west8 + rule_name: icmp + security_profile_group: null + target_secure_tags: + - name: tagValues/1234567890 + target_service_accounts: null + timeouts: null + tls_inspect: null + +counts: + google_compute_region_network_firewall_policy: 1 + google_compute_region_network_firewall_policy_association: 1 + google_compute_region_network_firewall_policy_rule: 2 + modules: 0 + resources: 4 diff --git a/tests/modules/net_firewall_policy/tftest.yaml b/tests/modules/net_firewall_policy/tftest.yaml new file mode 100644 index 000000000..192932f44 --- /dev/null +++ b/tests/modules/net_firewall_policy/tftest.yaml @@ -0,0 +1,19 @@ +# 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. + +module: modules/net-firewall-policy +tests: + context-g: + context-h: + context-r: From 7c920d7d353a1e93d9177e88f6c82b3a79e47db8 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 16 Oct 2025 07:54:14 +0200 Subject: [PATCH 12/13] Add support for context to net-lb-int net-vpc-firewall and net-vpc module (#3419) * net-lb-int * net-vpc-firewall * net-vpc * net-vpc --- modules/net-lb-int/README.md | 25 +-- modules/net-lb-int/groups.tf | 4 +- modules/net-lb-int/health-check.tf | 2 +- modules/net-lb-int/main.tf | 53 ++++-- modules/net-lb-int/variables.tf | 13 ++ modules/net-vpc-firewall/README.md | 15 +- modules/net-vpc-firewall/default-rules.tf | 62 ++++--- modules/net-vpc-firewall/main.tf | 46 +++-- modules/net-vpc-firewall/variables.tf | 12 ++ modules/net-vpc/README.md | 54 +++--- modules/net-vpc/internal-ranges.tf | 19 +- modules/net-vpc/main.tf | 54 ++++-- modules/net-vpc/psa.tf | 5 +- modules/net-vpc/routes.tf | 72 ++++--- modules/net-vpc/subnets.tf | 152 +++++++++------ modules/net-vpc/variables.tf | 15 +- tests/modules/net_lb_int/context.tfvars | 42 +++++ tests/modules/net_lb_int/context.yaml | 126 +++++++++++++ tests/modules/net_lb_int/tftest.yaml | 1 + tests/modules/net_vpc/context.tfvars | 96 ++++++++++ tests/modules/net_vpc/context.yaml | 175 ++++++++++++++++++ tests/modules/net_vpc/tftest.yaml | 1 + tests/modules/net_vpc_firewall/context.tfvars | 47 +++++ tests/modules/net_vpc_firewall/context.yaml | 157 ++++++++++++++++ tests/modules/net_vpc_firewall/tftest.yaml | 17 ++ 25 files changed, 1044 insertions(+), 221 deletions(-) create mode 100644 tests/modules/net_lb_int/context.tfvars create mode 100644 tests/modules/net_lb_int/context.yaml create mode 100644 tests/modules/net_vpc/context.tfvars create mode 100644 tests/modules/net_vpc/context.yaml create mode 100644 tests/modules/net_vpc_firewall/context.tfvars create mode 100644 tests/modules/net_vpc_firewall/context.yaml create mode 100644 tests/modules/net_vpc_firewall/tftest.yaml diff --git a/modules/net-lb-int/README.md b/modules/net-lb-int/README.md index 5ed7b40a9..99ae056fe 100644 --- a/modules/net-lb-int/README.md +++ b/modules/net-lb-int/README.md @@ -374,20 +374,21 @@ One other issue is a `Provider produced inconsistent final plan` error which is | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L188) | Name used for all resources. | string | ✓ | | -| [project_id](variables.tf#L193) | Project id where resources will be created. | string | ✓ | | -| [region](variables.tf#L198) | GCP region. | string | ✓ | | -| [vpc_config](variables.tf#L224) | VPC-level configuration. | object({…}) | ✓ | | +| [name](variables.tf#L201) | Name used for all resources. | string | ✓ | | +| [project_id](variables.tf#L206) | Project id where resources will be created. | string | ✓ | | +| [region](variables.tf#L211) | GCP region. | string | ✓ | | +| [vpc_config](variables.tf#L237) | VPC-level configuration. | object({…}) | ✓ | | | [backend_service_config](variables.tf#L17) | Backend service level configuration. | object({…}) | | {} | | [backends](variables.tf#L53) | Load balancer backends. | list(object({…})) | | [] | -| [description](variables.tf#L64) | Optional description used for resources. | string | | "Terraform managed." | -| [forwarding_rules_config](variables.tf#L70) | The optional forwarding rules configuration. | map(object({…})) | | {…} | -| [group_configs](variables.tf#L86) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…})) | | {} | -| [health_check](variables.tf#L98) | Name of existing health check to use, disables auto-created health check. Also set `health_check_config = null` when cross-referencing an health check from another load balancer module to avoid a Terraform error. | string | | null | -| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | {…} | -| [labels](variables.tf#L182) | Labels set on resources. | map(string) | | {} | -| [service_attachments](variables.tf#L203) | PSC service attachments, keyed by forwarding rule. | map(object({…})) | | null | -| [service_label](variables.tf#L218) | Optional prefix of the fully qualified forwarding rule name. | string | | null | +| [context](variables.tf#L64) | Context-specific interpolations. | object({…}) | | {} | +| [description](variables.tf#L77) | Optional description used for resources. | string | | "Terraform managed." | +| [forwarding_rules_config](variables.tf#L83) | The optional forwarding rules configuration. | map(object({…})) | | {…} | +| [group_configs](variables.tf#L99) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…})) | | {} | +| [health_check](variables.tf#L111) | Name of existing health check to use, disables auto-created health check. Also set `health_check_config = null` when cross-referencing an health check from another load balancer module to avoid a Terraform error. | string | | null | +| [health_check_config](variables.tf#L117) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | {…} | +| [labels](variables.tf#L195) | Labels set on resources. | map(string) | | {} | +| [service_attachments](variables.tf#L216) | PSC service attachments, keyed by forwarding rule. | map(object({…})) | | null | +| [service_label](variables.tf#L231) | Optional prefix of the fully qualified forwarding rule name. | string | | null | ## Outputs diff --git a/modules/net-lb-int/groups.tf b/modules/net-lb-int/groups.tf index 736dfc6f5..3492f5b3d 100644 --- a/modules/net-lb-int/groups.tf +++ b/modules/net-lb-int/groups.tf @@ -18,8 +18,8 @@ resource "google_compute_instance_group" "default" { for_each = var.group_configs - project = var.project_id - zone = each.value.zone + project = local.project_id + zone = lookup(local.ctx.locations, each.value.zone, each.value.zone) name = "${var.name}-${each.key}" description = each.value.description instances = each.value.instances diff --git a/modules/net-lb-int/health-check.tf b/modules/net-lb-int/health-check.tf index 1e0bd193b..18bc4ccac 100644 --- a/modules/net-lb-int/health-check.tf +++ b/modules/net-lb-int/health-check.tf @@ -31,7 +31,7 @@ locals { resource "google_compute_health_check" "default" { provider = google-beta count = local.hc != null ? 1 : 0 - project = var.project_id + project = local.project_id name = coalesce(local.hc.name, var.name) description = local.hc.description check_interval_sec = local.hc.check_interval_sec diff --git a/modules/net-lb-int/main.tf b/modules/net-lb-int/main.tf index 201d1e3eb..c517c662b 100644 --- a/modules/net-lb-int/main.tf +++ b/modules/net-lb-int/main.tf @@ -16,19 +16,34 @@ locals { + _service_attachments = ( + var.service_attachments == null ? {} : var.service_attachments + ) bs_conntrack = var.backend_service_config.connection_tracking bs_failover = var.backend_service_config.failover_config forwarding_rule_names = { for k, v in var.forwarding_rules_config : k => k == "" ? var.name : "${var.name}-${k}" } + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" health_check = ( var.health_check != null ? var.health_check : google_compute_health_check.default[0].self_link ) - _service_attachments = ( - var.service_attachments == null ? {} : var.service_attachments + network = lookup( + local.ctx.networks, var.vpc_config.network, var.vpc_config.network + ) + project_id = lookup( + local.ctx.project_ids, var.project_id, var.project_id + ) + region = lookup( + local.ctx.locations, var.region, var.region ) service_attachments = { for k, v in local._service_attachments : @@ -44,36 +59,38 @@ moved { resource "google_compute_forwarding_rule" "default" { for_each = var.forwarding_rules_config provider = google-beta - project = var.project_id + project = local.project_id name = coalesce(each.value.name, local.forwarding_rule_names[each.key]) - region = var.region + region = local.region description = each.value.description - ip_address = each.value.address + ip_address = try(local.ctx.addresses[each.value.address], each.value.address) ip_protocol = each.value.protocol ip_version = each.value.address != null ? null : each.value.ipv6 == true ? "IPV6" : "IPV4" # do not set if address is provided backend_service = ( google_compute_region_backend_service.default.self_link ) load_balancing_scheme = "INTERNAL" - network = var.vpc_config.network + network = local.network ports = each.value.ports # "nnnnn" or "nnnnn,nnnnn,nnnnn" max 5 - subnetwork = var.vpc_config.subnetwork - allow_global_access = each.value.global_access - labels = var.labels - all_ports = each.value.ports == null ? true : null - service_label = var.service_label + subnetwork = lookup( + local.ctx.subnets, var.vpc_config.subnetwork, var.vpc_config.subnetwork + ) + allow_global_access = each.value.global_access + labels = var.labels + all_ports = each.value.ports == null ? true : null + service_label = var.service_label # is_mirroring_collector = false } resource "google_compute_region_backend_service" "default" { provider = google-beta - project = var.project_id - region = var.region + project = local.project_id + region = local.region name = coalesce(var.backend_service_config.name, var.name) description = var.backend_service_config.description load_balancing_scheme = "INTERNAL" protocol = var.backend_service_config.protocol - network = var.vpc_config.network + network = local.network health_checks = [local.health_check] connection_draining_timeout_sec = var.backend_service_config.connection_draining_timeout_sec session_affinity = var.backend_service_config.session_affinity @@ -135,12 +152,14 @@ resource "google_compute_region_backend_service" "default" { resource "google_compute_service_attachment" "default" { for_each = local.service_attachments - project = var.project_id - region = var.region + project = local.project_id + region = local.region name = local.forwarding_rule_names[each.key] description = var.description target_service = google_compute_forwarding_rule.default[each.key].id - nat_subnets = each.value.nat_subnets + nat_subnets = each.value.nat_subnets == null ? null : [ + for s in each.value.nat_subnets : lookup(local.ctx.subnets, s, s) + ] connection_preference = ( each.value.automatic_connection ? "ACCEPT_AUTOMATIC" : "ACCEPT_MANUAL" ) diff --git a/modules/net-lb-int/variables.tf b/modules/net-lb-int/variables.tf index 5df659c0a..5b023581f 100644 --- a/modules/net-lb-int/variables.tf +++ b/modules/net-lb-int/variables.tf @@ -61,6 +61,19 @@ variable "backends" { nullable = false } +variable "context" { + description = "Context-specific interpolations." + type = object({ + addresses = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "description" { description = "Optional description used for resources." type = string diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md index 8750d073f..f074556e8 100644 --- a/modules/net-vpc-firewall/README.md +++ b/modules/net-vpc-firewall/README.md @@ -269,13 +269,14 @@ module "firewall" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [network](variables.tf#L111) | Name of the network this set of firewall rules applies to. | string | ✓ | | -| [project_id](variables.tf#L116) | Project id of the project that holds the network. | string | ✓ | | -| [default_rules_config](variables.tf#L17) | Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable. | object({…}) | | {} | -| [egress_rules](variables.tf#L37) | List of egress rule definitions, default to deny action. Null destination ranges will be replaced with 0/0. | map(object({…})) | | {} | -| [factories_config](variables.tf#L60) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [ingress_rules](variables.tf#L70) | List of ingress rule definitions, default to allow action. Null source ranges will be replaced with 0/0. | map(object({…})) | | {} | -| [named_ranges](variables.tf#L94) | Define mapping of names to ranges that can be used in custom rules. | map(list(string)) | | {…} | +| [network](variables.tf#L123) | Name of the network this set of firewall rules applies to. | string | ✓ | | +| [project_id](variables.tf#L128) | Project id of the project that holds the network. | string | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [default_rules_config](variables.tf#L29) | Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable. | object({…}) | | {} | +| [egress_rules](variables.tf#L49) | List of egress rule definitions, default to deny action. Null destination ranges will be replaced with 0/0. | map(object({…})) | | {} | +| [factories_config](variables.tf#L72) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [ingress_rules](variables.tf#L82) | List of ingress rule definitions, default to allow action. Null source ranges will be replaced with 0/0. | map(object({…})) | | {} | +| [named_ranges](variables.tf#L106) | Define mapping of names to ranges that can be used in custom rules. | map(list(string)) | | {…} | ## Outputs diff --git a/modules/net-vpc-firewall/default-rules.tf b/modules/net-vpc-firewall/default-rules.tf index bbca6dd57..a0f38ab70 100644 --- a/modules/net-vpc-firewall/default-rules.tf +++ b/modules/net-vpc-firewall/default-rules.tf @@ -25,23 +25,27 @@ locals { } resource "google_compute_firewall" "allow-admins" { - count = length(local.default_rules.admin_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-admins" - description = "Access from the admin subnet to all subnets." - network = var.network - project = var.project_id - source_ranges = local.default_rules.admin_ranges + count = length(local.default_rules.admin_ranges) > 0 ? 1 : 0 + project = local.project_id + network = local.network + name = "${local.network_name}-ingress-admins" + description = "Access from the admin subnet to all subnets." + source_ranges = [ + for r in local.default_rules.admin_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] allow { protocol = "all" } } resource "google_compute_firewall" "allow-tag-http" { - count = length(local.default_rules.http_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-http" - description = "Allow http to machines with matching tags." - network = var.network - project = var.project_id - source_ranges = local.default_rules.http_ranges - target_tags = local.default_rules.http_tags + count = length(local.default_rules.http_ranges) > 0 ? 1 : 0 + project = local.project_id + network = local.network + name = "${local.network_name}-ingress-tag-http" + description = "Allow http to machines with matching tags." + source_ranges = [ + for r in local.default_rules.http_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] + target_tags = local.default_rules.http_tags allow { protocol = "tcp" ports = ["80"] @@ -49,13 +53,15 @@ resource "google_compute_firewall" "allow-tag-http" { } resource "google_compute_firewall" "allow-tag-https" { - count = length(local.default_rules.https_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-https" - description = "Allow http to machines with matching tags." - network = var.network - project = var.project_id - source_ranges = local.default_rules.https_ranges - target_tags = local.default_rules.https_tags + count = length(local.default_rules.https_ranges) > 0 ? 1 : 0 + project = local.project_id + network = local.network + name = "${local.network_name}-ingress-tag-https" + description = "Allow http to machines with matching tags." + source_ranges = [ + for r in local.default_rules.https_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] + target_tags = local.default_rules.https_tags allow { protocol = "tcp" ports = ["443"] @@ -63,13 +69,15 @@ resource "google_compute_firewall" "allow-tag-https" { } resource "google_compute_firewall" "allow-tag-ssh" { - count = length(local.default_rules.ssh_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-ssh" - description = "Allow SSH to machines with matching tags." - network = var.network - project = var.project_id - source_ranges = local.default_rules.ssh_ranges - target_tags = local.default_rules.ssh_tags + count = length(local.default_rules.ssh_ranges) > 0 ? 1 : 0 + project = local.project_id + network = local.network + name = "${local.network_name}-ingress-tag-ssh" + description = "Allow SSH to machines with matching tags." + source_ranges = [ + for r in local.default_rules.ssh_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] + target_tags = local.default_rules.ssh_tags allow { protocol = "tcp" ports = ["22"] diff --git a/modules/net-vpc-firewall/main.tf b/modules/net-vpc-firewall/main.tf index 5fed5817e..4a55de8ed 100644 --- a/modules/net-vpc-firewall/main.tf +++ b/modules/net-vpc-firewall/main.tf @@ -66,6 +66,15 @@ locals { for name, rule in merge(var.ingress_rules) : name => merge(rule, { direction = "INGRESS" }) } + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } + } + ctx_p = "$" + network = lookup(local.ctx.networks, var.network, var.network) + network_name = reverse(split("/", local.network))[0] + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) # convert rules data to resource format and replace range template variables rules = { for name, rule in local._rules : @@ -94,30 +103,32 @@ locals { resource "google_compute_firewall" "custom-rules" { for_each = local.rules - project = var.project_id - network = var.network + project = local.project_id + network = local.network name = each.key description = each.value.description direction = each.value.direction source_ranges = ( - each.value.direction == "INGRESS" + each.value.source_ranges == null ? ( - each.value.source_ranges == null && each.value.sources == null + each.value.direction == "INGRESS" && each.value.sources == null ? ["0.0.0.0/0"] - : each.value.source_ranges + : null ) - #for egress, we will include the source_ranges when provided. Previously, null was forced - : each.value.source_ranges + : [ + for r in each.value.source_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] ) destination_ranges = ( - each.value.direction == "EGRESS" + each.value.destination_ranges == null ? ( - each.value.destination_ranges == null + each.value.direction == "EGRESS" ? ["0.0.0.0/0"] - : each.value.destination_ranges + : null ) - #for ingress, we will include the destination_ranges when provided. Previously, null was forced - : each.value.destination_ranges + : [ + for r in each.value.destination_ranges : lookup(local.ctx.cidr_ranges, r, r) + ] ) source_tags = ( each.value.use_service_accounts || each.value.direction == "EGRESS" @@ -126,14 +137,19 @@ resource "google_compute_firewall" "custom-rules" { ) source_service_accounts = ( each.value.use_service_accounts && each.value.direction == "INGRESS" - ? each.value.sources + ? (each.value.sources == null ? null : [ + for s in each.value.sources : lookup(local.ctx.iam_principals, s, s) + ]) : null ) target_tags = ( - each.value.use_service_accounts ? null : each.value.targets + !each.value.use_service_accounts ? each.value.targets : null ) target_service_accounts = ( - each.value.use_service_accounts ? each.value.targets : null + !each.value.use_service_accounts ? null : ( + each.value.targets == null ? null : [ + for s in each.value.targets : lookup(local.ctx.iam_principals, s, s) + ]) ) disabled = each.value.disabled == true priority = each.value.priority diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf index 104f87d5b..594492694 100644 --- a/modules/net-vpc-firewall/variables.tf +++ b/modules/net-vpc-firewall/variables.tf @@ -14,6 +14,18 @@ * limitations under the License. */ +variable "context" { + description = "Context-specific interpolations." + type = object({ + cidr_ranges = optional(map(string), {}) + iam_principals = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "default_rules_config" { description = "Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable." type = object({ diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 8309ec6c4..af95ac727 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -486,7 +486,7 @@ module "vpc" { project_id = var.project_id name = "my-network" context = { - regions = { + locations = { primary = "europe-west4" secondary = "europe-west8" } @@ -500,7 +500,7 @@ module "vpc" { ```yaml name: simple -region: $regions:primary +region: $locations:primary ip_cidr_range: 10.0.1.0/24 # tftest-file id=subnet-simple path=config/subnets/subnet-simple.yaml schema=subnet.schema.json @@ -899,32 +899,32 @@ secondary_ip_ranges: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L171) | The name of the network being created. | string | ✓ | | -| [project_id](variables.tf#L248) | The ID of the project where this VPC will be created. | string | ✓ | | +| [name](variables.tf#L184) | The name of the network being created. | string | ✓ | | +| [project_id](variables.tf#L261) | The ID of the project where this VPC will be created. | string | ✓ | | | [auto_create_subnetworks](variables.tf#L17) | Set to true to create an auto mode subnet, defaults to custom mode. | bool | | false | -| [context](variables.tf#L23) | Context-specific interpolations. | object({…}) | | {} | -| [create_googleapis_routes](variables.tf#L32) | Toggle creation of googleapis private/restricted routes. Disabled when vpc creation is turned off, or when set to null. | object({…}) | | {} | -| [delete_default_routes_on_create](variables.tf#L45) | Set to true to delete the default routes at creation time. | bool | | false | -| [description](variables.tf#L51) | An optional description of this resource (triggers recreation on change). | string | | "Terraform-managed." | -| [dns_policy](variables.tf#L57) | DNS policy setup for the VPC. | object({…}) | | null | -| [factories_config](variables.tf#L70) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy_enforcement_order](variables.tf#L79) | Order that Firewall Rules and Firewall Policies are evaluated. Can be either 'BEFORE_CLASSIC_FIREWALL' or 'AFTER_CLASSIC_FIREWALL'. | string | | "AFTER_CLASSIC_FIREWALL" | -| [internal_ranges](variables.tf#L91) | Internal range configuration for IPAM operations within the VPC network. | list(object({…})) | | [] | -| [ipv6_config](variables.tf#L155) | Optional IPv6 configuration for this network. | object({…}) | | {} | -| [mtu](variables.tf#L165) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes. | number | | null | -| [network_attachments](variables.tf#L176) | PSC network attachments, names as keys. | map(object({…})) | | {} | -| [peering_config](variables.tf#L189) | VPC peering configuration. | object({…}) | | null | -| [policy_based_routes](variables.tf#L200) | Policy based routes, keyed by name. | map(object({…})) | | {} | -| [psa_configs](variables.tf#L253) | The Private Service Access configuration. | list(object({…})) | | [] | -| [routes](variables.tf#L284) | Network routes, keyed by name. | map(object({…})) | | {} | -| [routing_mode](variables.tf#L305) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" | -| [shared_vpc_host](variables.tf#L315) | Enable shared VPC for this project. | bool | | false | -| [shared_vpc_service_projects](variables.tf#L321) | Shared VPC service projects to register with this host. | list(string) | | [] | -| [subnets](variables.tf#L327) | Subnet configuration. | list(object({…})) | | [] | -| [subnets_private_nat](variables.tf#L407) | List of private NAT subnets. | list(object({…})) | | [] | -| [subnets_proxy_only](variables.tf#L419) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | -| [subnets_psc](variables.tf#L453) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | -| [vpc_reuse](variables.tf#L485) | Reuse existing VPC if not null. If the network_id number is not passed in, a data source is used. | object({…}) | | null | +| [context](variables.tf#L23) | Context-specific interpolations. | object({…}) | | {} | +| [create_googleapis_routes](variables.tf#L45) | Toggle creation of googleapis private/restricted routes. Disabled when vpc creation is turned off, or when set to null. | object({…}) | | {} | +| [delete_default_routes_on_create](variables.tf#L58) | Set to true to delete the default routes at creation time. | bool | | false | +| [description](variables.tf#L64) | An optional description of this resource (triggers recreation on change). | string | | "Terraform-managed." | +| [dns_policy](variables.tf#L70) | DNS policy setup for the VPC. | object({…}) | | null | +| [factories_config](variables.tf#L83) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy_enforcement_order](variables.tf#L92) | Order that Firewall Rules and Firewall Policies are evaluated. Can be either 'BEFORE_CLASSIC_FIREWALL' or 'AFTER_CLASSIC_FIREWALL'. | string | | "AFTER_CLASSIC_FIREWALL" | +| [internal_ranges](variables.tf#L104) | Internal range configuration for IPAM operations within the VPC network. | list(object({…})) | | [] | +| [ipv6_config](variables.tf#L168) | Optional IPv6 configuration for this network. | object({…}) | | {} | +| [mtu](variables.tf#L178) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes. | number | | null | +| [network_attachments](variables.tf#L189) | PSC network attachments, names as keys. | map(object({…})) | | {} | +| [peering_config](variables.tf#L202) | VPC peering configuration. | object({…}) | | null | +| [policy_based_routes](variables.tf#L213) | Policy based routes, keyed by name. | map(object({…})) | | {} | +| [psa_configs](variables.tf#L266) | The Private Service Access configuration. | list(object({…})) | | [] | +| [routes](variables.tf#L297) | Network routes, keyed by name. | map(object({…})) | | {} | +| [routing_mode](variables.tf#L318) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" | +| [shared_vpc_host](variables.tf#L328) | Enable shared VPC for this project. | bool | | false | +| [shared_vpc_service_projects](variables.tf#L334) | Shared VPC service projects to register with this host. | list(string) | | [] | +| [subnets](variables.tf#L340) | Subnet configuration. | list(object({…})) | | [] | +| [subnets_private_nat](variables.tf#L420) | List of private NAT subnets. | list(object({…})) | | [] | +| [subnets_proxy_only](variables.tf#L432) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | +| [subnets_psc](variables.tf#L466) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | +| [vpc_reuse](variables.tf#L498) | Reuse existing VPC if not null. If the network_id number is not passed in, a data source is used. | object({…}) | | null | ## Outputs diff --git a/modules/net-vpc/internal-ranges.tf b/modules/net-vpc/internal-ranges.tf index 2304919d4..0a78b6717 100644 --- a/modules/net-vpc/internal-ranges.tf +++ b/modules/net-vpc/internal-ranges.tf @@ -52,12 +52,10 @@ locals { } } } - internal_ranges = merge( { for r in var.internal_ranges : r.name => r }, local._factory_internal_ranges ) - internal_ranges_ids = { for k, v in google_network_connectivity_internal_range.internal_range : k => v.id @@ -65,14 +63,15 @@ locals { } resource "google_network_connectivity_internal_range" "internal_range" { - provider = google-beta - for_each = local.internal_ranges - project = var.project_id - name = each.value.name - network = local.network.id - - description = each.value.description - ip_cidr_range = each.value.ip_cidr_range + provider = google-beta + for_each = local.internal_ranges + project = local.project_id + name = each.value.name + network = local.network.id + description = each.value.description + ip_cidr_range = try( + local.ctx.cidr_ranges[each.value.ip_cidr_range], each.value.ip_cidr_range + ) labels = each.value.labels usage = each.value.usage peering = each.value.peering diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf index 6fa23edd9..446505cb5 100644 --- a/modules/net-vpc/main.tf +++ b/modules/net-vpc/main.tf @@ -15,11 +15,14 @@ */ locals { - ctx = { + _ctx = { for k, v in var.context : k => { for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv } } + ctx = merge(local._ctx, { + locations = merge(local._ctx.regions, local._ctx.locations) + }) ctx_p = "$" network = ( var.vpc_reuse == null @@ -56,19 +59,28 @@ locals { peer_network = ( var.peering_config == null ? null - : element(reverse(split("/", var.peering_config.peer_vpc_self_link)), 0) + : ( + startswith(var.peering_config.peer_vpc_self_link, "$networks:") + ? lookup( + local.ctx.networks, + var.peering_config.peer_vpc_self_link, + var.peering_config.peer_vpc_self_link + ) + : element(reverse(split("/", var.peering_config.peer_vpc_self_link)), 0) + ) ) + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) } data "google_compute_network" "network" { count = try(var.vpc_reuse.use_data_source, null) == true ? 1 : 0 name = var.name - project = var.project_id + project = local.project_id } resource "google_compute_network" "network" { count = var.vpc_reuse == null ? 1 : 0 - project = var.project_id + project = local.project_id name = var.name description = var.description auto_create_subnetworks = var.auto_create_subnetworks @@ -81,11 +93,15 @@ resource "google_compute_network" "network" { } resource "google_compute_network_peering" "local" { - provider = google-beta - count = var.peering_config == null ? 0 : 1 - name = "${var.name}-${local.peer_network}" - network = local.network.self_link - peer_network = var.peering_config.peer_vpc_self_link + provider = google-beta + count = var.peering_config == null ? 0 : 1 + name = "${var.name}-${local.peer_network}" + network = local.network.self_link + peer_network = lookup( + local.ctx.networks, + var.peering_config.peer_vpc_self_link, + var.peering_config.peer_vpc_self_link + ) export_custom_routes = var.peering_config.export_routes import_custom_routes = var.peering_config.import_routes } @@ -97,8 +113,12 @@ resource "google_compute_network_peering" "remote" { ? 1 : 0 ) - name = "${local.peer_network}-${var.name}" - network = var.peering_config.peer_vpc_self_link + name = "${local.peer_network}-${var.name}" + network = lookup( + local.ctx.networks, + var.peering_config.peer_vpc_self_link, + var.peering_config.peer_vpc_self_link + ) peer_network = local.network.self_link export_custom_routes = var.peering_config.import_routes import_custom_routes = var.peering_config.export_routes @@ -108,7 +128,7 @@ resource "google_compute_network_peering" "remote" { resource "google_compute_shared_vpc_host_project" "shared_vpc_host" { provider = google-beta count = var.shared_vpc_host ? 1 : 0 - project = var.project_id + project = local.project_id depends_on = [local.network] } @@ -119,14 +139,14 @@ resource "google_compute_shared_vpc_service_project" "service_projects" { ? var.shared_vpc_service_projects : [] ) - host_project = var.project_id - service_project = each.value + host_project = local.project_id + service_project = lookup(local.ctx.project_ids, each.value, each.value) depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host] } resource "google_dns_policy" "default" { count = var.dns_policy == null ? 0 : 1 - project = var.project_id + project = local.project_id name = var.name enable_inbound_forwarding = try(var.dns_policy.inbound, null) enable_logging = try(var.dns_policy.logging, null) @@ -145,7 +165,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.value + ipv4_address = lookup(local.ctx.addresses, ns.value, ns.value) forwarding_path = "private" } } @@ -157,7 +177,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.value + ipv4_address = lookup(local.ctx.addresses, ns.value, ns.value) } } } diff --git a/modules/net-vpc/psa.tf b/modules/net-vpc/psa.tf index 959193896..8b9855896 100644 --- a/modules/net-vpc/psa.tf +++ b/modules/net-vpc/psa.tf @@ -44,7 +44,8 @@ locals { }) } psa_configs_ranges = { - for v in local._psa_configs_ranges : v.key => v.value + for v in local._psa_configs_ranges : + v.key => lookup(local.ctx.cidr_ranges, v.value, v.value) } psa_peered_domains = { for v in local._psa_peered_domains : v.key => v @@ -53,7 +54,7 @@ locals { resource "google_compute_global_address" "psa_ranges" { for_each = local.psa_configs_ranges - project = var.project_id + project = local.project_id network = local.network.id name = each.key purpose = "VPC_PEERING" diff --git a/modules/net-vpc/routes.tf b/modules/net-vpc/routes.tf index e8fd8812b..c31440aee 100644 --- a/modules/net-vpc/routes.tf +++ b/modules/net-vpc/routes.tf @@ -50,36 +50,42 @@ locals { } resource "google_compute_route" "gateway" { - for_each = local.routes.gateway - project = var.project_id - network = local.network.name - name = "${var.name}-${each.key}" - description = each.value.description - dest_range = each.value.dest_range + for_each = local.routes.gateway + project = local.project_id + network = local.network.name + name = "${var.name}-${each.key}" + description = each.value.description + dest_range = lookup( + local.ctx.cidr_ranges, each.value.dest_range, each.value.dest_range + ) priority = each.value.priority tags = each.value.tags next_hop_gateway = each.value.next_hop } resource "google_compute_route" "ilb" { - for_each = local.routes.ilb - project = var.project_id - network = local.network.name - name = "${var.name}-${each.key}" - description = each.value.description - dest_range = each.value.dest_range + for_each = local.routes.ilb + project = local.project_id + network = local.network.name + name = "${var.name}-${each.key}" + description = each.value.description + dest_range = lookup( + local.ctx.cidr_ranges, each.value.dest_range, each.value.dest_range + ) priority = each.value.priority tags = each.value.tags next_hop_ilb = each.value.next_hop } resource "google_compute_route" "instance" { - for_each = local.routes.instance - project = var.project_id - network = local.network.name - name = "${var.name}-${each.key}" - description = each.value.description - dest_range = each.value.dest_range + for_each = local.routes.instance + project = local.project_id + network = local.network.name + name = "${var.name}-${each.key}" + description = each.value.description + dest_range = lookup( + local.ctx.cidr_ranges, each.value.dest_range, each.value.dest_range + ) priority = each.value.priority tags = each.value.tags next_hop_instance = each.value.next_hop @@ -89,23 +95,29 @@ resource "google_compute_route" "instance" { resource "google_compute_route" "ip" { for_each = local.routes.ip - project = var.project_id + project = local.project_id network = local.network.name name = "${var.name}-${each.key}" description = each.value.description - dest_range = each.value.dest_range - priority = each.value.priority - tags = each.value.tags - next_hop_ip = each.value.next_hop + dest_range = lookup( + local.ctx.cidr_ranges, each.value.dest_range, each.value.dest_range + ) + priority = each.value.priority + tags = each.value.tags + next_hop_ip = lookup( + local.ctx.addresses, each.value.next_hop, each.value.next_hop + ) } resource "google_compute_route" "vpn_tunnel" { - for_each = local.routes.vpn_tunnel - project = var.project_id - network = local.network.name - name = "${var.name}-${each.key}" - description = each.value.description - dest_range = each.value.dest_range + for_each = local.routes.vpn_tunnel + project = local.project_id + network = local.network.name + name = "${var.name}-${each.key}" + description = each.value.description + dest_range = lookup( + local.ctx.cidr_ranges, each.value.dest_range, each.value.dest_range + ) priority = each.value.priority tags = each.value.tags next_hop_vpn_tunnel = each.value.next_hop @@ -113,7 +125,7 @@ resource "google_compute_route" "vpn_tunnel" { resource "google_network_connectivity_policy_based_route" "default" { for_each = var.policy_based_routes - project = var.project_id + project = local.project_id network = local.network.id name = "${var.name}-${each.key}" description = each.value.description diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf index 5d3ff70f0..6d6f5891e 100644 --- a/modules/net-vpc/subnets.tf +++ b/modules/net-vpc/subnets.tf @@ -23,7 +23,7 @@ locals { } _factory_data = { for k, v in local._factory_data_raw : k => merge(v, { - region_computed = lookup(local.ctx.regions, v.region, v.region) + region_computed = lookup(local.ctx.locations, v.region, v.region) }) } _factory_path = try(pathexpand(var.factories_config.subnets_folder), null) @@ -88,7 +88,9 @@ locals { _is_proxy_only = try(v.proxy_only == true, false) } } - + _iam_subnets = concat( + var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets) + ) all_subnets = merge( { for k, v in google_compute_subnetwork.subnetwork : k => v }, { for k, v in google_compute_subnetwork.proxy_only : k => v }, @@ -96,22 +98,21 @@ locals { ) subnet_iam = flatten(concat( [ - for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : [ - for role, members in s.iam : - { - role = role + for s in local._iam_subnets : [ + for role, members in s.iam : { + role = lookup(local.ctx.custom_roles, role, role) members = members - subnet = "${s.region}/${s.name}" + subnet = "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" } ] ], )) subnet_iam_bindings = merge([ - for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : { + for s in local._iam_subnets : { for key, data in s.iam_bindings : key => { - role = data.role - subnet = "${s.region}/${s.name}" + role = lookup(local.ctx.custom_roles, data.role, data.role) + subnet = "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" members = data.members condition = data.condition } @@ -121,42 +122,60 @@ locals { # In other words, if you have multiple additive bindings with the # same name, only one will be used subnet_iam_bindings_additive = merge([ - for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : { + for s in local._iam_subnets : { for key, data in s.iam_bindings_additive : key => { - role = data.role - subnet = "${s.region}/${s.name}" + role = lookup(local.ctx.custom_roles, data.role, data.role) + subnet = "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" member = data.member condition = data.condition } } ]...) subnets = merge( - { for s in var.subnets : "${s.region}/${s.name}" => s }, + { + for s in var.subnets : + "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" => s + }, { for k, v in local._factory_subnets : k => v if v._is_regular } ) subnets_proxy_only = merge( - { for s in var.subnets_proxy_only : "${s.region}/${s.name}" => s }, + { + for s in var.subnets_proxy_only : + "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" => s + }, { for k, v in local._factory_subnets : k => v if v._is_proxy_only }, ) subnets_private_nat = merge( - { for s in var.subnets_private_nat : "${s.region}/${s.name}" => s }, + { + for s in var.subnets_private_nat : + "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" => s + }, # { for k, v in local._factory_subnets : k => v if v._is_proxy_only }, ) subnets_psc = merge( - { for s in var.subnets_psc : "${s.region}/${s.name}" => s }, + { + for s in var.subnets_psc : + "${lookup(local.ctx.locations, s.region, s.region)}/${s.name}" => s + }, { for k, v in local._factory_subnets : k => v if v._is_psc } ) } resource "google_compute_subnetwork" "subnetwork" { - provider = google-beta - for_each = local.subnets - project = var.project_id - network = local.network.name - name = each.value.name - region = each.value.region - ip_cidr_range = try(each.value.ipv6.ipv6_only, false) ? null : each.value.ip_cidr_range + provider = google-beta + for_each = local.subnets + project = local.project_id + network = local.network.name + name = each.value.name + region = lookup( + local.ctx.locations, each.value.region, each.value.region + ) + ip_cidr_range = ( + try(each.value.ipv6.ipv6_only, false) + ? null + : try(local.ctx.cidr_ranges[each.value.ip_cidr_range], each.value.ip_cidr_range) + ) allow_subnet_cidr_routes_overlap = each.value.allow_subnet_cidr_routes_overlap reserved_internal_range = ( each.value.reserved_internal_range != null @@ -189,8 +208,13 @@ resource "google_compute_subnetwork" "subnetwork" { dynamic "secondary_ip_range" { for_each = each.value.secondary_ip_ranges == null ? {} : each.value.secondary_ip_ranges content { - range_name = secondary_ip_range.key - ip_cidr_range = try(secondary_ip_range.value.ip_cidr_range, secondary_ip_range.value) + range_name = secondary_ip_range.key + ip_cidr_range = try( + local.ctx.cidr_ranges[secondary_ip_range.value.ip_cidr_range], + secondary_ip_range.value.ip_cidr_range, + local.ctx.cidr_ranges[secondary_ip_range.value], + secondary_ip_range.value + ) reserved_internal_range = ( try(secondary_ip_range.value.reserved_internal_range, null) != null ? "networkconnectivity.googleapis.com/${try(local.internal_ranges_ids[secondary_ip_range.value.reserved_internal_range], secondary_ip_range.value.reserved_internal_range)}" @@ -215,12 +239,16 @@ resource "google_compute_subnetwork" "subnetwork" { } resource "google_compute_subnetwork" "proxy_only" { - for_each = local.subnets_proxy_only - project = var.project_id - network = local.network.name - name = each.value.name - region = each.value.region - ip_cidr_range = each.value.ip_cidr_range + for_each = local.subnets_proxy_only + project = local.project_id + network = local.network.name + name = each.value.name + region = lookup( + local.ctx.locations, each.value.region, each.value.region + ) + ip_cidr_range = lookup( + local.ctx.cidr_ranges, each.value.ip_cidr_range, each.value.ip_cidr_range + ) description = ( # Set description to an empty string (eg "") to create subnet without a description. each.value.description == null @@ -232,12 +260,16 @@ resource "google_compute_subnetwork" "proxy_only" { } resource "google_compute_subnetwork" "private_nat" { - for_each = local.subnets_private_nat - project = var.project_id - network = local.network.name - name = each.value.name - region = each.value.region - ip_cidr_range = each.value.ip_cidr_range + for_each = local.subnets_private_nat + project = local.project_id + network = local.network.name + name = each.value.name + region = lookup( + local.ctx.locations, each.value.region, each.value.region + ) + ip_cidr_range = lookup( + local.ctx.cidr_ranges, each.value.ip_cidr_range, each.value.ip_cidr_range + ) description = ( # Set description to an empty string (eg "") to create subnet without a description. each.value.description == null @@ -248,12 +280,16 @@ resource "google_compute_subnetwork" "private_nat" { } resource "google_compute_subnetwork" "psc" { - for_each = local.subnets_psc - project = var.project_id - network = local.network.name - name = each.value.name - region = each.value.region - ip_cidr_range = each.value.ip_cidr_range + for_each = local.subnets_psc + project = local.project_id + network = local.network.name + name = each.value.name + region = lookup( + local.ctx.locations, each.value.region, each.value.region + ) + ip_cidr_range = lookup( + local.ctx.cidr_ranges, each.value.ip_cidr_range, each.value.ip_cidr_range + ) description = ( # Set description to an empty string (eg "") to create subnet without a description. each.value.description == null @@ -269,24 +305,30 @@ resource "google_compute_subnetwork_iam_binding" "authoritative" { for binding in local.subnet_iam : "${binding.subnet}.${binding.role}" => binding } - project = var.project_id + project = local.project_id subnetwork = local.all_subnets[each.value.subnet].name region = local.all_subnets[each.value.subnet].region role = each.value.role - members = each.value.members + members = [ + for m in each.value.members : lookup(local.ctx.iam_principals, m, m) + ] } resource "google_compute_subnetwork_iam_binding" "bindings" { for_each = local.subnet_iam_bindings - project = var.project_id + project = local.project_id subnetwork = local.all_subnets[each.value.subnet].name region = local.all_subnets[each.value.subnet].region role = each.value.role - members = each.value.members + members = [ + for m in each.value.members : lookup(local.ctx.iam_principals, m, m) + ] dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } @@ -295,15 +337,19 @@ resource "google_compute_subnetwork_iam_binding" "bindings" { resource "google_compute_subnetwork_iam_member" "bindings" { for_each = local.subnet_iam_bindings_additive - project = var.project_id + project = local.project_id subnetwork = local.all_subnets[each.value.subnet].name region = local.all_subnets[each.value.subnet].region role = each.value.role - member = each.value.member + member = lookup( + local.ctx.iam_principals, each.value.member, each.value.member + ) dynamic "condition" { for_each = each.value.condition == null ? [] : [""] content { - expression = each.value.condition.expression + expression = templatestring( + each.value.condition.expression, var.context.condition_vars + ) title = each.value.condition.title description = each.value.condition.description } @@ -313,7 +359,7 @@ resource "google_compute_subnetwork_iam_member" "bindings" { resource "google_compute_network_attachment" "default" { provider = google-beta for_each = var.network_attachments - project = var.project_id + project = local.project_id region = google_compute_subnetwork.subnetwork[each.value.subnet].region name = each.key description = each.value.description diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf index 646a75212..1f7c2e02f 100644 --- a/modules/net-vpc/variables.tf +++ b/modules/net-vpc/variables.tf @@ -23,10 +23,23 @@ variable "auto_create_subnetworks" { variable "context" { description = "Context-specific interpolations." type = object({ - regions = optional(map(string), {}) + addresses = optional(map(string), {}) + cidr_ranges = optional(map(string), {}) + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + # legacy context + regions = optional(map(string), {}) + project_ids = optional(map(string), {}) }) default = {} nullable = false + validation { + condition = length(var.context.regions) == 0 || length(var.context.locations) == 0 + error_message = "Only one of locations, regions can be used." + } } variable "create_googleapis_routes" { diff --git a/tests/modules/net_lb_int/context.tfvars b/tests/modules/net_lb_int/context.tfvars new file mode 100644 index 000000000..c2aff4baf --- /dev/null +++ b/tests/modules/net_lb_int/context.tfvars @@ -0,0 +1,42 @@ +context = { + addresses = { + test = "10.0.0.10" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/gce" + test-nat = "projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/test-nat" + } + project_ids = { + test = "foo-test-0" + } +} +project_id = "$project_ids:test" +region = "$locations:ew8" +name = "test" +vpc_config = { + network = "$networks:test" + subnetwork = "$subnets:test" +} +backends = [{ + group = "foo" + failover = false +}] +forwarding_rules_config = { + "" = { + address = "$addresses:test" + } +} +service_attachments = { + "" = { + nat_subnets = ["$subnets:test-nat"] + } +} diff --git a/tests/modules/net_lb_int/context.yaml b/tests/modules/net_lb_int/context.yaml new file mode 100644 index 000000000..ceebc8374 --- /dev/null +++ b/tests/modules/net_lb_int/context.yaml @@ -0,0 +1,126 @@ +# 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: + google_compute_forwarding_rule.default[""]: + all_ports: true + allow_global_access: true + allow_psc_global_access: null + description: null + ip_address: 10.0.0.10 + ip_collection: null + ip_protocol: TCP + is_mirroring_collector: null + labels: null + load_balancing_scheme: INTERNAL + name: test + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + no_automate_dns_zone: null + ports: null + project: foo-test-0 + recreate_closed_psc: false + region: europe-west8 + service_label: null + source_ip_ranges: null + subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/gce + target: null + timeouts: null + google_compute_health_check.default[0]: + check_interval_sec: 5 + description: Terraform managed. + grpc_health_check: [] + grpc_tls_health_check: [] + healthy_threshold: 2 + http2_health_check: [] + http_health_check: [] + https_health_check: [] + name: test + project: foo-test-0 + source_regions: null + ssl_health_check: [] + tcp_health_check: + - port: null + port_name: null + port_specification: USE_SERVING_PORT + proxy_header: NONE + request: null + response: null + timeout_sec: 5 + timeouts: null + unhealthy_threshold: 2 + google_compute_region_backend_service.default: + affinity_cookie_ttl_sec: null + backend: + - balancing_mode: CONNECTION + capacity_scaler: null + custom_metrics: [] + description: Terraform managed. + failover: false + group: foo + max_connections: null + max_connections_per_endpoint: null + max_connections_per_instance: null + max_rate: null + max_rate_per_endpoint: null + max_rate_per_instance: null + max_utilization: null + circuit_breakers: [] + connection_draining_timeout_sec: 300 + connection_tracking_policy: [] + consistent_hash: [] + custom_metrics: [] + description: Terraform managed. + dynamic_forwarding: [] + enable_cdn: null + failover_policy: [] + ha_policy: [] + iap: + - enabled: false + oauth2_client_id: null + oauth2_client_secret: null + ip_address_selection_policy: null + load_balancing_scheme: INTERNAL + locality_lb_policy: null + name: test + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + outlier_detection: [] + project: foo-test-0 + protocol: UNSPECIFIED + region: europe-west8 + security_policy: null + strong_session_affinity_cookie: [] + subsetting: [] + timeouts: null + google_compute_service_attachment.default[""]: + connection_preference: ACCEPT_MANUAL + consumer_accept_lists: [] + consumer_reject_lists: null + description: Terraform managed. + domain_names: null + enable_proxy_protocol: false + name: test + nat_subnets: + - projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/test-nat + project: foo-test-0 + region: europe-west8 + send_propagated_connection_limit_if_zero: false + timeouts: null + +counts: + google_compute_forwarding_rule: 1 + google_compute_health_check: 1 + google_compute_region_backend_service: 1 + google_compute_service_attachment: 1 + modules: 0 + resources: 4 diff --git a/tests/modules/net_lb_int/tftest.yaml b/tests/modules/net_lb_int/tftest.yaml index 5cc14ee22..0837391d7 100644 --- a/tests/modules/net_lb_int/tftest.yaml +++ b/tests/modules/net_lb_int/tftest.yaml @@ -15,5 +15,6 @@ module: modules/net-lb-int tests: + context: defaults: forwarding-rule: diff --git a/tests/modules/net_vpc/context.tfvars b/tests/modules/net_vpc/context.tfvars new file mode 100644 index 000000000..c54ff5fef --- /dev/null +++ b/tests/modules/net_vpc/context.tfvars @@ -0,0 +1,96 @@ +context = { + addresses = { + dns-external = "8.8.8.8" + dns-internal = "10.10.10.10" + test = "10.20.20.20" + } + cidr_ranges = { + rfc1918-10 = "10.0.0.0/8" + rfc1918-172 = "172.16.10.0/12" + rfc1918-192 = "192.168.0.0/16" + test = "8.8.8.8/32" + } + condition_vars = { + organization = { + id = 1234567890 + } + } + custom_roles = { + myrole = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + test = "serviceAccount:test@test-project.iam.gserviceaccount.com" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } +} +dns_policy = { + inbound = true + outbound = { + private_ns = ["$addresses:dns-internal"] + public_ns = ["$addresses:dns-external"] + } +} +internal_ranges = [ + { + name = "pods-range" + usage = "FOR_VPC" + peering = "FOR_SELF" + ip_cidr_range = "$cidr_ranges:rfc1918-172" + } +] +project_id = "$project_ids:test" +routes = { + next-hop = { + description = "Route to internal range." + dest_range = "$cidr_ranges:test" + next_hop_type = "ip" + next_hop = "$addresses:test" + } +} +subnets = [ + { + name = "production" + region = "$locations:ew8" + reserved_internal_range = "pods-range" + iam = { + "$custom_roles:myrole" = [ + "iam_principals:test" + ] + } + iam_bindings = { + myrole_two = { + role = "$custom_roles:myrole" + members = [ + "$iam_principals:test" + ] + condition = { + title = "Test" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } + } + iam_bindings_additive = { + myrole_two = { + role = "$custom_roles:myrole" + member = "$iam_principals:test" + } + } + secondary_ip_ranges = { + pods = { + reserved_internal_range = "pods-range" + } + # Mixed configuration: some ranges use internal ranges, others use CIDR + traditional = { + ip_cidr_range = "$cidr_ranges:rfc1918-192" + } + } + } +] diff --git a/tests/modules/net_vpc/context.yaml b/tests/modules/net_vpc/context.yaml new file mode 100644 index 000000000..36beb5c4c --- /dev/null +++ b/tests/modules/net_vpc/context.yaml @@ -0,0 +1,175 @@ +# 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: + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: test + network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL + network_profile: null + params: [] + project: foo-test-0 + routing_mode: GLOBAL + timeouts: null + google_compute_route.gateway["directpath-googleapis"]: + description: Terraform-managed. + dest_range: 34.126.0.0/18 + name: test-directpath-googleapis + network: test + next_hop_gateway: default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + params: [] + priority: 1000 + project: foo-test-0 + tags: null + timeouts: null + google_compute_route.gateway["private-googleapis"]: + description: Terraform-managed. + dest_range: 199.36.153.8/30 + name: test-private-googleapis + network: test + next_hop_gateway: default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + params: [] + priority: 1000 + project: foo-test-0 + tags: null + timeouts: null + google_compute_route.gateway["restricted-googleapis"]: + description: Terraform-managed. + dest_range: 199.36.153.4/30 + name: test-restricted-googleapis + network: test + next_hop_gateway: default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + params: [] + priority: 1000 + project: foo-test-0 + tags: null + timeouts: null + google_compute_route.ip["next-hop"]: + description: Route to internal range. + dest_range: 8.8.8.8/32 + name: test-next-hop + network: test + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: null + next_hop_ip: 10.20.20.20 + next_hop_vpn_tunnel: null + params: [] + priority: 1000 + project: foo-test-0 + tags: null + timeouts: null + google_compute_subnetwork.subnetwork["europe-west8/production"]: + description: Terraform-managed. + ip_collection: null + ipv6_access_type: null + log_config: [] + name: production + network: test + params: [] + private_ip_google_access: true + project: foo-test-0 + region: europe-west8 + role: null + secondary_ip_range: + - range_name: pods + - ip_cidr_range: 192.168.0.0/16 + range_name: traditional + reserved_internal_range: null + send_secondary_ip_range_if_empty: true + timeouts: null + google_compute_subnetwork_iam_binding.authoritative["europe-west8/production.organizations/366118655033/roles/myRoleOne"]: + condition: [] + members: + - iam_principals:test + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + subnetwork: production + google_compute_subnetwork_iam_binding.bindings["myrole_two"]: + condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + title: Test + members: + - serviceAccount:test@test-project.iam.gserviceaccount.com + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + subnetwork: production + google_compute_subnetwork_iam_member.bindings["myrole_two"]: + condition: [] + member: serviceAccount:test@test-project.iam.gserviceaccount.com + project: foo-test-0 + region: europe-west8 + role: organizations/366118655033/roles/myRoleOne + subnetwork: production + google_dns_policy.default[0]: + alternative_name_server_config: + - target_name_servers: + - forwarding_path: '' + ipv4_address: 8.8.8.8 + - forwarding_path: private + ipv4_address: 10.10.10.10 + description: Managed by Terraform + enable_inbound_forwarding: true + enable_logging: null + name: test + networks: + - {} + project: foo-test-0 + timeouts: null + google_network_connectivity_internal_range.internal_range["pods-range"]: + allocation_options: [] + description: null + effective_labels: + goog-terraform-provisioned: 'true' + exclude_cidr_ranges: null + immutable: null + ip_cidr_range: 172.16.10.0/12 + labels: null + migration: [] + name: pods-range + overlaps: null + peering: FOR_SELF + prefix_length: null + project: foo-test-0 + target_cidr_range: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + usage: FOR_VPC + +counts: + google_compute_network: 1 + google_compute_route: 4 + google_compute_subnetwork: 1 + google_compute_subnetwork_iam_binding: 2 + google_compute_subnetwork_iam_member: 1 + google_dns_policy: 1 + google_network_connectivity_internal_range: 1 + modules: 0 + resources: 11 diff --git a/tests/modules/net_vpc/tftest.yaml b/tests/modules/net_vpc/tftest.yaml index 5e9668ea4..a8cfbe17b 100644 --- a/tests/modules/net_vpc/tftest.yaml +++ b/tests/modules/net_vpc/tftest.yaml @@ -17,6 +17,7 @@ common_tfvars: - common.tfvars tests: + context: shared_vpc: psa_routes_export: psa_routes_import: diff --git a/tests/modules/net_vpc_firewall/context.tfvars b/tests/modules/net_vpc_firewall/context.tfvars new file mode 100644 index 000000000..782617fe3 --- /dev/null +++ b/tests/modules/net_vpc_firewall/context.tfvars @@ -0,0 +1,47 @@ +context = { + cidr_ranges = { + rfc1918-10 = "10.0.0.0/8" + } + iam_principals = { + test = "serviceAccount:test@test-project.iam.gserviceaccount.com" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } +} +project_id = "$project_ids:test" +network = "$networks:test" +attachments = { + test = "$networks:test" +} +default_rules_config = { + admin_ranges = ["$cidr_ranges:rfc1918-10"] + http_ranges = ["$cidr_ranges:rfc1918-10"] + https_ranges = ["$cidr_ranges:rfc1918-10"] + ssh_ranges = ["$cidr_ranges:rfc1918-10"] +} +egress_rules = { + allow-egress-rfc1918 = { + deny = false + description = "Allow egress." + destination_ranges = [ + "$cidr_ranges:rfc1918-10", "172.16.0.0/12", "192.168.0.0/16" + ] + source_ranges = ["$cidr_ranges:rfc1918-10"] + targets = ["$iam_principals:test"] + use_service_accounts = true + } +} +ingress_rules = { + allow-ingress-tag = { + description = "Allow ingress." + destination_ranges = ["$cidr_ranges:rfc1918-10"] + source_ranges = ["$cidr_ranges:rfc1918-10"] + sources = ["$iam_principals:test"] + targets = ["$iam_principals:test"] + use_service_accounts = true + } +} diff --git a/tests/modules/net_vpc_firewall/context.yaml b/tests/modules/net_vpc_firewall/context.yaml new file mode 100644 index 000000000..2ad022a5a --- /dev/null +++ b/tests/modules/net_vpc_firewall/context.yaml @@ -0,0 +1,157 @@ +# 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: + google_compute_firewall.allow-admins[0]: + allow: + - ports: [] + protocol: all + deny: [] + description: Access from the admin subnet to all subnets. + disabled: null + log_config: [] + name: dev-spoke-0-ingress-admins + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: null + source_tags: null + target_service_accounts: null + target_tags: null + timeouts: null + google_compute_firewall.allow-tag-http[0]: + allow: + - ports: + - '80' + protocol: tcp + deny: [] + description: Allow http to machines with matching tags. + disabled: null + log_config: [] + name: dev-spoke-0-ingress-tag-http + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: null + source_tags: null + target_service_accounts: null + target_tags: + - http-server + timeouts: null + google_compute_firewall.allow-tag-https[0]: + allow: + - ports: + - '443' + protocol: tcp + deny: [] + description: Allow http to machines with matching tags. + disabled: null + log_config: [] + name: dev-spoke-0-ingress-tag-https + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: null + source_tags: null + target_service_accounts: null + target_tags: + - https-server + timeouts: null + google_compute_firewall.allow-tag-ssh[0]: + allow: + - ports: + - '22' + protocol: tcp + deny: [] + description: Allow SSH to machines with matching tags. + disabled: null + log_config: [] + name: dev-spoke-0-ingress-tag-ssh + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: null + source_tags: null + target_service_accounts: null + target_tags: + - ssh + timeouts: null + google_compute_firewall.custom-rules["allow-egress-rfc1918"]: + allow: + - ports: [] + protocol: all + deny: [] + description: Allow egress. + destination_ranges: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + direction: EGRESS + disabled: false + log_config: [] + name: allow-egress-rfc1918 + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: null + source_tags: null + target_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + target_tags: null + timeouts: null + google_compute_firewall.custom-rules["allow-ingress-tag"]: + allow: + - ports: [] + protocol: all + deny: [] + description: Allow ingress. + destination_ranges: + - 10.0.0.0/8 + direction: INGRESS + disabled: false + log_config: [] + name: allow-ingress-tag + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + params: [] + priority: 1000 + project: foo-test-0 + source_ranges: + - 10.0.0.0/8 + source_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + source_tags: null + target_service_accounts: + - serviceAccount:test@test-project.iam.gserviceaccount.com + target_tags: null + timeouts: null + +counts: + google_compute_firewall: 6 + modules: 0 + resources: 6 diff --git a/tests/modules/net_vpc_firewall/tftest.yaml b/tests/modules/net_vpc_firewall/tftest.yaml new file mode 100644 index 000000000..5223867ff --- /dev/null +++ b/tests/modules/net_vpc_firewall/tftest.yaml @@ -0,0 +1,17 @@ +# 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. + +module: modules/net-vpc-firewall +tests: + context: From ccecb0dd24fbed7425f872815b41a64d2cd61ad0 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 16 Oct 2025 05:56:41 +0000 Subject: [PATCH 13/13] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c7d7e2..f90d93d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,12 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#3419](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3419)] Add support for context to net-lb-int net-vpc-firewall and net-vpc module ([ludoo](https://github.com/ludoo)) +- [[#3414](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3414)] Add support for context to net-cloudnat, net-firewall-policy modules ([ludoo](https://github.com/ludoo)) +- [[#3413](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3413)] Make SSM gitignores a list ([juliocc](https://github.com/juliocc)) +- [[#3412](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3412)] Add support for context to DNS modules ([ludoo](https://github.com/ludoo)) +- [[#3403](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3403)] modules fixes for E2E tests ([wiktorn](https://github.com/wiktorn)) +- [[#3406](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3406)] Add support for contexts to compute-vm module ([ludoo](https://github.com/ludoo)) - [[#3332](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3332)] Update gke-hub module to use new Policy Controller API ([juliocc](https://github.com/juliocc)) - [[#3402](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3402)] Fix incorrect cloudservices agent email for global universe in project module ([ludoo](https://github.com/ludoo)) - [[#3388](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3388)] Add support for context to bigquery module ([ludoo](https://github.com/ludoo)) @@ -56,6 +62,7 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#3407](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3407)] remove tf version from matrix, to keep workflow names stable across upgrades ([wiktorn](https://github.com/wiktorn)) - [[#3332](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3332)] Update gke-hub module to use new Policy Controller API ([juliocc](https://github.com/juliocc)) - [[#3404](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/3404)] Add tests for service agents iam_emails ([wiktorn](https://github.com/wiktorn))