From dd213ea9a9df1f4c1d8b5bd64bfc79d7284262cd Mon Sep 17 00:00:00 2001 From: Luca Prete Date: Tue, 23 Apr 2024 15:19:38 +0200 Subject: [PATCH 01/29] Fix permissions for branch network dev - read sa (#2233) Co-authored-by: Luca Prete --- fast/stages/1-resman/branch-networking.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fast/stages/1-resman/branch-networking.tf b/fast/stages/1-resman/branch-networking.tf index e57a874ff6..e886234bdb 100644 --- a/fast/stages/1-resman/branch-networking.tf +++ b/fast/stages/1-resman/branch-networking.tf @@ -101,10 +101,10 @@ module "branch-network-dev-folder" { ) # read-only (plan) automation service accounts "roles/compute.networkViewer" = concat( - local.branch_optional_r_sa_lists.dp-prod, - local.branch_optional_r_sa_lists.gke-prod, + local.branch_optional_r_sa_lists.dp-dev, + local.branch_optional_r_sa_lists.gke-dev, local.branch_optional_r_sa_lists.gcve-dev, - local.branch_optional_r_sa_lists.pf-prod, + local.branch_optional_r_sa_lists.pf-dev, ) (local.custom_roles.gcve_network_admin) = local.branch_optional_sa_lists.gcve-dev } From d9019926076cbaec824d8aeee47824d073d2f26a Mon Sep 17 00:00:00 2001 From: luigi-bitonti <93377317+luigi-bitonti@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:20:38 +0200 Subject: [PATCH 02/29] Added build env vars in cloud function v1 (#2234) --- modules/cloud-function-v1/README.md | 39 +++++++++++++------------- modules/cloud-function-v1/main.tf | 5 ++-- modules/cloud-function-v1/variables.tf | 6 ++++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 2d52842bb8..7099946e2d 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -264,26 +264,27 @@ module "cf-http" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [bucket_name](variables.tf#L26) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string | ✓ | | -| [bundle_config](variables.tf#L38) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…}) | ✓ | | -| [name](variables.tf#L103) | Name used for cloud function and associated resources. | string | ✓ | | -| [project_id](variables.tf#L118) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L123) | Region used for all resources. | string | ✓ | | +| [bundle_config](variables.tf#L44) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…}) | ✓ | | +| [name](variables.tf#L109) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L124) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L129) | Region used for all resources. | string | ✓ | | | [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) | | null | -| [build_worker_pool](variables.tf#L32) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | -| [description](variables.tf#L47) | Optional description. | string | | "Terraform managed." | -| [environment_variables](variables.tf#L53) | Cloud function environment variables. | map(string) | | {} | -| [function_config](variables.tf#L59) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | -| [https_security_level](variables.tf#L79) | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string | | null | -| [iam](variables.tf#L85) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [ingress_settings](variables.tf#L91) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | -| [labels](variables.tf#L97) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L108) | Optional prefix used for resource names. | string | | null | -| [secrets](variables.tf#L128) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | -| [service_account](variables.tf#L140) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L146) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L152) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L162) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L172) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [build_environment_variables](variables.tf#L32) | A set of key/value environment variable pairs available during build time. | map(string) | | {} | +| [build_worker_pool](variables.tf#L38) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | +| [description](variables.tf#L53) | Optional description. | string | | "Terraform managed." | +| [environment_variables](variables.tf#L59) | Cloud function environment variables. | map(string) | | {} | +| [function_config](variables.tf#L65) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | +| [https_security_level](variables.tf#L85) | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string | | null | +| [iam](variables.tf#L91) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [ingress_settings](variables.tf#L97) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | +| [labels](variables.tf#L103) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L114) | Optional prefix used for resource names. | string | | null | +| [secrets](variables.tf#L134) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account](variables.tf#L146) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L152) | Auto-create service account. | bool | | false | +| [trigger_config](variables.tf#L158) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L168) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L178) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index 8a9af6df6c..b57d8431cb 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -68,8 +68,9 @@ resource "google_cloudfunctions_function" "function" { trigger_http = var.trigger_config == null ? true : null https_trigger_security_level = var.https_security_level == null ? "SECURE_ALWAYS" : var.https_security_level - ingress_settings = var.ingress_settings - build_worker_pool = var.build_worker_pool + ingress_settings = var.ingress_settings + build_worker_pool = var.build_worker_pool + build_environment_variables = var.build_environment_variables vpc_connector = local.vpc_connector vpc_connector_egress_settings = try( diff --git a/modules/cloud-function-v1/variables.tf b/modules/cloud-function-v1/variables.tf index 567a8c7a7d..73964d0b18 100644 --- a/modules/cloud-function-v1/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -29,6 +29,12 @@ variable "bucket_name" { nullable = false } +variable "build_environment_variables" { + description = "A set of key/value environment variable pairs available during build time." + type = map(string) + default = {} +} + variable "build_worker_pool" { description = "Build worker pool, in projects//locations//workerPools/ format." type = string From 99129d54a37da4c2d977b7db705de5024d530944 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 25 Apr 2024 09:31:51 +0300 Subject: [PATCH 03/29] Update FAST logging (#2235) * Update FAST logging * Fix readme * Fix tests --- fast/stages/0-bootstrap/README.md | 20 +++++++++------- fast/stages/0-bootstrap/automation.tf | 18 ++++++++++++++ fast/stages/0-bootstrap/variables.tf | 24 ++++++++++++++++--- tests/fast/stages/s0_bootstrap/checklist.yaml | 10 ++++---- tests/fast/stages/s0_bootstrap/simple.yaml | 10 ++++---- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index 83031f5d2a..ee5c7f52e7 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -138,7 +138,9 @@ Because of limitations of API availability, manual steps have to be followed to ### Organization-level logging -We create organization-level log sinks early in the bootstrap process to ensure a proper audit trail is in place from the very beginning. By default, we provide log filters to capture [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit), [VPC Service Controls violations](https://cloud.google.com/vpc-service-controls/docs/troubleshooting#vpc-sc-errors) and [Workspace Logs](https://cloud.google.com/logging/docs/audit/configure-gsuite-audit-logs) into logging buckets in the top-level audit logging project. +We create organization-level log sinks early in the bootstrap process to ensure a proper audit trail is in place from the very beginning. By default, we provide log filters to capture [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit), [VPC Service Controls violations](https://cloud.google.com/vpc-service-controls/docs/troubleshooting#vpc-sc-errors) and [Workspace Logs](https://cloud.google.com/logging/docs/audit/configure-gsuite-audit-logs) into logging buckets in the top-level audit logging project. + +An organization-level sink captures IAM data access logs, including authentication and impersonation events for service accounts. To manage logging costs, the default configuration enables IAM data access logging only within the automation project (where sensitive service accounts reside). For enhanced security across the entire organization, consider enabling these logs at the organization level. The [Customizations](#log-sinks-and-log-destinations) section explains how to change the logs captured and their destination. @@ -626,8 +628,8 @@ The `fast_features` variable consists of 4 toggles: | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | | -| [organization](variables.tf#L223) | Organization details. | object({…}) | ✓ | | | -| [prefix](variables.tf#L238) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | +| [organization](variables.tf#L241) | Organization details. | object({…}) | ✓ | | | +| [prefix](variables.tf#L256) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | | [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | string | | null | | | [cicd_repositories](variables.tf#L33) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | | [custom_roles](variables.tf#L79) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string)) | | {} | | @@ -639,12 +641,12 @@ The `fast_features` variable consists of 4 toggles: | [iam_bindings_additive](variables.tf#L141) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | | [iam_by_principals](variables.tf#L156) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | | [locations](variables.tf#L163) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | | -| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | -| [org_policies_config](variables.tf#L206) | Organization policies customization. | object({…}) | | {} | | -| [outputs_location](variables.tf#L232) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [project_parent_ids](variables.tf#L247) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {} | | -| [workforce_identity_providers](variables.tf#L258) | Workforce Identity Federation pools. | map(object({…})) | | {} | | -| [workload_identity_providers](variables.tf#L274) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [org_policies_config](variables.tf#L224) | Organization policies customization. | object({…}) | | {} | | +| [outputs_location](variables.tf#L250) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L265) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {} | | +| [workforce_identity_providers](variables.tf#L276) | Workforce Identity Federation pools. | map(object({…})) | | {} | | +| [workload_identity_providers](variables.tf#L292) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | ## Outputs diff --git a/fast/stages/0-bootstrap/automation.tf b/fast/stages/0-bootstrap/automation.tf index 6200deab38..8463ff16b6 100644 --- a/fast/stages/0-bootstrap/automation.tf +++ b/fast/stages/0-bootstrap/automation.tf @@ -156,6 +156,24 @@ module "automation-project" { "container.googleapis.com", ] ) + + # Enable IAM data access logs to capture impersonation and service + # account token generation events. This is implemented within the + # automation project to limit log volume. For heightened security, + # consider enabling it at the organization level. A log sink within + # the organization will collect and store these logs in a logging + # bucket. See + # https://cloud.google.com/iam/docs/audit-logging#audited_operations + logging_data_access = { + "iam.googleapis.com" = { + # ADMIN_READ captures impersonation and token generation/exchanges + ADMIN_READ = [] + # enable DATA_WRITE if you want to capture configuration changes + # to IAM-related resources (roles, deny policies, service + # accounts, identity pools, etc) + # DATA_WRITE = [] + } + } } # output files bucket diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf index 64978dc30f..b769f1088f 100644 --- a/fast/stages/0-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -182,15 +182,33 @@ variable "log_sinks" { })) default = { audit-logs = { - filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\" OR protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.TransparencyLog\"" + filter = <<-FILTER + log_id("cloudaudit.googleapis.com/activity") OR + log_id("cloudaudit.googleapis.com/system_event") OR + log_id("cloudaudit.googleapis.com/policy") OR + log_id("cloudaudit.googleapis.com/access_transparency") + FILTER + type = "logging" + } + iam = { + filter = <<-FILTER + protoPayload.serviceName="iamcredentials.googleapis.com" OR + protoPayload.serviceName="iam.googleapis.com" OR + protoPayload.serviceName="sts.googleapis.com" + FILTER type = "logging" } vpc-sc = { - filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" + filter = <<-FILTER + protoPayload.metadata.@type:"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" + FILTER type = "logging" } workspace-audit-logs = { - filter = "logName:\"/logs/cloudaudit.googleapis.com%2Fdata_access\" and protoPayload.serviceName:\"login.googleapis.com\"" + filter = <<-FILTER + log_id("cloudaudit.googleapis.com/data_access") + protoPayload.serviceName:"login.googleapis.com" + FILTER type = "logging" } } diff --git a/tests/fast/stages/s0_bootstrap/checklist.yaml b/tests/fast/stages/s0_bootstrap/checklist.yaml index 91fe60eb76..8c24a1fdfe 100644 --- a/tests/fast/stages/s0_bootstrap/checklist.yaml +++ b/tests/fast/stages/s0_bootstrap/checklist.yaml @@ -360,15 +360,15 @@ counts: google_bigquery_dataset: 1 google_bigquery_default_service_account: 3 google_essential_contacts_contact: 3 - google_logging_organization_sink: 3 - google_logging_project_bucket_config: 3 + google_logging_organization_sink: 4 + google_logging_project_bucket_config: 4 google_org_policy_policy: 22 google_organization_iam_binding: 27 google_organization_iam_custom_role: 7 google_organization_iam_member: 35 google_project: 3 google_project_iam_binding: 19 - google_project_iam_member: 6 + google_project_iam_member: 7 google_project_service: 31 google_project_service_identity: 4 google_service_account: 4 @@ -380,5 +380,5 @@ counts: google_storage_project_service_account: 3 google_tags_tag_key: 1 google_tags_tag_value: 1 - modules: 17 - resources: 198 + modules: 18 + resources: 202 diff --git a/tests/fast/stages/s0_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml index 904471a061..69908ad358 100644 --- a/tests/fast/stages/s0_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -39,15 +39,15 @@ counts: google_bigquery_dataset: 1 google_bigquery_default_service_account: 3 google_essential_contacts_contact: 3 - google_logging_organization_sink: 3 - google_logging_project_bucket_config: 3 + google_logging_organization_sink: 4 + google_logging_project_bucket_config: 4 google_org_policy_policy: 22 google_organization_iam_binding: 27 google_organization_iam_custom_role: 7 google_organization_iam_member: 22 google_project: 3 google_project_iam_binding: 19 - google_project_iam_member: 6 + google_project_iam_member: 7 google_project_service: 31 google_project_service_identity: 4 google_service_account: 4 @@ -60,8 +60,8 @@ counts: google_tags_tag_key: 1 google_tags_tag_value: 1 local_file: 7 - modules: 16 - resources: 189 + modules: 17 + resources: 193 outputs: custom_roles: From 2446b4dd7c48f4c118514f55d31bb1a4398b17ac Mon Sep 17 00:00:00 2001 From: Vince Gonzalez Date: Thu, 25 Apr 2024 19:14:32 -0400 Subject: [PATCH 04/29] Update README.md (#2239) --- blueprints/data-solutions/data-playground/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprints/data-solutions/data-playground/README.md b/blueprints/data-solutions/data-playground/README.md index a2eceeba91..ef490ab27c 100644 --- a/blueprints/data-solutions/data-playground/README.md +++ b/blueprints/data-solutions/data-playground/README.md @@ -1,6 +1,6 @@ # Data Playground -This blueprint creates a minimum viable architecture for a data experimentation project with the needed APIs enabled, VPC and Firewall set in place, BigQuesy dataset, GCS bucket and an AI notebook to get started. +This blueprint creates a minimum viable architecture for a data experimentation project with the needed APIs enabled, VPC and Firewall set in place, BigQuery dataset, GCS bucket and an AI notebook to get started. This is the high level diagram: From 64ac89d59c18e3b6b4034b3fd521cf60e33dcaba Mon Sep 17 00:00:00 2001 From: Deepak Kumar <21131061+kumadee@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:17:48 +0200 Subject: [PATCH 05/29] fix: allow disabling node autoprovisioning (#2238) - This fix allows a GKE Standard cluster to be configured with no auto-provisioned node pool, but allow setting autocluster profile for user-provisioned node pools like created via `gke-nodepool` module. Co-authored-by: Julio Castillo --- modules/gke-cluster-standard/README.md | 42 +++++++++++------------ modules/gke-cluster-standard/main.tf | 2 +- modules/gke-cluster-standard/variables.tf | 1 + 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/modules/gke-cluster-standard/README.md b/modules/gke-cluster-standard/README.md index 286b20a3dc..c53017267f 100644 --- a/modules/gke-cluster-standard/README.md +++ b/modules/gke-cluster-standard/README.md @@ -310,28 +310,28 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L233) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L368) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L404) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L415) | VPC-level configuration. | object({…}) | ✓ | | +| [location](variables.tf#L234) | Cluster zone or region. | string | ✓ | | +| [name](variables.tf#L369) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L405) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L416) | VPC-level configuration. | object({…}) | ✓ | | | [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | -| [cluster_autoscaling](variables.tf#L38) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | -| [default_nodepool](variables.tf#L116) | Enable default nodepool. | object({…}) | | {} | -| [deletion_protection](variables.tf#L134) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | -| [description](variables.tf#L141) | Cluster description. | string | | null | -| [enable_addons](variables.tf#L147) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | -| [enable_features](variables.tf#L171) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | -| [issue_client_certificate](variables.tf#L221) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L227) | Cluster resource labels. | map(string) | | null | -| [logging_config](variables.tf#L238) | Logging configuration. | object({…}) | | {} | -| [maintenance_config](variables.tf#L259) | Maintenance window configuration. | object({…}) | | {…} | -| [max_pods_per_node](variables.tf#L282) | Maximum number of pods per node in this cluster. | number | | 110 | -| [min_master_version](variables.tf#L288) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L294) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_config](variables.tf#L373) | Node-level configuration. | object({…}) | | {} | -| [node_locations](variables.tf#L383) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L390) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L409) | Release channel for GKE upgrades. | string | | null | +| [cluster_autoscaling](variables.tf#L38) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | +| [default_nodepool](variables.tf#L117) | Enable default nodepool. | object({…}) | | {} | +| [deletion_protection](variables.tf#L135) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | +| [description](variables.tf#L142) | Cluster description. | string | | null | +| [enable_addons](variables.tf#L148) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | +| [enable_features](variables.tf#L172) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | +| [issue_client_certificate](variables.tf#L222) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L228) | Cluster resource labels. | map(string) | | null | +| [logging_config](variables.tf#L239) | Logging configuration. | object({…}) | | {} | +| [maintenance_config](variables.tf#L260) | Maintenance window configuration. | object({…}) | | {…} | +| [max_pods_per_node](variables.tf#L283) | Maximum number of pods per node in this cluster. | number | | 110 | +| [min_master_version](variables.tf#L289) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L295) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_config](variables.tf#L374) | Node-level configuration. | object({…}) | | {} | +| [node_locations](variables.tf#L384) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L391) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L410) | Release channel for GKE upgrades. | string | | null | ## Outputs diff --git a/modules/gke-cluster-standard/main.tf b/modules/gke-cluster-standard/main.tf index 6ab4c9d881..99db49f80d 100644 --- a/modules/gke-cluster-standard/main.tf +++ b/modules/gke-cluster-standard/main.tf @@ -132,7 +132,7 @@ resource "google_container_cluster" "cluster" { dynamic "cluster_autoscaling" { for_each = local.cas == null ? [] : [""] content { - enabled = true + enabled = var.cluster_autoscaling.enabled autoscaling_profile = var.cluster_autoscaling.autoscaling_profile dynamic "auto_provisioning_defaults" { for_each = local.cas_apd != null ? [""] : [] diff --git a/modules/gke-cluster-standard/variables.tf b/modules/gke-cluster-standard/variables.tf index 6644595494..5580320f8f 100644 --- a/modules/gke-cluster-standard/variables.tf +++ b/modules/gke-cluster-standard/variables.tf @@ -38,6 +38,7 @@ variable "backup_configs" { variable "cluster_autoscaling" { description = "Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler." type = object({ + enabled = optional(bool, true) autoscaling_profile = optional(string, "BALANCED") auto_provisioning_defaults = optional(object({ boot_disk_kms_key = optional(string) From d831d328648b1a539215360f91f4e0f9f3c4e1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sat, 27 Apr 2024 07:07:04 +0000 Subject: [PATCH 06/29] Use default labels on pubsub subscription when no override is provided --- modules/pubsub/README.md | 1 + modules/pubsub/main.tf | 2 +- tests/modules/pubsub/examples/subscriptions.yaml | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/pubsub/README.md b/modules/pubsub/README.md index 6d924d90d2..4aaa4a4812 100644 --- a/modules/pubsub/README.md +++ b/modules/pubsub/README.md @@ -60,6 +60,7 @@ module "pubsub" { source = "./fabric/modules/pubsub" project_id = var.project_id name = "my-topic" + labels = { test = "default" } subscriptions = { test-pull = {} test-pull-override = { diff --git a/modules/pubsub/main.tf b/modules/pubsub/main.tf index b65d372f11..a090f861b0 100644 --- a/modules/pubsub/main.tf +++ b/modules/pubsub/main.tf @@ -54,7 +54,7 @@ resource "google_pubsub_subscription" "default" { project = var.project_id name = each.key topic = google_pubsub_topic.default.name - labels = each.value.labels + labels = coalesce(each.value.labels, var.labels) ack_deadline_seconds = each.value.ack_deadline_seconds message_retention_duration = each.value.message_retention_duration retain_acked_messages = each.value.retain_acked_messages diff --git a/tests/modules/pubsub/examples/subscriptions.yaml b/tests/modules/pubsub/examples/subscriptions.yaml index 89acf08b22..267f4ebc5c 100644 --- a/tests/modules/pubsub/examples/subscriptions.yaml +++ b/tests/modules/pubsub/examples/subscriptions.yaml @@ -20,7 +20,8 @@ values: enable_exactly_once_delivery: false enable_message_ordering: false filter: null - labels: null + labels: + test: default message_retention_duration: 604800s name: test-pull project: project-id @@ -52,7 +53,8 @@ values: topic: my-topic module.pubsub.google_pubsub_topic.default: kms_key_name: null - labels: null + labels: + test: default message_retention_duration: null name: my-topic project: project-id From a95e681f059af9e112636befd03efb124439115f Mon Sep 17 00:00:00 2001 From: apichick Date: Sun, 28 Apr 2024 12:11:07 +0200 Subject: [PATCH 07/29] Removed BFD settings from net-vpn-ha module as it is not supported (#2244) * Removed bfd settings from net-vpn-ha as it is not supported * Removed bfd settings from net-vpn-ha as it is not supported --- modules/net-vpn-ha/README.md | 6 +++--- modules/net-vpn-ha/main.tf | 9 --------- modules/net-vpn-ha/variables.tf | 6 ------ 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/modules/net-vpn-ha/README.md b/modules/net-vpn-ha/README.md index 2de72f54ea..164e009f7e 100644 --- a/modules/net-vpn-ha/README.md +++ b/modules/net-vpn-ha/README.md @@ -213,9 +213,9 @@ module "vpn_ha" { | [region](variables.tf#L52) | Region used for resources. | string | ✓ | | | [router_config](variables.tf#L57) | Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router. | object({…}) | ✓ | | | [peer_gateways](variables.tf#L27) | Configuration of the (external or GCP) peer gateway. | map(object({…})) | | {} | -| [tunnels](variables.tf#L72) | VPN tunnel configurations. | map(object({…})) | | {} | -| [vpn_gateway](variables.tf#L114) | HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`. | string | | null | -| [vpn_gateway_create](variables.tf#L120) | Create HA VPN Gateway. Set to null to avoid creation. | object({…}) | | {} | +| [tunnels](variables.tf#L72) | VPN tunnel configurations. | map(object({…})) | | {} | +| [vpn_gateway](variables.tf#L108) | HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`. | string | | null | +| [vpn_gateway_create](variables.tf#L114) | Create HA VPN Gateway. Set to null to avoid creation. | object({…}) | | {} | ## Outputs diff --git a/modules/net-vpn-ha/main.tf b/modules/net-vpn-ha/main.tf index 20af29015a..bbb7ca0295 100644 --- a/modules/net-vpn-ha/main.tf +++ b/modules/net-vpn-ha/main.tf @@ -117,15 +117,6 @@ resource "google_compute_router_peer" "bgp_peer" { description = range.value } } - dynamic "bfd" { - for_each = each.value.bgp_peer.bfd != null ? [each.value.bgp_peer.bfd] : [] - content { - session_initialization_mode = bfd.value.session_initialization_mode - min_receive_interval = bfd.value.min_receive_interval - min_transmit_interval = bfd.value.min_transmit_interval - multiplier = bfd.value.multiplier - } - } dynamic "md5_authentication_key" { for_each = each.value.bgp_peer.md5_authentication_key != null ? toset([each.value.bgp_peer.md5_authentication_key]) : [] content { diff --git a/modules/net-vpn-ha/variables.tf b/modules/net-vpn-ha/variables.tf index d507c89881..ba86eee6e9 100644 --- a/modules/net-vpn-ha/variables.tf +++ b/modules/net-vpn-ha/variables.tf @@ -76,12 +76,6 @@ variable "tunnels" { address = string asn = number route_priority = optional(number, 1000) - bfd = optional(object({ - min_receive_interval = optional(number) - min_transmit_interval = optional(number) - multiplier = optional(number) - session_initialization_mode = optional(string, "ACTIVE") - })) custom_advertise = optional(object({ all_subnets = bool all_vpc_subnets = bool From ab174274de35c6b73f44e23d312c86a25c729d72 Mon Sep 17 00:00:00 2001 From: apichick Date: Sun, 28 Apr 2024 17:31:42 +0200 Subject: [PATCH 08/29] Added new attributes Apigee organization and bumped up providers version (#2243) --- .../patterns/autopilot-cluster/versions.tf | 4 +- blueprints/gke/patterns/batch/versions.tf | 4 +- blueprints/gke/patterns/kafka/versions.tf | 4 +- blueprints/gke/patterns/mysql/versions.tf | 4 +- .../gke/patterns/redis-cluster/versions.tf | 4 +- default-versions.tf | 4 +- .../alloydb-instance/versions.tf | 4 +- .../net-neg/versions.tf | 4 +- .../project-iam-magic/versions.tf | 4 +- modules/analytics-hub/versions.tf | 4 +- modules/api-gateway/versions.tf | 4 +- modules/apigee/README.md | 8 +-- modules/apigee/main.tf | 33 ++++++++---- modules/apigee/variables.tf | 50 ++++++++++--------- modules/apigee/versions.tf | 4 +- modules/artifact-registry/versions.tf | 4 +- modules/bigquery-dataset/versions.tf | 4 +- modules/bigtable-instance/versions.tf | 4 +- modules/billing-account/versions.tf | 4 +- modules/binauthz/versions.tf | 4 +- .../__need_fixing/onprem/versions.tf | 4 +- .../__need_fixing/squid/versions.tf | 4 +- .../coredns/versions.tf | 4 +- .../cos-generic-metadata/versions.tf | 4 +- .../envoy-sni-dyn-fwd-proxy/versions.tf | 4 +- .../envoy-traffic-director/versions.tf | 4 +- .../cloud-config-container/mysql/versions.tf | 4 +- .../nginx-tls/versions.tf | 4 +- .../cloud-config-container/nginx/versions.tf | 4 +- .../simple-nva/versions.tf | 4 +- modules/cloud-function-v1/versions.tf | 4 +- modules/cloud-function-v2/versions.tf | 4 +- modules/cloud-identity-group/versions.tf | 4 +- modules/cloud-run-v2/versions.tf | 4 +- modules/cloud-run/versions.tf | 4 +- modules/cloudsql-instance/versions.tf | 4 +- modules/compute-mig/versions.tf | 4 +- modules/compute-vm/versions.tf | 4 +- modules/container-registry/versions.tf | 4 +- modules/data-catalog-policy-tag/versions.tf | 4 +- modules/data-catalog-tag-template/versions.tf | 4 +- modules/data-catalog-tag/versions.tf | 4 +- modules/dataform-repository/versions.tf | 4 +- modules/datafusion/versions.tf | 4 +- modules/dataplex-datascan/versions.tf | 4 +- modules/dataplex/versions.tf | 4 +- modules/dataproc/versions.tf | 4 +- modules/dns-response-policy/versions.tf | 4 +- modules/dns/versions.tf | 4 +- modules/endpoints/versions.tf | 4 +- modules/folder/versions.tf | 4 +- modules/gcs/versions.tf | 4 +- modules/gcve-private-cloud/versions.tf | 4 +- modules/gke-cluster-autopilot/versions.tf | 4 +- modules/gke-cluster-standard/versions.tf | 4 +- modules/gke-hub/versions.tf | 4 +- modules/gke-nodepool/versions.tf | 4 +- modules/iam-service-account/versions.tf | 4 +- modules/kms/versions.tf | 4 +- modules/logging-bucket/versions.tf | 4 +- modules/ncc-spoke-ra/versions.tf | 4 +- modules/net-address/versions.tf | 4 +- modules/net-cloudnat/versions.tf | 4 +- modules/net-firewall-policy/versions.tf | 4 +- .../net-ipsec-over-interconnect/versions.tf | 4 +- modules/net-lb-app-ext-regional/versions.tf | 4 +- modules/net-lb-app-ext/versions.tf | 4 +- .../net-lb-app-int-cross-region/versions.tf | 4 +- modules/net-lb-app-int/versions.tf | 4 +- modules/net-lb-ext/versions.tf | 4 +- modules/net-lb-int/versions.tf | 4 +- modules/net-lb-proxy-int/versions.tf | 4 +- modules/net-swp/versions.tf | 4 +- modules/net-vlan-attachment/versions.tf | 4 +- modules/net-vpc-firewall/versions.tf | 4 +- modules/net-vpc-peering/versions.tf | 4 +- modules/net-vpc/versions.tf | 4 +- modules/net-vpn-dynamic/versions.tf | 4 +- modules/net-vpn-ha/versions.tf | 4 +- modules/net-vpn-static/versions.tf | 4 +- modules/organization/versions.tf | 4 +- modules/project/versions.tf | 4 +- modules/projects-data-source/versions.tf | 4 +- modules/pubsub/versions.tf | 4 +- modules/secret-manager/versions.tf | 4 +- modules/service-directory/versions.tf | 4 +- modules/source-repository/versions.tf | 4 +- modules/vpc-sc/versions.tf | 4 +- modules/workstation-cluster/versions.tf | 4 +- tests/examples_e2e/setup_module/versions.tf | 4 +- 90 files changed, 229 insertions(+), 210 deletions(-) diff --git a/blueprints/gke/patterns/autopilot-cluster/versions.tf b/blueprints/gke/patterns/autopilot-cluster/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/blueprints/gke/patterns/autopilot-cluster/versions.tf +++ b/blueprints/gke/patterns/autopilot-cluster/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/blueprints/gke/patterns/batch/versions.tf b/blueprints/gke/patterns/batch/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/blueprints/gke/patterns/batch/versions.tf +++ b/blueprints/gke/patterns/batch/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/blueprints/gke/patterns/kafka/versions.tf b/blueprints/gke/patterns/kafka/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/blueprints/gke/patterns/kafka/versions.tf +++ b/blueprints/gke/patterns/kafka/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/blueprints/gke/patterns/mysql/versions.tf b/blueprints/gke/patterns/mysql/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/blueprints/gke/patterns/mysql/versions.tf +++ b/blueprints/gke/patterns/mysql/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/blueprints/gke/patterns/redis-cluster/versions.tf b/blueprints/gke/patterns/redis-cluster/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/blueprints/gke/patterns/redis-cluster/versions.tf +++ b/blueprints/gke/patterns/redis-cluster/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/default-versions.tf b/default-versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/default-versions.tf +++ b/default-versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/__experimental_deprecated/alloydb-instance/versions.tf b/modules/__experimental_deprecated/alloydb-instance/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/__experimental_deprecated/alloydb-instance/versions.tf +++ b/modules/__experimental_deprecated/alloydb-instance/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/__experimental_deprecated/net-neg/versions.tf b/modules/__experimental_deprecated/net-neg/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/__experimental_deprecated/net-neg/versions.tf +++ b/modules/__experimental_deprecated/net-neg/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/__experimental_deprecated/project-iam-magic/versions.tf b/modules/__experimental_deprecated/project-iam-magic/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/__experimental_deprecated/project-iam-magic/versions.tf +++ b/modules/__experimental_deprecated/project-iam-magic/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/analytics-hub/versions.tf b/modules/analytics-hub/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/analytics-hub/versions.tf +++ b/modules/analytics-hub/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/api-gateway/versions.tf b/modules/api-gateway/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/api-gateway/versions.tf +++ b/modules/api-gateway/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/apigee/README.md b/modules/apigee/README.md index 24c6f4166a..ebae675f31 100644 --- a/modules/apigee/README.md +++ b/modules/apigee/README.md @@ -359,13 +359,13 @@ module "apigee" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L126) | Project ID. | string | ✓ | | +| [project_id](variables.tf#L130) | Project ID. | string | ✓ | | | [addons_config](variables.tf#L17) | Addons configuration. | object({…}) | | null | | [endpoint_attachments](variables.tf#L29) | Endpoint attachments. | map(object({…})) | | {} | | [envgroups](variables.tf#L39) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | {} | -| [environments](variables.tf#L46) | Environments. | map(object({…})) | | {} | -| [instances](variables.tf#L73) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | -| [organization](variables.tf#L98) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +| [environments](variables.tf#L46) | Environments. | map(object({…})) | | {} | +| [instances](variables.tf#L73) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | +| [organization](variables.tf#L98) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | ## Outputs diff --git a/modules/apigee/main.tf b/modules/apigee/main.tf index bb0417b279..afbc1e5af6 100644 --- a/modules/apigee/main.tf +++ b/modules/apigee/main.tf @@ -20,15 +20,30 @@ locals { } resource "google_apigee_organization" "organization" { - count = var.organization == null ? 0 : 1 - analytics_region = var.organization.analytics_region - project_id = var.project_id - authorized_network = var.organization.authorized_network - billing_type = var.organization.billing_type - runtime_type = var.organization.runtime_type - runtime_database_encryption_key_name = var.organization.database_encryption_key - retention = var.organization.retention - disable_vpc_peering = var.organization.disable_vpc_peering + count = var.organization == null ? 0 : 1 + analytics_region = var.organization.analytics_region + project_id = var.project_id + authorized_network = var.organization.authorized_network + billing_type = var.organization.billing_type + runtime_type = var.organization.runtime_type + runtime_database_encryption_key_name = var.organization.database_encryption_key + retention = var.organization.retention + disable_vpc_peering = var.organization.disable_vpc_peering + api_consumer_data_location = var.organization.api_consumer_data_location + api_consumer_data_encryption_key_name = var.organization.api_consumer_data_encryption_key + control_plane_encryption_key_name = var.organization.control_plane_encryption_key + dynamic "properties" { + for_each = length(var.organization.properties) > 0 ? [""] : [] + content { + dynamic "property" { + for_each = var.organization.properties + content { + name = properties.key + value = properties.value + } + } + } + } } resource "google_apigee_envgroup" "envgroups" { diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf index 027ad05e47..6ce9fdcf1f 100644 --- a/modules/apigee/variables.tf +++ b/modules/apigee/variables.tf @@ -46,16 +46,12 @@ variable "envgroups" { variable "environments" { description = "Environments." type = map(object({ - display_name = optional(string) + api_proxy_type = optional(string) description = optional(string, "Terraform-managed") + display_name = optional(string) deployment_type = optional(string) - api_proxy_type = optional(string) - type = optional(string) - node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - })) - iam = optional(map(list(string)), {}) + envgroups = optional(list(string), []) + iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ role = string members = list(string) @@ -64,7 +60,11 @@ variable "environments" { role = string member = string })), {}) - envgroups = optional(list(string), []) + node_config = optional(object({ + min_node_count = optional(number) + max_node_count = optional(number) + })) + type = optional(string) })) default = {} nullable = false @@ -73,15 +73,15 @@ variable "environments" { variable "instances" { description = "Instances ([REGION] => [INSTANCE])." type = map(object({ - name = optional(string) - display_name = optional(string) + consumer_accept_list = optional(list(string)) description = optional(string, "Terraform-managed") - runtime_ip_cidr_range = optional(string) - troubleshooting_ip_cidr_range = optional(string) disk_encryption_key = optional(string) - consumer_accept_list = optional(list(string)) + display_name = optional(string) enable_nat = optional(bool, false) environments = optional(list(string), []) + name = optional(string) + runtime_ip_cidr_range = optional(string) + troubleshooting_ip_cidr_range = optional(string) })) validation { condition = alltrue([ @@ -98,15 +98,19 @@ variable "instances" { variable "organization" { description = "Apigee organization. If set to null the organization must already exist." type = object({ - display_name = optional(string) - description = optional(string, "Terraform-managed") - authorized_network = optional(string) - runtime_type = optional(string, "CLOUD") - billing_type = optional(string) - database_encryption_key = optional(string) - analytics_region = optional(string, "europe-west1") - retention = optional(string) - disable_vpc_peering = optional(bool, false) + analytics_region = optional(string) + api_consumer_data_encryption_key = optional(string) + api_consumer_data_location = optional(string) + authorized_network = optional(string) + billing_type = optional(string) + control_plane_encryption_key = optional(string) + database_encryption_key = optional(string) + description = optional(string, "Terraform-managed") + disable_vpc_peering = optional(bool, false) + display_name = optional(string) + properties = optional(map(string), {}) + runtime_type = optional(string, "CLOUD") + retention = optional(string) }) validation { condition = var.organization == null || ( diff --git a/modules/apigee/versions.tf b/modules/apigee/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/apigee/versions.tf +++ b/modules/apigee/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/artifact-registry/versions.tf b/modules/artifact-registry/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/artifact-registry/versions.tf +++ b/modules/artifact-registry/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/bigquery-dataset/versions.tf b/modules/bigquery-dataset/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/bigquery-dataset/versions.tf +++ b/modules/bigquery-dataset/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/bigtable-instance/versions.tf b/modules/bigtable-instance/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/bigtable-instance/versions.tf +++ b/modules/bigtable-instance/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/billing-account/versions.tf b/modules/billing-account/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/billing-account/versions.tf +++ b/modules/billing-account/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/binauthz/versions.tf b/modules/binauthz/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/binauthz/versions.tf +++ b/modules/binauthz/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/__need_fixing/onprem/versions.tf b/modules/cloud-config-container/__need_fixing/onprem/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/__need_fixing/onprem/versions.tf +++ b/modules/cloud-config-container/__need_fixing/onprem/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/__need_fixing/squid/versions.tf b/modules/cloud-config-container/__need_fixing/squid/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/__need_fixing/squid/versions.tf +++ b/modules/cloud-config-container/__need_fixing/squid/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/coredns/versions.tf b/modules/cloud-config-container/coredns/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/coredns/versions.tf +++ b/modules/cloud-config-container/coredns/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/cos-generic-metadata/versions.tf b/modules/cloud-config-container/cos-generic-metadata/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/cos-generic-metadata/versions.tf +++ b/modules/cloud-config-container/cos-generic-metadata/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/versions.tf b/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/versions.tf +++ b/modules/cloud-config-container/envoy-sni-dyn-fwd-proxy/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/envoy-traffic-director/versions.tf b/modules/cloud-config-container/envoy-traffic-director/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/envoy-traffic-director/versions.tf +++ b/modules/cloud-config-container/envoy-traffic-director/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/mysql/versions.tf b/modules/cloud-config-container/mysql/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/mysql/versions.tf +++ b/modules/cloud-config-container/mysql/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx-tls/versions.tf b/modules/cloud-config-container/nginx-tls/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/nginx-tls/versions.tf +++ b/modules/cloud-config-container/nginx-tls/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx/versions.tf b/modules/cloud-config-container/nginx/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/nginx/versions.tf +++ b/modules/cloud-config-container/nginx/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-config-container/simple-nva/versions.tf b/modules/cloud-config-container/simple-nva/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-config-container/simple-nva/versions.tf +++ b/modules/cloud-config-container/simple-nva/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-function-v1/versions.tf b/modules/cloud-function-v1/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-function-v1/versions.tf +++ b/modules/cloud-function-v1/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-function-v2/versions.tf b/modules/cloud-function-v2/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-function-v2/versions.tf +++ b/modules/cloud-function-v2/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-identity-group/versions.tf b/modules/cloud-identity-group/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-identity-group/versions.tf +++ b/modules/cloud-identity-group/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-run-v2/versions.tf b/modules/cloud-run-v2/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-run-v2/versions.tf +++ b/modules/cloud-run-v2/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloud-run/versions.tf b/modules/cloud-run/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloud-run/versions.tf +++ b/modules/cloud-run/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/cloudsql-instance/versions.tf b/modules/cloudsql-instance/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/cloudsql-instance/versions.tf +++ b/modules/cloudsql-instance/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/compute-mig/versions.tf b/modules/compute-mig/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/compute-mig/versions.tf +++ b/modules/compute-mig/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/compute-vm/versions.tf b/modules/compute-vm/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/compute-vm/versions.tf +++ b/modules/compute-vm/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/container-registry/versions.tf b/modules/container-registry/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/container-registry/versions.tf +++ b/modules/container-registry/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/data-catalog-policy-tag/versions.tf b/modules/data-catalog-policy-tag/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/data-catalog-policy-tag/versions.tf +++ b/modules/data-catalog-policy-tag/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/data-catalog-tag-template/versions.tf b/modules/data-catalog-tag-template/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/data-catalog-tag-template/versions.tf +++ b/modules/data-catalog-tag-template/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/data-catalog-tag/versions.tf b/modules/data-catalog-tag/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/data-catalog-tag/versions.tf +++ b/modules/data-catalog-tag/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dataform-repository/versions.tf b/modules/dataform-repository/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dataform-repository/versions.tf +++ b/modules/dataform-repository/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/datafusion/versions.tf b/modules/datafusion/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/datafusion/versions.tf +++ b/modules/datafusion/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dataplex-datascan/versions.tf b/modules/dataplex-datascan/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dataplex-datascan/versions.tf +++ b/modules/dataplex-datascan/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dataplex/versions.tf b/modules/dataplex/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dataplex/versions.tf +++ b/modules/dataplex/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dataproc/versions.tf b/modules/dataproc/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dataproc/versions.tf +++ b/modules/dataproc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dns-response-policy/versions.tf b/modules/dns-response-policy/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dns-response-policy/versions.tf +++ b/modules/dns-response-policy/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/dns/versions.tf b/modules/dns/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/dns/versions.tf +++ b/modules/dns/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/endpoints/versions.tf b/modules/endpoints/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/endpoints/versions.tf +++ b/modules/endpoints/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/folder/versions.tf b/modules/folder/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/folder/versions.tf +++ b/modules/folder/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gcs/versions.tf b/modules/gcs/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gcs/versions.tf +++ b/modules/gcs/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gcve-private-cloud/versions.tf b/modules/gcve-private-cloud/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gcve-private-cloud/versions.tf +++ b/modules/gcve-private-cloud/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gke-cluster-autopilot/versions.tf b/modules/gke-cluster-autopilot/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gke-cluster-autopilot/versions.tf +++ b/modules/gke-cluster-autopilot/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gke-cluster-standard/versions.tf b/modules/gke-cluster-standard/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gke-cluster-standard/versions.tf +++ b/modules/gke-cluster-standard/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gke-hub/versions.tf b/modules/gke-hub/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gke-hub/versions.tf +++ b/modules/gke-hub/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/gke-nodepool/versions.tf b/modules/gke-nodepool/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/gke-nodepool/versions.tf +++ b/modules/gke-nodepool/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/iam-service-account/versions.tf b/modules/iam-service-account/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/iam-service-account/versions.tf +++ b/modules/iam-service-account/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/kms/versions.tf +++ b/modules/kms/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/logging-bucket/versions.tf b/modules/logging-bucket/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/logging-bucket/versions.tf +++ b/modules/logging-bucket/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/ncc-spoke-ra/versions.tf b/modules/ncc-spoke-ra/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/ncc-spoke-ra/versions.tf +++ b/modules/ncc-spoke-ra/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-address/versions.tf b/modules/net-address/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-address/versions.tf +++ b/modules/net-address/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-cloudnat/versions.tf b/modules/net-cloudnat/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-cloudnat/versions.tf +++ b/modules/net-cloudnat/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-firewall-policy/versions.tf b/modules/net-firewall-policy/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-firewall-policy/versions.tf +++ b/modules/net-firewall-policy/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-ipsec-over-interconnect/versions.tf b/modules/net-ipsec-over-interconnect/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-ipsec-over-interconnect/versions.tf +++ b/modules/net-ipsec-over-interconnect/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-app-ext-regional/versions.tf b/modules/net-lb-app-ext-regional/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-app-ext-regional/versions.tf +++ b/modules/net-lb-app-ext-regional/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-app-ext/versions.tf b/modules/net-lb-app-ext/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-app-ext/versions.tf +++ b/modules/net-lb-app-ext/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-app-int-cross-region/versions.tf b/modules/net-lb-app-int-cross-region/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-app-int-cross-region/versions.tf +++ b/modules/net-lb-app-int-cross-region/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-app-int/versions.tf b/modules/net-lb-app-int/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-app-int/versions.tf +++ b/modules/net-lb-app-int/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-ext/versions.tf b/modules/net-lb-ext/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-ext/versions.tf +++ b/modules/net-lb-ext/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-int/versions.tf b/modules/net-lb-int/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-int/versions.tf +++ b/modules/net-lb-int/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-lb-proxy-int/versions.tf b/modules/net-lb-proxy-int/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-lb-proxy-int/versions.tf +++ b/modules/net-lb-proxy-int/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-swp/versions.tf b/modules/net-swp/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-swp/versions.tf +++ b/modules/net-swp/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vlan-attachment/versions.tf b/modules/net-vlan-attachment/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vlan-attachment/versions.tf +++ b/modules/net-vlan-attachment/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpc-firewall/versions.tf b/modules/net-vpc-firewall/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpc-firewall/versions.tf +++ b/modules/net-vpc-firewall/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpc-peering/versions.tf b/modules/net-vpc-peering/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpc-peering/versions.tf +++ b/modules/net-vpc-peering/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpc/versions.tf b/modules/net-vpc/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpc/versions.tf +++ b/modules/net-vpc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpn-dynamic/versions.tf b/modules/net-vpn-dynamic/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpn-dynamic/versions.tf +++ b/modules/net-vpn-dynamic/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpn-ha/versions.tf b/modules/net-vpn-ha/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpn-ha/versions.tf +++ b/modules/net-vpn-ha/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/net-vpn-static/versions.tf b/modules/net-vpn-static/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/net-vpn-static/versions.tf +++ b/modules/net-vpn-static/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/organization/versions.tf +++ b/modules/organization/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/project/versions.tf b/modules/project/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/project/versions.tf +++ b/modules/project/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/projects-data-source/versions.tf b/modules/projects-data-source/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/projects-data-source/versions.tf +++ b/modules/projects-data-source/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/pubsub/versions.tf b/modules/pubsub/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/pubsub/versions.tf +++ b/modules/pubsub/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/secret-manager/versions.tf b/modules/secret-manager/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/secret-manager/versions.tf +++ b/modules/secret-manager/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/service-directory/versions.tf b/modules/service-directory/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/service-directory/versions.tf +++ b/modules/service-directory/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/source-repository/versions.tf b/modules/source-repository/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/source-repository/versions.tf +++ b/modules/source-repository/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/vpc-sc/versions.tf b/modules/vpc-sc/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/vpc-sc/versions.tf +++ b/modules/vpc-sc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/modules/workstation-cluster/versions.tf b/modules/workstation-cluster/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/modules/workstation-cluster/versions.tf +++ b/modules/workstation-cluster/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } diff --git a/tests/examples_e2e/setup_module/versions.tf b/tests/examples_e2e/setup_module/versions.tf index 3d6a43260a..bc9986b3c7 100644 --- a/tests/examples_e2e/setup_module/versions.tf +++ b/tests/examples_e2e/setup_module/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 5.24.0, < 6.0.0" # tftest + version = ">= 5.26.0, < 6.0.0" # tftest } } } From be966c4f3273beff68ad51250ca3b5ae49ecbc35 Mon Sep 17 00:00:00 2001 From: apichick Date: Sun, 28 Apr 2024 22:18:02 +0200 Subject: [PATCH 09/29] Fixed issue with service networking DNS peering (#2246) Co-authored-by: Ludovico Magnocavallo --- modules/net-vpc/psa.tf | 2 +- tests/modules/net_vpc/examples/psa-routes.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/net-vpc/psa.tf b/modules/net-vpc/psa.tf index 1e44a9accb..913cebfb3a 100644 --- a/modules/net-vpc/psa.tf +++ b/modules/net-vpc/psa.tf @@ -28,7 +28,7 @@ locals { _psa_peered_domains = flatten([ for config in local.psa_configs : [ for v in config.peered_domains : { - key = "${config.key}-${replace(v, ".", "-")}" + key = "${config.key}-${trimsuffix(replace(v, ".", "-"), "-")}" dns_suffix = v service_producer = config.service_producer } diff --git a/tests/modules/net_vpc/examples/psa-routes.yaml b/tests/modules/net_vpc/examples/psa-routes.yaml index c64353b788..a9a8bfddb3 100644 --- a/tests/modules/net_vpc/examples/psa-routes.yaml +++ b/tests/modules/net_vpc/examples/psa-routes.yaml @@ -84,9 +84,9 @@ values: - servicenetworking-googleapis-com-myrange service: servicenetworking.googleapis.com timeouts: null - module.vpc.google_service_networking_peered_dns_domain.name["servicenetworking-googleapis-com-gcp-example-com-"]: + module.vpc.google_service_networking_peered_dns_domain.name["servicenetworking-googleapis-com-gcp-example-com"]: dns_suffix: gcp.example.com. - name: servicenetworking-googleapis-com-gcp-example-com- + name: servicenetworking-googleapis-com-gcp-example-com network: my-network project: project-id service: servicenetworking.googleapis.com From e1226676fdb5f9bc918e8734ee8348c9b1968963 Mon Sep 17 00:00:00 2001 From: jnahelou Date: Tue, 30 Apr 2024 19:21:35 +0200 Subject: [PATCH 10/29] Added missing identity when connectors API is enabled (#2248) --- modules/project/service-agents.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/project/service-agents.yaml b/modules/project/service-agents.yaml index 66c95a4640..455287e807 100644 --- a/modules/project/service-agents.yaml +++ b/modules/project/service-agents.yaml @@ -121,6 +121,7 @@ service_agent: "service-%s@gcp-sa-anthossupport.iam.gserviceaccount.com" - name: "connectors" service_agent: "service-%s@gcp-sa-connectors.iam.gserviceaccount.com" + jit: true - name: "contactcenteraiplatform" service_agent: "service-%s@gcp-sa-ccaip.iam.gserviceaccount.com" - name: "contactcenterinsights" From 27a055a9cbffda216250af736ec2a68241bec12e Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 1 May 2024 18:50:30 +0200 Subject: [PATCH 11/29] fix factory ingress policies (#2251) --- modules/vpc-sc/README.md | 23 +++++++++++++++++++--- modules/vpc-sc/factory.tf | 2 +- tests/modules/vpc_sc/examples/factory.yaml | 20 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index 2bdeebe9fe..fb99c10657 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -233,7 +233,7 @@ module "test" { resources = ["projects/11111", "projects/111111"] restricted_services = ["storage.googleapis.com"] egress_policies = ["gcs-sa-foo"] - ingress_policies = ["sa-tf-test"] + ingress_policies = ["sa-tf-test-geo", "sa-tf-test"] vpc_accessible_services = { allowed_services = ["storage.googleapis.com"] enable_restriction = true @@ -242,7 +242,7 @@ module "test" { } } } -# tftest modules=1 resources=3 files=a1,a2,e1,i1 inventory=factory.yaml +# tftest modules=1 resources=3 files=a1,a2,e1,i1,i2 inventory=factory.yaml ``` ```yaml @@ -282,12 +282,29 @@ from: - serviceAccount:test-tf@myproject.iam.gserviceaccount.com to: operations: - - service_name: "*" + - service_name: compute.googleapis.com + method_selectors: + - ProjectsService.Get + - RegionsService.Get resources: - "*" # tftest-file id=i1 path=data/ingress-policies/sa-tf-test.yaml ``` +```yaml +from: + access_levels: + - geo-it + identities: + - serviceAccount:test-tf@myproject.iam.gserviceaccount.com +to: + operations: + - service_name: "*" + resources: + - projects/1234567890 +# tftest-file id=i2 path=data/ingress-policies/sa-tf-test-geo.yaml +``` + ## Notes - To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again. diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf index eca5867a7e..d8a0a53622 100644 --- a/modules/vpc-sc/factory.tf +++ b/modules/vpc-sc/factory.tf @@ -74,7 +74,7 @@ locals { }, try(v.from, {})) to = { operations = [ - for o in try(v.operations, []) : merge({ + for o in try(v.to.operations, []) : merge({ method_selectors = [] permission_selectors = [] service_name = null diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml index 475c4d1e89..4496566709 100644 --- a/tests/modules/vpc_sc/examples/factory.yaml +++ b/tests/modules/vpc_sc/examples/factory.yaml @@ -81,9 +81,27 @@ values: - access_level: '*' resource: null ingress_to: - - operations: [] + - operations: + - method_selectors: + - method: ProjectsService.Get + permission: null + - method: RegionsService.Get + permission: null + service_name: compute.googleapis.com resources: - '*' + - ingress_from: + - identities: + - serviceAccount:test-tf@myproject.iam.gserviceaccount.com + identity_type: null + sources: + - resource: null + ingress_to: + - operations: + - method_selectors: [] + service_name: '*' + resources: + - projects/1234567890 resources: - projects/11111 - projects/111111 From fdcd309729b6c99f4ce2672e71d64df430529ff8 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 1 May 2024 20:20:21 +0200 Subject: [PATCH 12/29] add support for labels to GKE backup plans (#2252) --- modules/gke-cluster-autopilot/README.md | 38 +++++++++---------- modules/gke-cluster-autopilot/main.tf | 1 + modules/gke-cluster-autopilot/variables.tf | 1 + modules/gke-cluster-standard/README.md | 44 +++++++++++----------- modules/gke-cluster-standard/main.tf | 1 + modules/gke-cluster-standard/variables.tf | 1 + 6 files changed, 45 insertions(+), 41 deletions(-) diff --git a/modules/gke-cluster-autopilot/README.md b/modules/gke-cluster-autopilot/README.md index bf8affbe51..0091d6e04d 100644 --- a/modules/gke-cluster-autopilot/README.md +++ b/modules/gke-cluster-autopilot/README.md @@ -206,25 +206,25 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L112) | Autopilot clusters are always regional. | string | ✓ | | -| [name](variables.tf#L189) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L225) | Cluster project ID. | string | ✓ | | -| [vpc_config](variables.tf#L241) | VPC-level configuration. | object({…}) | ✓ | | -| [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | -| [deletion_protection](variables.tf#L37) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | -| [description](variables.tf#L44) | Cluster description. | string | | null | -| [enable_addons](variables.tf#L50) | Addons enabled in the cluster (true means enabled). | object({…}) | | {} | -| [enable_features](variables.tf#L64) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {} | -| [issue_client_certificate](variables.tf#L100) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L106) | Cluster resource labels. | map(string) | | null | -| [logging_config](variables.tf#L117) | Logging configuration. | object({…}) | | {} | -| [maintenance_config](variables.tf#L128) | Maintenance window configuration. | object({…}) | | {…} | -| [min_master_version](variables.tf#L151) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L157) | Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_config](variables.tf#L194) | Configuration for nodes and nodepools. | object({…}) | | {} | -| [node_locations](variables.tf#L204) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L211) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L230) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | +| [location](variables.tf#L113) | Autopilot clusters are always regional. | string | ✓ | | +| [name](variables.tf#L190) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L226) | Cluster project ID. | string | ✓ | | +| [vpc_config](variables.tf#L242) | VPC-level configuration. | object({…}) | ✓ | | +| [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | +| [deletion_protection](variables.tf#L38) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | +| [description](variables.tf#L45) | Cluster description. | string | | null | +| [enable_addons](variables.tf#L51) | Addons enabled in the cluster (true means enabled). | object({…}) | | {} | +| [enable_features](variables.tf#L65) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {} | +| [issue_client_certificate](variables.tf#L101) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L107) | Cluster resource labels. | map(string) | | null | +| [logging_config](variables.tf#L118) | Logging configuration. | object({…}) | | {} | +| [maintenance_config](variables.tf#L129) | Maintenance window configuration. | object({…}) | | {…} | +| [min_master_version](variables.tf#L152) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L158) | Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_config](variables.tf#L195) | Configuration for nodes and nodepools. | object({…}) | | {} | +| [node_locations](variables.tf#L205) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L212) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L231) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | ## Outputs diff --git a/modules/gke-cluster-autopilot/main.tf b/modules/gke-cluster-autopilot/main.tf index 825c8739e5..6824d22309 100644 --- a/modules/gke-cluster-autopilot/main.tf +++ b/modules/gke-cluster-autopilot/main.tf @@ -322,6 +322,7 @@ resource "google_gke_backup_backup_plan" "backup_plan" { cluster = google_container_cluster.cluster.id location = each.value.region project = var.project_id + labels = each.value.labels retention_policy { backup_delete_lock_days = try(each.value.retention_policy_delete_lock_days) backup_retain_days = try(each.value.retention_policy_days) diff --git a/modules/gke-cluster-autopilot/variables.tf b/modules/gke-cluster-autopilot/variables.tf index 570899274e..a31596a6e6 100644 --- a/modules/gke-cluster-autopilot/variables.tf +++ b/modules/gke-cluster-autopilot/variables.tf @@ -22,6 +22,7 @@ variable "backup_configs" { encryption_key = optional(string) include_secrets = optional(bool, true) include_volume_data = optional(bool, true) + labels = optional(map(string)) namespaces = optional(list(string)) region = string schedule = string diff --git a/modules/gke-cluster-standard/README.md b/modules/gke-cluster-standard/README.md index c53017267f..dd8ad0f444 100644 --- a/modules/gke-cluster-standard/README.md +++ b/modules/gke-cluster-standard/README.md @@ -310,28 +310,28 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L234) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L369) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L405) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L416) | VPC-level configuration. | object({…}) | ✓ | | -| [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | -| [cluster_autoscaling](variables.tf#L38) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | -| [default_nodepool](variables.tf#L117) | Enable default nodepool. | object({…}) | | {} | -| [deletion_protection](variables.tf#L135) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | -| [description](variables.tf#L142) | Cluster description. | string | | null | -| [enable_addons](variables.tf#L148) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | -| [enable_features](variables.tf#L172) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | -| [issue_client_certificate](variables.tf#L222) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L228) | Cluster resource labels. | map(string) | | null | -| [logging_config](variables.tf#L239) | Logging configuration. | object({…}) | | {} | -| [maintenance_config](variables.tf#L260) | Maintenance window configuration. | object({…}) | | {…} | -| [max_pods_per_node](variables.tf#L283) | Maximum number of pods per node in this cluster. | number | | 110 | -| [min_master_version](variables.tf#L289) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L295) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_config](variables.tf#L374) | Node-level configuration. | object({…}) | | {} | -| [node_locations](variables.tf#L384) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L391) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L410) | Release channel for GKE upgrades. | string | | null | +| [location](variables.tf#L235) | Cluster zone or region. | string | ✓ | | +| [name](variables.tf#L370) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L406) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L417) | VPC-level configuration. | object({…}) | ✓ | | +| [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | +| [cluster_autoscaling](variables.tf#L39) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | +| [default_nodepool](variables.tf#L118) | Enable default nodepool. | object({…}) | | {} | +| [deletion_protection](variables.tf#L136) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | +| [description](variables.tf#L143) | Cluster description. | string | | null | +| [enable_addons](variables.tf#L149) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | +| [enable_features](variables.tf#L173) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | +| [issue_client_certificate](variables.tf#L223) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L229) | Cluster resource labels. | map(string) | | null | +| [logging_config](variables.tf#L240) | Logging configuration. | object({…}) | | {} | +| [maintenance_config](variables.tf#L261) | Maintenance window configuration. | object({…}) | | {…} | +| [max_pods_per_node](variables.tf#L284) | Maximum number of pods per node in this cluster. | number | | 110 | +| [min_master_version](variables.tf#L290) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L296) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_config](variables.tf#L375) | Node-level configuration. | object({…}) | | {} | +| [node_locations](variables.tf#L385) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L392) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L411) | Release channel for GKE upgrades. | string | | null | ## Outputs diff --git a/modules/gke-cluster-standard/main.tf b/modules/gke-cluster-standard/main.tf index 99db49f80d..bc743fc5ce 100644 --- a/modules/gke-cluster-standard/main.tf +++ b/modules/gke-cluster-standard/main.tf @@ -511,6 +511,7 @@ resource "google_gke_backup_backup_plan" "backup_plan" { cluster = google_container_cluster.cluster.id location = each.value.region project = var.project_id + labels = each.value.labels retention_policy { backup_delete_lock_days = try(each.value.retention_policy_delete_lock_days) backup_retain_days = try(each.value.retention_policy_days) diff --git a/modules/gke-cluster-standard/variables.tf b/modules/gke-cluster-standard/variables.tf index 5580320f8f..2436b467cd 100644 --- a/modules/gke-cluster-standard/variables.tf +++ b/modules/gke-cluster-standard/variables.tf @@ -24,6 +24,7 @@ variable "backup_configs" { encryption_key = optional(string) include_secrets = optional(bool, true) include_volume_data = optional(bool, true) + labels = optional(map(string)) namespaces = optional(list(string)) schedule = optional(string) retention_policy_days = optional(number) From dccf5735c58f26fb08814eff7845ebab1297ac30 Mon Sep 17 00:00:00 2001 From: simonebruzzechesse <60114646+simonebruzzechesse@users.noreply.github.com> Date: Thu, 2 May 2024 08:09:10 +0200 Subject: [PATCH 13/29] fis issues with private workstation-cluster module and persistent_directories (#2247) Co-authored-by: Ludovico Magnocavallo --- modules/workstation-cluster/README.md | 3 +++ modules/workstation-cluster/main.tf | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/modules/workstation-cluster/README.md b/modules/workstation-cluster/README.md index cf45451479..05a0acdea2 100644 --- a/modules/workstation-cluster/README.md +++ b/modules/workstation-cluster/README.md @@ -59,6 +59,9 @@ module "workstation-cluster" { } workstation_configs = { my-workstation-config = { + gce_instance = { + disable_public_ip_addresses = true + } workstations = { my-workstation = { labels = { diff --git a/modules/workstation-cluster/main.tf b/modules/workstation-cluster/main.tf index 07399df4c8..b26a9fe579 100644 --- a/modules/workstation-cluster/main.tf +++ b/modules/workstation-cluster/main.tf @@ -70,8 +70,8 @@ resource "google_workstations_workstation_config" "configs" { pool_size = each.value.gce_instance.pool_size boot_disk_size_gb = each.value.gce_instance.boot_disk_size_gb tags = each.value.gce_instance.tags - disable_public_ip_addresses = each.value.disable_public_ip_addresses - enable_nested_virtualization = each.value.enable_nested_virtualization + disable_public_ip_addresses = each.value.gce_instance.disable_public_ip_addresses + enable_nested_virtualization = each.value.gce_instance.enable_nested_virtualization dynamic "shielded_instance_config" { for_each = each.value.gce_instance.shielded_instance_config == null ? [] : [""] content { @@ -81,7 +81,7 @@ resource "google_workstations_workstation_config" "configs" { } } dynamic "confidential_instance_config" { - for_each = each.value.gce_instance.enable_confidential_compute ? [] : [""] + for_each = each.value.gce_instance.enable_confidential_compute ? [""] : [] content { enable_confidential_compute = true } @@ -114,6 +114,21 @@ resource "google_workstations_workstation_config" "configs" { kms_key_service_account = each.value.encryption_key.kms_key_service_account } } + dynamic "persistent_directories" { + for_each = each.value.persistent_directories + content { + mount_path = persistent_directories.value.mount_path + dynamic "gce_pd" { + for_each = persistent_directories.value.gce_pd == null ? [] : [""] + content { + size_gb = persistent_directories.value.gce_pd.size_gb + fs_type = persistent_directories.value.gce_pd.fs_type + disk_type = persistent_directories.value.gce_pd.disk_type + reclaim_policy = persistent_directories.value.gce_pd.reclaim_policy + } + } + } + } } resource "google_workstations_workstation" "workstations" { From 94c32c1d71e96d8332b8e74f71045aa4e4b5e0a6 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 2 May 2024 08:56:26 +0200 Subject: [PATCH 14/29] Misc FAST fixes (#2253) * Misc FAST fixes * Fix readme * Fix FAST nva bgp tests --- .../0-bootstrap-tenant/README.md | 2 +- .../0-bootstrap-tenant/variables.tf | 4 ++-- fast/stages/0-bootstrap/README.md | 18 +++++++++++++-- fast/stages/0-bootstrap/variables.tf | 2 +- fast/stages/1-resman/README.md | 22 +++++++++---------- fast/stages/1-resman/outputs.tf | 2 +- fast/stages/1-resman/variables.tf | 17 +++++--------- .../data/hierarchical-ingress-rules.yaml | 4 ++-- .../data/hierarchical-ingress-rules.yaml | 4 ++-- .../data/hierarchical-ingress-rules.yaml | 4 ++-- .../data/hierarchical-ingress-rules.yaml | 4 ++-- .../data/hierarchical-ingress-rules.yaml | 4 ++-- tests/fast/stages/s0_bootstrap/checklist.yaml | 22 +++++++++---------- tests/fast/stages/s0_bootstrap/simple.yaml | 2 +- tests/fast/stages/s1_resman/checklist.tfvars | 2 +- tests/fast/stages/s1_resman/simple.tfvars | 2 +- .../s2_networking_a_peering/simple.tfvars | 2 +- .../stages/s2_networking_b_vpn/simple.tfvars | 2 +- .../stages/s2_networking_c_nva/simple.tfvars | 2 +- .../simple.tfvars | 2 +- .../s2_networking_e_nva_bgp/simple.tfvars | 2 +- .../s2_networking_e_nva_bgp/simple.yaml | 3 ++- .../s0_bootstrap_tenant/simple.tfvars | 2 +- .../s1_resman_tenant/simple.tfvars | 2 +- 24 files changed, 71 insertions(+), 61 deletions(-) diff --git a/fast/stages-multitenant/0-bootstrap-tenant/README.md b/fast/stages-multitenant/0-bootstrap-tenant/README.md index 9e3a74d84a..a1b3ba367b 100644 --- a/fast/stages-multitenant/0-bootstrap-tenant/README.md +++ b/fast/stages-multitenant/0-bootstrap-tenant/README.md @@ -208,7 +208,7 @@ This configuration is possible but unsupported and only exists for development p | [custom_roles](variables.tf#L95) | Custom roles defined at the organization level, in key => id format. | object({…}) | | null | 0-bootstrap | | [fast_features](variables.tf#L105) | Selective control for top-level FAST features. | object({…}) | | {} | 0-bootstrap | | [federated_identity_providers](variables.tf#L119) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | -| [groups](variables.tf#L133) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | +| [groups](variables.tf#L133) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | | [iam](variables.tf#L146) | Tenant-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | | [iam_bindings_additive](variables.tf#L152) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | | [iam_by_principals](variables.tf#L167) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | diff --git a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf index 74daa0a9f1..5fa964beee 100644 --- a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf +++ b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -136,7 +136,7 @@ variable "groups" { description = "Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated." type = object({ gcp-devops = optional(string, "gcp-devops") - gcp-network-admins = optional(string, "gcp-network-admins") + gcp-network-admins = optional(string, "gcp-vpc-network-admins") gcp-security-admins = optional(string, "gcp-security-admins") }) nullable = false diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index ee5c7f52e7..43060232fe 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -39,6 +39,7 @@ Use the following diagram as a simple high level reference for the following sec - [Log sinks and log destinations](#log-sinks-and-log-destinations) - [Names and naming convention](#names-and-naming-convention) - [Workload Identity Federation](#workload-identity-federation) + - [Project folders](#project-folders) - [CI/CD repositories](#cicd-repositories) - [Toggling features](#toggling-features) - [Files](#files) @@ -533,6 +534,18 @@ workload_identity_providers = { } ``` +### Project folders + +By default this stage creates all its projects directly under the orgaization node. If desired, projects can be moved under a folder using the `project_parent_ids` variable. + +```tfvars +project_parent_ids = { + automation = "folders/1234567890" + billing = "folders/9876543210" + logging = "folders/1234567890" +} +``` + ### CI/CD repositories FAST is designed to directly support running in automated workflows from separate repositories for each stage. The `cicd_repositories` variable allows you to configure impersonation from external repositories leveraging Workload identity Federation, and pre-configures a FAST workflow file that can be used to validate and apply the code in each repository. @@ -595,9 +608,10 @@ The remaining configuration is manual, as it regards the repositories themselves Some FAST features can be enabled or disabled using the `fast_features` variables. While this variable is not directly used in the bootstrap stage, it can instruct the following stages to create certain resources only if needed. -The `fast_features` variable consists of 4 toggles: +The `fast_features` variable consists of 6 toggles: - **`data_platform`** controls the creation of required resources (folders, service accounts, buckets, IAM bindings) to deploy the [3-data-platform](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/stages/3-data-platform) stage +- **`gcve`** controls the creation of required resources (folders, service accounts, buckets, IAM bindings) to deploy the [3-gcve](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/stages/3-gcve) stage - **`gke`** controls the creation of required resources (folders, service accounts, buckets, IAM bindings) to deploy the [3-gke-multitenant](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/stages/3-gke-multitenant) stage - **`project_factory`** controls the creation of required resources (folders, service accounts, buckets, IAM bindings) to deploy the [3-project-factory](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/stages/3-project-factory) stage - **`sandbox`** controls the creation of a "Sandbox" top level folder with relaxed policies, intended for sandbox environments where users can experiment @@ -636,7 +650,7 @@ The `fast_features` variable consists of 4 toggles: | [essential_contacts](variables.tf#L86) | Email used for essential contacts, unset if null. | string | | null | | | [factories_config](variables.tf#L92) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_features](variables.tf#L104) | Selective control for top-level FAST features. | object({…}) | | {} | | -| [groups](variables.tf#L118) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | | +| [groups](variables.tf#L118) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | | | [iam](variables.tf#L134) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | | [iam_bindings_additive](variables.tf#L141) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | | [iam_by_principals](variables.tf#L156) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf index b769f1088f..26ec513ac9 100644 --- a/fast/stages/0-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -121,7 +121,7 @@ variable "groups" { type = object({ gcp-billing-admins = optional(string, "gcp-billing-admins") gcp-devops = optional(string, "gcp-devops") - gcp-network-admins = optional(string, "gcp-network-admins") + gcp-network-admins = optional(string, "gcp-vpc-network-admins") gcp-organization-admins = optional(string, "gcp-organization-admins") gcp-security-admins = optional(string, "gcp-security-admins") # aliased to gcp-devops as the checklist does not create it diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index 6b7836fd8b..5ac024870b 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -358,21 +358,21 @@ Due to its simplicity, this stage lends itself easily to customizations: adding |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables.tf#L42) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…}) | ✓ | | 0-bootstrap | -| [organization](variables.tf#L232) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables.tf#L248) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [organization](variables.tf#L227) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L243) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | | [cicd_repositories](variables.tf#L53) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | | [custom_roles](variables.tf#L147) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | | [factories_config](variables.tf#L159) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_features](variables.tf#L168) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | | [folder_iam](variables.tf#L183) | Authoritative IAM for top-level folders. | object({…}) | | {} | | -| [groups](variables.tf#L199) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | -| [locations](variables.tf#L214) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | -| [outputs_location](variables.tf#L242) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [tag_names](variables.tf#L259) | Customized names for resource management tags. | object({…}) | | {} | | -| [tags](variables.tf#L274) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | -| [team_folders](variables.tf#L295) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | -| [tenants](variables.tf#L311) | Lightweight tenant definitions. | map(object({…})) | | {} | | -| [tenants_config](variables.tf#L327) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…}) | | {} | | +| [groups](variables.tf#L199) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L214) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | +| [outputs_location](variables.tf#L237) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [tag_names](variables.tf#L254) | Customized names for resource management tags. | object({…}) | | {} | | +| [tags](variables.tf#L269) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | +| [team_folders](variables.tf#L290) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [tenants](variables.tf#L306) | Lightweight tenant definitions. | map(object({…})) | | {} | | +| [tenants_config](variables.tf#L322) | Lightweight tenants shared configuration. Roles will be assigned to tenant admin group and service accounts. | object({…}) | | {} | | ## Outputs @@ -380,7 +380,7 @@ Due to its simplicity, this stage lends itself easily to customizations: adding |---|---|:---:|---| | [cicd_repositories](outputs.tf#L391) | WIF configuration for CI/CD repositories. | | | | [dataplatform](outputs.tf#L405) | Data for the Data Platform stage. | | | -| [gcve](outputs.tf#L421) | Data for the GCVE stage. | | 03-gke-multitenant | +| [gcve](outputs.tf#L421) | Data for the GCVE stage. | | 03-gcve | | [gke_multitenant](outputs.tf#L442) | Data for the GKE multitenant stage. | | 03-gke-multitenant | | [networking](outputs.tf#L463) | Data for the networking stage. | | | | [project_factories](outputs.tf#L472) | Data for the project factories stage. | | | diff --git a/fast/stages/1-resman/outputs.tf b/fast/stages/1-resman/outputs.tf index 31e9590203..de411b9115 100644 --- a/fast/stages/1-resman/outputs.tf +++ b/fast/stages/1-resman/outputs.tf @@ -419,7 +419,7 @@ output "dataplatform" { } output "gcve" { - # tfdoc:output:consumers 03-gke-multitenant + # tfdoc:output:consumers 03-gcve description = "Data for the GCVE stage." value = ( var.fast_features.gcve diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf index 8417466730..3256b9bb74 100644 --- a/fast/stages/1-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -203,7 +203,7 @@ variable "groups" { type = object({ gcp-billing-admins = optional(string, "gcp-billing-admins") gcp-devops = optional(string, "gcp-devops") - gcp-network-admins = optional(string, "gcp-network-admins") + gcp-network-admins = optional(string, "gcp-vpc-network-admins") gcp-organization-admins = optional(string, "gcp-organization-admins") gcp-security-admins = optional(string, "gcp-security-admins") }) @@ -215,18 +215,13 @@ variable "locations" { # tfdoc:variable:source 0-bootstrap description = "Optional locations for GCS, BigQuery, and logging buckets created here." type = object({ - bq = string - gcs = string - logging = string - pubsub = list(string) + bq = optional(string, "EU") + gcs = optional(string, "EU") + logging = optional(string, "global") + pubsub = optional(list(string), []) }) - default = { - bq = "EU" - gcs = "EU" - logging = "global" - pubsub = [] - } nullable = false + default = {} } variable "organization" { diff --git a/fast/stages/2-networking-a-peering/data/hierarchical-ingress-rules.yaml b/fast/stages/2-networking-a-peering/data/hierarchical-ingress-rules.yaml index 93d42fbee8..e444dd3f0b 100644 --- a/fast/stages/2-networking-a-peering/data/hierarchical-ingress-rules.yaml +++ b/fast/stages/2-networking-a-peering/data/hierarchical-ingress-rules.yaml @@ -11,14 +11,14 @@ # - rfc1918 allow-healthchecks: - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks priority: 1001 match: source_ranges: - healthchecks layer4_configs: - protocol: tcp - ports: ["80", "443"] + ports: ["22", "80", "443"] allow-ssh-from-iap: description: Enable SSH from IAP diff --git a/fast/stages/2-networking-b-vpn/data/hierarchical-ingress-rules.yaml b/fast/stages/2-networking-b-vpn/data/hierarchical-ingress-rules.yaml index 0504f376c8..817be2e99d 100644 --- a/fast/stages/2-networking-b-vpn/data/hierarchical-ingress-rules.yaml +++ b/fast/stages/2-networking-b-vpn/data/hierarchical-ingress-rules.yaml @@ -11,14 +11,14 @@ # - rfc1918 allow-healthchecks: - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks priority: 1001 match: source_ranges: - healthchecks layer4_configs: - protocol: tcp - ports: ["80", "443"] + ports: ["22", "80", "443"] allow-ssh-from-iap: description: Enable SSH from IAP diff --git a/fast/stages/2-networking-c-nva/data/hierarchical-ingress-rules.yaml b/fast/stages/2-networking-c-nva/data/hierarchical-ingress-rules.yaml index 0504f376c8..817be2e99d 100644 --- a/fast/stages/2-networking-c-nva/data/hierarchical-ingress-rules.yaml +++ b/fast/stages/2-networking-c-nva/data/hierarchical-ingress-rules.yaml @@ -11,14 +11,14 @@ # - rfc1918 allow-healthchecks: - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks priority: 1001 match: source_ranges: - healthchecks layer4_configs: - protocol: tcp - ports: ["80", "443"] + ports: ["22", "80", "443"] allow-ssh-from-iap: description: Enable SSH from IAP diff --git a/fast/stages/2-networking-d-separate-envs/data/hierarchical-ingress-rules.yaml b/fast/stages/2-networking-d-separate-envs/data/hierarchical-ingress-rules.yaml index 0504f376c8..817be2e99d 100644 --- a/fast/stages/2-networking-d-separate-envs/data/hierarchical-ingress-rules.yaml +++ b/fast/stages/2-networking-d-separate-envs/data/hierarchical-ingress-rules.yaml @@ -11,14 +11,14 @@ # - rfc1918 allow-healthchecks: - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks priority: 1001 match: source_ranges: - healthchecks layer4_configs: - protocol: tcp - ports: ["80", "443"] + ports: ["22", "80", "443"] allow-ssh-from-iap: description: Enable SSH from IAP diff --git a/fast/stages/2-networking-e-nva-bgp/data/hierarchical-ingress-rules.yaml b/fast/stages/2-networking-e-nva-bgp/data/hierarchical-ingress-rules.yaml index 0504f376c8..817be2e99d 100644 --- a/fast/stages/2-networking-e-nva-bgp/data/hierarchical-ingress-rules.yaml +++ b/fast/stages/2-networking-e-nva-bgp/data/hierarchical-ingress-rules.yaml @@ -11,14 +11,14 @@ # - rfc1918 allow-healthchecks: - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks priority: 1001 match: source_ranges: - healthchecks layer4_configs: - protocol: tcp - ports: ["80", "443"] + ports: ["22", "80", "443"] allow-ssh-from-iap: description: Enable SSH from IAP diff --git a/tests/fast/stages/s0_bootstrap/checklist.yaml b/tests/fast/stages/s0_bootstrap/checklist.yaml index 8c24a1fdfe..0b1d71d48f 100644 --- a/tests/fast/stages/s0_bootstrap/checklist.yaml +++ b/tests/fast/stages/s0_bootstrap/checklist.yaml @@ -55,9 +55,9 @@ values: module.organization.google_organization_iam_binding.authoritative["roles/cloudasset.owner"]: condition: [] members: - - group:gcp-network-admins@fast.example.com - group:gcp-organization-admins@fast.example.com - group:gcp-security-admins@fast.example.com + - group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/cloudasset.owner module.organization.google_organization_iam_binding.authoritative["roles/cloudsupport.admin"]: @@ -70,8 +70,8 @@ values: condition: [] members: - group:gcp-devops@fast.example.com - - group:gcp-network-admins@fast.example.com - group:gcp-security-admins@fast.example.com + - group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/cloudsupport.techSupportEditor module.organization.google_organization_iam_binding.authoritative["roles/compute.osAdminLogin"]: @@ -131,7 +131,7 @@ values: condition: [] members: - group:gcp-devops@fast.example.com - - group:gcp-network-admins@fast.example.com + - group:gcp-vpc-network-admins@fast.example.com - serviceAccount:fast-prod-bootstrap-0r@fast-prod-iac-core-0.iam.gserviceaccount.com - serviceAccount:fast-prod-resman-0r@fast-prod-iac-core-0.iam.gserviceaccount.com org_id: '123456789012' @@ -240,19 +240,19 @@ values: member: serviceAccount:fast-prod-resman-0r@fast-prod-iac-core-0.iam.gserviceaccount.com org_id: '123456789012' role: roles/billing.viewer - ? module.organization.google_organization_iam_member.bindings["roles/compute.networkAdmin-group:gcp-network-admins@fast.example.com"] + ? module.organization.google_organization_iam_member.bindings["roles/compute.networkAdmin-group:gcp-vpc-network-admins@fast.example.com"] : condition: [] - member: group:gcp-network-admins@fast.example.com + member: group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/compute.networkAdmin - ? module.organization.google_organization_iam_member.bindings["roles/compute.orgFirewallPolicyAdmin-group:gcp-network-admins@fast.example.com"] + ? module.organization.google_organization_iam_member.bindings["roles/compute.orgFirewallPolicyAdmin-group:gcp-vpc-network-admins@fast.example.com"] : condition: [] - member: group:gcp-network-admins@fast.example.com + member: group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/compute.orgFirewallPolicyAdmin - ? module.organization.google_organization_iam_member.bindings["roles/compute.securityAdmin-group:gcp-network-admins@fast.example.com"] + ? module.organization.google_organization_iam_member.bindings["roles/compute.securityAdmin-group:gcp-vpc-network-admins@fast.example.com"] : condition: [] - member: group:gcp-network-admins@fast.example.com + member: group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/compute.securityAdmin ? module.organization.google_organization_iam_member.bindings["roles/compute.viewer-group:gcp-security-admins@fast.example.com"] @@ -260,9 +260,9 @@ values: member: group:gcp-security-admins@fast.example.com org_id: '123456789012' role: roles/compute.viewer - ? module.organization.google_organization_iam_member.bindings["roles/compute.xpnAdmin-group:gcp-network-admins@fast.example.com"] + ? module.organization.google_organization_iam_member.bindings["roles/compute.xpnAdmin-group:gcp-vpc-network-admins@fast.example.com"] : condition: [] - member: group:gcp-network-admins@fast.example.com + member: group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/compute.xpnAdmin ? module.organization.google_organization_iam_member.bindings["roles/container.viewer-group:gcp-security-admins@fast.example.com"] diff --git a/tests/fast/stages/s0_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml index 69908ad358..bdc0ec68cb 100644 --- a/tests/fast/stages/s0_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -16,9 +16,9 @@ values: module.organization.google_organization_iam_binding.authoritative["roles/cloudsupport.techSupportEditor"]: condition: [] members: - - group:gcp-network-admins@fast.example.com - group:gcp-security-admins@fast.example.com - group:gcp-support@example.com + - group:gcp-vpc-network-admins@fast.example.com org_id: '123456789012' role: roles/cloudsupport.techSupportEditor module.organization.google_organization_iam_binding.authoritative["roles/logging.viewer"]: diff --git a/tests/fast/stages/s1_resman/checklist.tfvars b/tests/fast/stages/s1_resman/checklist.tfvars index 88df6b629c..0e303043d1 100644 --- a/tests/fast/stages/s1_resman/checklist.tfvars +++ b/tests/fast/stages/s1_resman/checklist.tfvars @@ -24,7 +24,7 @@ factories_config = { groups = { gcp-billing-admins = "gcp-billing-admins", gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins", + gcp-network-admins = "gcp-vpc-network-admins", gcp-organization-admins = "gcp-organization-admins", gcp-security-admins = "gcp-security-admins", gcp-support = "gcp-support" diff --git a/tests/fast/stages/s1_resman/simple.tfvars b/tests/fast/stages/s1_resman/simple.tfvars index 9cb8528817..8086201d88 100644 --- a/tests/fast/stages/s1_resman/simple.tfvars +++ b/tests/fast/stages/s1_resman/simple.tfvars @@ -21,7 +21,7 @@ custom_roles = { groups = { gcp-billing-admins = "gcp-billing-admins", gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins", + gcp-network-admins = "gcp-vpc-network-admins", gcp-organization-admins = "gcp-organization-admins", gcp-security-admins = "gcp-security-admins", gcp-support = "gcp-support" diff --git a/tests/fast/stages/s2_networking_a_peering/simple.tfvars b/tests/fast/stages/s2_networking_a_peering/simple.tfvars index bdff7a433f..9a117062e3 100644 --- a/tests/fast/stages/s2_networking_a_peering/simple.tfvars +++ b/tests/fast/stages/s2_networking_a_peering/simple.tfvars @@ -19,7 +19,7 @@ folder_ids = { networking-prod = null } groups = { - gcp-network-admins = "gcp-network-admins" + gcp-network-admins = "gcp-vpc-network-admins" } service_accounts = { data-platform-dev = "string" diff --git a/tests/fast/stages/s2_networking_b_vpn/simple.tfvars b/tests/fast/stages/s2_networking_b_vpn/simple.tfvars index 24d3a8e039..b9c4ec9ddb 100644 --- a/tests/fast/stages/s2_networking_b_vpn/simple.tfvars +++ b/tests/fast/stages/s2_networking_b_vpn/simple.tfvars @@ -19,7 +19,7 @@ folder_ids = { networking-prod = null } groups = { - gcp-network-admins = "gcp-network-admins" + gcp-network-admins = "gcp-vpc-network-admins" } service_accounts = { data-platform-dev = "string" diff --git a/tests/fast/stages/s2_networking_c_nva/simple.tfvars b/tests/fast/stages/s2_networking_c_nva/simple.tfvars index fca8913f8a..c2a9cef0bd 100644 --- a/tests/fast/stages/s2_networking_c_nva/simple.tfvars +++ b/tests/fast/stages/s2_networking_c_nva/simple.tfvars @@ -19,7 +19,7 @@ folder_ids = { networking-prod = null } groups = { - gcp-network-admins = "gcp-network-admins" + gcp-network-admins = "gcp-vpc-network-admins" } service_accounts = { data-platform-dev = "string" diff --git a/tests/fast/stages/s2_networking_d_separate_envs/simple.tfvars b/tests/fast/stages/s2_networking_d_separate_envs/simple.tfvars index 071011dd5e..8522e2b2f0 100644 --- a/tests/fast/stages/s2_networking_d_separate_envs/simple.tfvars +++ b/tests/fast/stages/s2_networking_d_separate_envs/simple.tfvars @@ -20,7 +20,7 @@ folder_ids = { networking-prod = null } groups = { - gcp-network-admins = "gcp-network-admins" + gcp-network-admins = "gcp-vpc-network-admins" } service_accounts = { data-platform-dev = "string" diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/simple.tfvars b/tests/fast/stages/s2_networking_e_nva_bgp/simple.tfvars index fca8913f8a..c2a9cef0bd 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/simple.tfvars +++ b/tests/fast/stages/s2_networking_e_nva_bgp/simple.tfvars @@ -19,7 +19,7 @@ folder_ids = { networking-prod = null } groups = { - gcp-network-admins = "gcp-network-admins" + gcp-network-admins = "gcp-vpc-network-admins" } service_accounts = { data-platform-dev = "string" diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml index f0594c4ca6..993910f7e7 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml +++ b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml @@ -740,7 +740,7 @@ values: timeouts: null ? module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-healthchecks"] : action: allow - description: Enable HTTP and HTTPS healthchecks + description: Enable SSH, HTTP and HTTPS healthchecks direction: INGRESS disabled: false enable_logging: null @@ -753,6 +753,7 @@ values: layer4_configs: - ip_protocol: tcp ports: + - "22" - "80" - "443" src_address_groups: null diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars index 52ca76a3db..e3691e9ca5 100644 --- a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars @@ -16,7 +16,7 @@ custom_roles = { groups = { gcp-billing-admins = "gcp-billing-admins", gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins", + gcp-network-admins = "gcp-vpc-network-admins", gcp-organization-admins = "gcp-organization-admins", gcp-security-admins = "gcp-security-admins", gcp-support = "gcp-support" diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars index 33cf461989..9cc9d46c69 100644 --- a/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars +++ b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars @@ -34,7 +34,7 @@ fast_features = { } groups = { gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins", + gcp-network-admins = "gcp-vpc-network-admins", gcp-security-admins = "gcp-security-admins", } organization = { From 7aa6c7e059d5e46a8630259ae5bd1edeefaa3336 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 2 May 2024 22:11:33 +0200 Subject: [PATCH 15/29] Style fixes to FAST log sinks expressions --- fast/stages/0-bootstrap/variables.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf index 26ec513ac9..09c0e08567 100644 --- a/fast/stages/0-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -200,14 +200,14 @@ variable "log_sinks" { } vpc-sc = { filter = <<-FILTER - protoPayload.metadata.@type:"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" + protoPayload.metadata.@type="type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata" FILTER type = "logging" } workspace-audit-logs = { filter = <<-FILTER - log_id("cloudaudit.googleapis.com/data_access") - protoPayload.serviceName:"login.googleapis.com" + log_id("cloudaudit.googleapis.com/data_access") AND + protoPayload.serviceName="login.googleapis.com" FILTER type = "logging" } From c9503d5ac506f4bd4c83afdc57f99a97463bdb62 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 9 May 2024 15:09:54 +0200 Subject: [PATCH 16/29] Remove data source from folder module (#2260) * remove data source from folder module * fix fast tfdoc * fix locals type error * fix folder test * fix fast test --- fast/stages/0-bootstrap/README.md | 2 +- modules/folder/iam.tf | 6 +++--- modules/folder/logging.tf | 9 ++++----- modules/folder/main.tf | 15 +++++---------- modules/folder/organization-policies.tf | 4 ++-- modules/folder/outputs.tf | 6 +++--- modules/folder/tags.tf | 2 +- .../stages/s2_networking_e_nva_bgp/simple.yaml | 2 -- .../s0_bootstrap_tenant/simple.yaml | 4 ++-- .../test_plan_org_policies_modules.py | 8 ++++---- 10 files changed, 25 insertions(+), 33 deletions(-) diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index 43060232fe..5d456302f5 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -655,7 +655,7 @@ The `fast_features` variable consists of 6 toggles: | [iam_bindings_additive](variables.tf#L141) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | | [iam_by_principals](variables.tf#L156) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | | [locations](variables.tf#L163) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | | -| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | | [org_policies_config](variables.tf#L224) | Organization policies customization. | object({…}) | | {} | | | [outputs_location](variables.tf#L250) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | | [project_parent_ids](variables.tf#L265) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {} | | diff --git a/modules/folder/iam.tf b/modules/folder/iam.tf index ba852837cb..68d3e606b0 100644 --- a/modules/folder/iam.tf +++ b/modules/folder/iam.tf @@ -35,14 +35,14 @@ locals { resource "google_folder_iam_binding" "authoritative" { for_each = local.iam - folder = local.folder.name + folder = local.folder_id role = each.key members = each.value } resource "google_folder_iam_binding" "bindings" { for_each = var.iam_bindings - folder = local.folder.name + folder = local.folder_id role = each.value.role members = each.value.members dynamic "condition" { @@ -57,7 +57,7 @@ resource "google_folder_iam_binding" "bindings" { resource "google_folder_iam_member" "bindings" { for_each = var.iam_bindings_additive - folder = local.folder.name + folder = local.folder_id role = each.value.role member = each.value.member dynamic "condition" { diff --git a/modules/folder/logging.tf b/modules/folder/logging.tf index f6942baa4b..817c4d1e06 100644 --- a/modules/folder/logging.tf +++ b/modules/folder/logging.tf @@ -37,7 +37,7 @@ locals { resource "google_folder_iam_audit_config" "default" { for_each = var.logging_data_access - folder = local.folder.name + folder = local.folder_id service = each.key dynamic "audit_log_config" { for_each = each.value @@ -53,7 +53,7 @@ resource "google_logging_folder_sink" "sink" { for_each = local.logging_sinks name = each.key description = coalesce(each.value.description, "${each.key} (Terraform-managed).") - folder = local.folder.name + folder = local.folder_id destination = "${each.value.type}.googleapis.com/${each.value.destination}" filter = each.value.filter include_children = each.value.include_children @@ -108,10 +108,9 @@ resource "google_project_iam_member" "bucket-sinks-binding" { project = split("/", each.value.destination)[1] role = "roles/logging.bucketWriter" member = google_logging_folder_sink.sink[each.key].writer_identity - condition { title = "${each.key} bucket writer" - description = "Grants bucketWriter to ${google_logging_folder_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${local.folder.id}" + description = "Grants bucketWriter to ${google_logging_folder_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${local.folder_id}" expression = "resource.name.endsWith('${each.value.destination}')" } } @@ -126,7 +125,7 @@ resource "google_project_iam_member" "project-sinks-binding" { resource "google_logging_folder_exclusion" "logging-exclusion" { for_each = var.logging_exclusions name = each.key - folder = local.folder.name + folder = local.folder_id description = "${each.key} (Terraform-managed)." filter = each.value } diff --git a/modules/folder/main.tf b/modules/folder/main.tf index 4335dbe515..8bb46a533c 100644 --- a/modules/folder/main.tf +++ b/modules/folder/main.tf @@ -15,18 +15,13 @@ */ locals { - folder = ( + folder_id = ( var.folder_create - ? try(google_folder.folder[0], null) - : try(data.google_folder.folder[0], null) + ? try(google_folder.folder[0].id, null) + : var.id ) } -data "google_folder" "folder" { - count = var.folder_create ? 0 : 1 - folder = var.id -} - resource "google_folder" "folder" { count = var.folder_create ? 1 : 0 display_name = var.name @@ -36,7 +31,7 @@ resource "google_folder" "folder" { resource "google_essential_contacts_contact" "contact" { provider = google-beta for_each = var.contacts - parent = local.folder.name + parent = local.folder_id email = each.key language_tag = "en" notification_category_subscriptions = each.value @@ -49,7 +44,7 @@ resource "google_essential_contacts_contact" "contact" { resource "google_compute_firewall_policy_association" "default" { count = var.firewall_policy == null ? 0 : 1 - attachment_target = local.folder.id + attachment_target = local.folder_id name = var.firewall_policy.name firewall_policy = var.firewall_policy.policy } diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 38b69871c9..6de3081ce8 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -52,8 +52,8 @@ locals { org_policies = { for k, v in local._org_policies : k => merge(v, { - name = "${local.folder.name}/policies/${k}" - parent = local.folder.name + name = "${local.folder_id}/policies/${k}" + parent = local.folder_id is_boolean_policy = ( alltrue([for r in v.rules : r.allow == null && r.deny == null]) ) diff --git a/modules/folder/outputs.tf b/modules/folder/outputs.tf index aec28f7152..79ff37cb7d 100644 --- a/modules/folder/outputs.tf +++ b/modules/folder/outputs.tf @@ -16,12 +16,12 @@ output "folder" { description = "Folder resource." - value = local.folder + value = try(google_folder.folder[0], null) } output "id" { description = "Fully qualified folder id." - value = local.folder.name + value = local.folder_id depends_on = [ google_folder_iam_binding.authoritative, google_folder_iam_binding.bindings, @@ -32,7 +32,7 @@ output "id" { output "name" { description = "Folder name." - value = local.folder.display_name + value = try(google_folder.folder[0].display_name, null) } output "sink_writer_identities" { diff --git a/modules/folder/tags.tf b/modules/folder/tags.tf index 2cd2f2fd1a..323886ef50 100644 --- a/modules/folder/tags.tf +++ b/modules/folder/tags.tf @@ -16,6 +16,6 @@ resource "google_tags_tag_binding" "binding" { for_each = coalesce(var.tag_bindings, {}) - parent = "//cloudresourcemanager.googleapis.com/${local.folder.id}" + parent = "//cloudresourcemanager.googleapis.com/${local.folder_id}" tag_value = each.value } diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml index 993910f7e7..d9b2d67c9c 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml +++ b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml @@ -1520,8 +1520,6 @@ values: log_config: - enable: false filter: ALL - max_ports_per_vm: 65536 - min_ports_per_vm: 64 name: ew1 nat_ip_allocate_option: AUTO_ONLY nat_ips: null diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml index 3e84c00ef6..f6f0d3ea26 100644 --- a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml @@ -14,7 +14,7 @@ counts: google_bigquery_default_service_account: 2 - google_folder: 2 + google_folder: 1 google_folder_iam_binding: 5 google_organization_iam_member: 39 google_project: 2 @@ -30,4 +30,4 @@ counts: google_tags_tag_binding: 1 google_tags_tag_value: 1 modules: 19 - resources: 129 + resources: 128 diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py index 632b45e726..f8839fc251 100644 --- a/tests/modules/organization/test_plan_org_policies_modules.py +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -37,8 +37,8 @@ def test_policy_implementation(): '@@ -55,2 +55,2 @@\n', '- name = "projects/${local.project.project_id}/policies/${k}"\n', '- parent = "projects/${local.project.project_id}"\n', - '+ name = "${local.folder.name}/policies/${k}"\n', - '+ parent = local.folder.name\n', + '+ name = "${local.folder_id}/policies/${k}"\n', + '+ parent = local.folder_id\n', ] diff2 = difflib.unified_diff(lines['folder'], lines['organization'], 'folder', @@ -50,8 +50,8 @@ def test_policy_implementation(): '-# tfdoc:file:description Folder-level organization policies.\n', '+# tfdoc:file:description Organization-level organization policies.\n', '@@ -55,2 +55,2 @@\n', - '- name = "${local.folder.name}/policies/${k}"\n', - '- parent = local.folder.name\n', + '- name = "${local.folder_id}/policies/${k}"\n', + '- parent = local.folder_id\n', '+ name = "${var.organization_id}/policies/${k}"\n', '+ parent = var.organization_id\n', '@@ -113,0 +114,9 @@\n', From c58850c096df0a194dc1437512267f99e4ca2d9d Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 9 May 2024 15:24:41 +0200 Subject: [PATCH 17/29] Add Hybrid NAT support (#2261) * Updates to support hybid NAT * Fix readme * Fix variable order --- modules/net-cloudnat/README.md | 61 +++++++++++- modules/net-cloudnat/main.tf | 25 +++-- modules/net-cloudnat/variables.tf | 42 ++++++--- modules/net-vpc/README.md | 12 ++- modules/net-vpc/outputs.tf | 7 +- modules/net-vpc/subnets.tf | 20 +++- modules/net-vpc/variables.tf | 14 ++- .../s2_networking_e_nva_bgp/simple.yaml | 4 +- .../modules/net_cloudnat/examples/hybrid.yaml | 94 +++++++++++++++++++ 9 files changed, 247 insertions(+), 32 deletions(-) create mode 100644 tests/modules/net_cloudnat/examples/hybrid.yaml diff --git a/modules/net-cloudnat/README.md b/modules/net-cloudnat/README.md index 407bce3f08..34fb560660 100644 --- a/modules/net-cloudnat/README.md +++ b/modules/net-cloudnat/README.md @@ -6,6 +6,7 @@ Simple Cloud NAT management, with optional router creation. - [Basic Example](#basic-example) - [Subnetwork configuration](#subnetwork-configuration) - [Reserved IPs and custom rules](#reserved-ips-and-custom-rules) +- [Hybrid NAT](#hybrid-nat) - [Variables](#variables) - [Outputs](#outputs) @@ -102,6 +103,59 @@ module "nat" { } # tftest modules=2 resources=5 inventory=rules.yaml e2e ``` +## Hybrid NAT +```hcl +module "vpc1" { + source = "./fabric/modules/net-vpc" + project_id = var.project_id + name = "vpc1" + subnets = [ + { + ip_cidr_range = "10.0.0.0/24" + name = "vpc1-subnet" + region = var.region + } + ] + subnets_private_nat = [ + { + ip_cidr_range = "192.168.15.0/24" + name = "vpc1-nat" + region = var.region + } + ] +} + +module "vpc1-nat" { + source = "./fabric/modules/net-cloudnat" + project_id = var.project_id + region = var.region + name = "vpc1-nat" + type = "PRIVATE" + router_network = module.vpc1.id + config_source_subnetworks = { + all = false + subnetworks = [ + { + self_link = module.vpc1.subnet_ids["${var.region}/vpc1-subnet"] + } + ] + } + config_port_allocation = { + enable_endpoint_independent_mapping = false + enable_dynamic_port_allocation = true + } + rules = [ + { + description = "private nat" + match = "nexthop.is_hybrid" + source_ranges = [ + module.vpc1.subnets_private_nat["${var.region}/vpc1-nat"].id + ] + } + ] +} +# tftest modules=2 resources=7 inventory=hybrid.yaml +``` ## Variables @@ -111,15 +165,16 @@ module "nat" { | [project_id](variables.tf#L82) | Project where resources will be created. | string | ✓ | | | [region](variables.tf#L87) | 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_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#L58) | Timeout configurations. | object({…}) | | {} | +| [config_timeouts](variables.tf#L58) | Timeout configurations. | object({…}) | | {} | | [logging_filter](variables.tf#L71) | Enables logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string | | null | | [router_asn](variables.tf#L92) | Router ASN used for auto-created router. | number | | null | | [router_create](variables.tf#L98) | Create router. | bool | | true | | [router_name](variables.tf#L104) | Router name, leave blank if router will be created to use auto generated name. | string | | null | | [router_network](variables.tf#L110) | Name of the VPC used for auto-created router. | string | | null | -| [rules](variables.tf#L116) | List of rules associated with this NAT. | list(object({…})) | | [] | +| [rules](variables.tf#L116) | List of rules associated with this NAT. | list(object({…})) | | [] | +| [type](variables.tf#L136) | 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 7a537d8eee..bf5b7bc785 100644 --- a/modules/net-cloudnat/main.tf +++ b/modules/net-cloudnat/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,13 +47,21 @@ resource "google_compute_router" "router" { } resource "google_compute_router_nat" "nat" { - project = var.project_id - region = var.region - name = var.name - router = local.router_name - nat_ips = var.addresses + provider = google-beta + project = var.project_id + region = var.region + name = var.name + type = var.type + router = local.router_name + nat_ips = var.addresses nat_ip_allocate_option = ( - length(var.addresses) > 0 ? "MANUAL_ONLY" : "AUTO_ONLY" + var.type == "PRIVATE" + ? null + : ( + length(var.addresses) > 0 + ? "MANUAL_ONLY" + : "AUTO_ONLY" + ) ) source_subnetwork_ip_ranges_to_nat = local.subnet_config icmp_idle_timeout_sec = var.config_timeouts.icmp @@ -114,7 +122,8 @@ resource "google_compute_router_nat" "nat" { description = rules.value.description match = rules.value.match action { - source_nat_active_ips = rules.value.source_ips + source_nat_active_ips = rules.value.source_ips + source_nat_active_ranges = rules.value.source_ranges } } } diff --git a/modules/net-cloudnat/variables.tf b/modules/net-cloudnat/variables.tf index 0ec05e4221..27484ee319 100644 --- a/modules/net-cloudnat/variables.tf +++ b/modules/net-cloudnat/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ variable "config_port_allocation" { type = object({ enable_endpoint_independent_mapping = optional(bool, true) enable_dynamic_port_allocation = optional(bool, false) - min_ports_per_vm = optional(number, 64) - max_ports_per_vm = optional(number, 65536) + min_ports_per_vm = optional(number) + max_ports_per_vm = optional(number) }) default = {} nullable = false @@ -58,11 +58,11 @@ output "foo" { variable "config_timeouts" { description = "Timeout configurations." type = object({ - icmp = optional(number, 30) - tcp_established = optional(number, 1200) - tcp_time_wait = optional(number, 120) - tcp_transitory = optional(number, 30) - udp = optional(number, 30) + icmp = optional(number) + tcp_established = optional(number) + tcp_time_wait = optional(number) + tcp_transitory = optional(number) + udp = optional(number) }) default = {} nullable = false @@ -116,10 +116,30 @@ variable "router_network" { variable "rules" { description = "List of rules associated with this NAT." type = list(object({ - description = optional(string), - match = string - source_ips = list(string) + description = optional(string) + match = string + source_ips = optional(list(string)) + source_ranges = optional(list(string)) })) default = [] nullable = false + validation { + condition = alltrue([ + for r in var.rules : + r.source_ips != null || r.source_ranges != null + ]) + + error_message = "All rules must specify either source_ips or source_ranges." + } +} + +variable "type" { + description = "Whether this Cloud NAT is used for public or private IP translation. One of 'PUBLIC' or 'PRIVATE'." + type = string + default = "PUBLIC" + nullable = false + validation { + condition = var.type == "PUBLIC" || var.type == "PRIVATE" + error_message = "Field type must be either PUBLIC or PRIVATE." + } } diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index feee775cc6..d29251cc96 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -662,9 +662,10 @@ module "vpc" { | [shared_vpc_host](variables.tf#L238) | Enable shared VPC for this project. | bool | | false | | [shared_vpc_service_projects](variables.tf#L244) | Shared VPC service projects to register with this host. | list(string) | | [] | | [subnets](variables.tf#L250) | Subnet configuration. | list(object({…})) | | [] | -| [subnets_proxy_only](variables.tf#L297) | 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#L331) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | -| [vpc_create](variables.tf#L363) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | +| [subnets_private_nat](variables.tf#L297) | List of private NAT subnets. | list(object({…})) | | [] | +| [subnets_proxy_only](variables.tf#L309) | 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#L343) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | +| [vpc_create](variables.tf#L375) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | ## Outputs @@ -684,6 +685,7 @@ module "vpc" { | [subnet_secondary_ranges](outputs.tf#L122) | Map of subnet secondary ranges keyed by name. | | | [subnet_self_links](outputs.tf#L133) | Map of subnet self links keyed by name. | | | [subnets](outputs.tf#L142) | Subnet resources. | | -| [subnets_proxy_only](outputs.tf#L151) | L7 ILB or L7 Regional LB subnet resources. | | -| [subnets_psc](outputs.tf#L156) | Private Service Connect subnet resources. | | +| [subnets_private_nat](outputs.tf#L151) | Private NAT subnet resources. | | +| [subnets_proxy_only](outputs.tf#L156) | L7 ILB or L7 Regional LB subnet resources. | | +| [subnets_psc](outputs.tf#L161) | Private Service Connect subnet resources. | | diff --git a/modules/net-vpc/outputs.tf b/modules/net-vpc/outputs.tf index 412e363a4a..4d143e829f 100644 --- a/modules/net-vpc/outputs.tf +++ b/modules/net-vpc/outputs.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -148,6 +148,11 @@ output "subnets" { ] } +output "subnets_private_nat" { + description = "Private NAT subnet resources." + value = { for k, v in google_compute_subnetwork.private_nat : k => v } +} + output "subnets_proxy_only" { description = "L7 ILB or L7 Regional LB subnet resources." value = { for k, v in google_compute_subnetwork.proxy_only : k => v } diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf index ca3541450c..09d4748d00 100644 --- a/modules/net-vpc/subnets.tf +++ b/modules/net-vpc/subnets.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,6 +124,10 @@ locals { { for s in var.subnets_proxy_only : "${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 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 k, v in local._factory_subnets : k => v if v._is_psc } @@ -185,6 +189,20 @@ resource "google_compute_subnetwork" "proxy_only" { role = each.value.active ? "ACTIVE" : "BACKUP" } +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 + description = coalesce( + each.value.description, + "Terraform-managed private NAT subnet." + ) + purpose = "PRIVATE_NAT" +} + resource "google_compute_subnetwork" "psc" { for_each = local.subnets_psc project = var.project_id diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf index d8948a2640..6fd011bec8 100644 --- a/modules/net-vpc/variables.tf +++ b/modules/net-vpc/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -294,6 +294,18 @@ variable "subnets" { nullable = false } +variable "subnets_private_nat" { + description = "List of private NAT subnets." + type = list(object({ + name = string + ip_cidr_range = string + region = string + description = optional(string) + })) + default = [] + nullable = false +} + variable "subnets_proxy_only" { description = "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." type = list(object({ diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml index d9b2d67c9c..38a23789d5 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml +++ b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml @@ -1520,6 +1520,7 @@ values: log_config: - enable: false filter: ALL + max_ports_per_vm: null name: ew1 nat_ip_allocate_option: AUTO_ONLY nat_ips: null @@ -1551,8 +1552,7 @@ values: log_config: - enable: false filter: ALL - max_ports_per_vm: 65536 - min_ports_per_vm: 64 + max_ports_per_vm: null name: ew4 nat_ip_allocate_option: AUTO_ONLY nat_ips: null diff --git a/tests/modules/net_cloudnat/examples/hybrid.yaml b/tests/modules/net_cloudnat/examples/hybrid.yaml new file mode 100644 index 0000000000..4dd1939598 --- /dev/null +++ b/tests/modules/net_cloudnat/examples/hybrid.yaml @@ -0,0 +1,94 @@ +# Copyright 2024 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.vpc1-nat.google_compute_router.router[0]: + bgp: [] + description: null + encrypted_interconnect_router: null + name: vpc1-nat-nat + project: project-id + region: europe-west8 + module.vpc1-nat.google_compute_router_nat.nat: + drain_nat_ips: null + enable_dynamic_port_allocation: true + enable_endpoint_independent_mapping: false + icmp_idle_timeout_sec: 30 + log_config: + - enable: false + filter: ALL + max_ports_per_vm: null + name: vpc1-nat + nat_ip_allocate_option: null + nat_ips: null + project: project-id + region: europe-west8 + router: vpc1-nat-nat + rules: + - action: + - source_nat_active_ips: [] + source_nat_drain_ips: [] + source_nat_drain_ranges: [] + description: private nat + match: nexthop.is_hybrid + rule_number: 0 + source_subnetwork_ip_ranges_to_nat: LIST_OF_SUBNETWORKS + subnetwork: + - 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 + type: PRIVATE + udp_idle_timeout_sec: 30 + module.vpc1.google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: vpc1 + network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL + project: project-id + routing_mode: GLOBAL + module.vpc1.google_compute_subnetwork.private_nat["europe-west8/vpc1-nat"]: + description: Terraform-managed private NAT subnet. + ip_cidr_range: 192.168.15.0/24 + ipv6_access_type: null + log_config: [] + name: vpc1-nat + network: vpc1 + project: project-id + purpose: PRIVATE_NAT + region: europe-west8 + role: null + module.vpc1.google_compute_subnetwork.subnetwork["europe-west8/vpc1-subnet"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + ipv6_access_type: null + log_config: [] + name: vpc1-subnet + network: vpc1 + private_ip_google_access: true + project: project-id + region: europe-west8 + role: null + secondary_ip_range: [] + +counts: + google_compute_network: 1 + google_compute_route: 2 + google_compute_router: 1 + google_compute_router_nat: 1 + google_compute_subnetwork: 2 From d838c4ac478ea71ec30f69c06dcbe7173b07f08d Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 9 May 2024 18:29:25 +0200 Subject: [PATCH 18/29] Make Simple NVA route IAP traffic through NIC 0 (#2262) --- .../simple-nva/cloud-config.yaml | 5 +- .../s2_networking_e_nva_bgp/simple.yaml | 1889 +++++++++-------- 2 files changed, 949 insertions(+), 945 deletions(-) diff --git a/modules/cloud-config-container/simple-nva/cloud-config.yaml b/modules/cloud-config-container/simple-nva/cloud-config.yaml index 328ace7e2c..b5553793c0 100644 --- a/modules/cloud-config-container/simple-nva/cloud-config.yaml +++ b/modules/cloud-config-container/simple-nva/cloud-config.yaml @@ -1,6 +1,6 @@ #cloud-config -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -51,6 +51,9 @@ write_files: %{ endfor ~} iptables -t nat -A POSTROUTING -o ${interface.name} -j MASQUERADE %{ endif ~} +%{ if interface.number == 0 ~} + ip route add 35.235.240.0/20 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway -H "Metadata-Flavor:Google"` dev ${interface.name} +%{ endif ~} %{ for route in interface.routes ~} ip route add ${route} via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/${interface.number}/gateway -H "Metadata-Flavor:Google"` dev ${interface.name} %{ endfor ~} diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml index 38a23789d5..25ba7ccf9b 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml +++ b/tests/fast/stages/s2_networking_e_nva_bgp/simple.yaml @@ -10,7 +10,6 @@ # 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_address.nva_static_ip_dmz["primary-b"]: @@ -113,22 +112,21 @@ values: alert_strategy: [] combiner: OR conditions: - - condition_absent: [] - condition_matched_log: [] - condition_monitoring_query_language: - - duration: 120s - evaluation_missing_data: null - query: - fetch vpn_gateway| { metric vpn.googleapis.com/network/sent_bytes_count; - metric vpn.googleapis.com/network/received_bytes_count }| align rate (1m)| - group_by [metric.tunnel_name]| outer_join 0,0| value val(0) + val(1)| condition - val() > 187.5 "MBy/s" - trigger: - - count: 1 - percent: null - condition_prometheus_query_language: [] - condition_threshold: [] - display_name: VPN Tunnel Bandwidth usage + - condition_absent: [] + condition_matched_log: [] + condition_monitoring_query_language: + - duration: 120s + evaluation_missing_data: null + query: fetch vpn_gateway| { metric vpn.googleapis.com/network/sent_bytes_count; + metric vpn.googleapis.com/network/received_bytes_count }| align rate (1m)| + group_by [metric.tunnel_name]| outer_join 0,0| value val(0) + val(1)| condition + val() > 187.5 "MBy/s" + trigger: + - count: 1 + percent: null + condition_prometheus_query_language: [] + condition_threshold: [] + display_name: VPN Tunnel Bandwidth usage display_name: VPN Tunnel Bandwidth usage documentation: [] enabled: true @@ -141,21 +139,20 @@ values: alert_strategy: [] combiner: OR conditions: - - condition_absent: [] - condition_matched_log: [] - condition_monitoring_query_language: - - duration: 120s - evaluation_missing_data: null - query: - "fetch vpn_gateway| metric vpn.googleapis.com/tunnel_established| group_by - 5m, [value_tunnel_established_max: max(value.tunnel_established)]| every - 5m| condition val() < 1 '1'" - trigger: - - count: 1 - percent: null - condition_prometheus_query_language: [] - condition_threshold: [] - display_name: VPN Tunnel Established + - condition_absent: [] + condition_matched_log: [] + condition_monitoring_query_language: + - duration: 120s + evaluation_missing_data: null + query: 'fetch vpn_gateway| metric vpn.googleapis.com/tunnel_established| group_by + 5m, [value_tunnel_established_max: max(value.tunnel_established)]| every + 5m| condition val() < 1 ''1''' + trigger: + - count: 1 + percent: null + condition_prometheus_query_language: [] + condition_threshold: [] + display_name: VPN Tunnel Established display_name: VPN Tunnel Established documentation: [] enabled: true @@ -165,17 +162,15 @@ values: timeouts: null user_labels: null google_monitoring_dashboard.dashboard["firewall_insights.json"]: - dashboard_json: - '{"displayName":"Firewall Insights Monitoring","gridLayout":{"columns":"2","widgets":[{"title":"Subnet + dashboard_json: '{"displayName":"Firewall Insights Monitoring","gridLayout":{"columns":"2","widgets":[{"title":"Subnet Firewall Hit Counts","xyChart":{"chartOptions":{"mode":"COLOR"},"dataSets":[{"minAlignmentPeriod":"60s","plotType":"LINE","targetAxis":"Y1","timeSeriesQuery":{"timeSeriesFilter":{"aggregation":{"perSeriesAligner":"ALIGN_RATE"},"filter":"metric.type=\"firewallinsights.googleapis.com/subnet/firewall_hit_count\" resource.type=\"gce_subnetwork\"","secondaryAggregation":{}},"unitOverride":"1"}}],"timeshiftDuration":"0s","yAxis":{"label":"y1Axis","scale":"LINEAR"}}},{"title":"VM Firewall Hit Counts","xyChart":{"chartOptions":{"mode":"COLOR"},"dataSets":[{"minAlignmentPeriod":"60s","plotType":"LINE","targetAxis":"Y1","timeSeriesQuery":{"timeSeriesFilter":{"aggregation":{"perSeriesAligner":"ALIGN_RATE"},"filter":"metric.type=\"firewallinsights.googleapis.com/vm/firewall_hit_count\" resource.type=\"gce_instance\"","secondaryAggregation":{}},"unitOverride":"1"}}],"timeshiftDuration":"0s","yAxis":{"label":"y1Axis","scale":"LINEAR"}}}]}}' project: fast2-prod-net-landing-0 timeouts: null - ? google_monitoring_dashboard.dashboard["vpc_and_vpc_peering_group_quotas.json"] - : dashboard_json: - '{"dashboardFilters":[],"displayName":"VPC \u0026 VPC Peering + google_monitoring_dashboard.dashboard["vpc_and_vpc_peering_group_quotas.json"]: + dashboard_json: '{"dashboardFilters":[],"displayName":"VPC \u0026 VPC Peering Group Quotas","labels":{},"mosaicLayout":{"columns":12,"tiles":[{"height":4,"widget":{"title":"Internal network (L4) Load Balancers per VPC Peering Group","xyChart":{"chartOptions":{"mode":"COLOR"},"dataSets":[{"breakdowns":[],"dimensions":[],"measures":[],"plotType":"LINE","targetAxis":"Y1","timeSeriesQuery":{"timeSeriesQueryLanguage":"fetch compute.googleapis.com/VpcNetwork\n|{ metric\n compute.googleapis.com/quota/internal_lb_forwarding_rules_per_peering_group/usage\n | @@ -228,8 +223,7 @@ values: project: fast2-prod-net-landing-0 timeouts: null google_monitoring_dashboard.dashboard["vpn.json"]: - dashboard_json: - '{"displayName":"VPN Monitoring","mosaicLayout":{"columns":12,"tiles":[{"height":4,"widget":{"title":"Number + dashboard_json: '{"displayName":"VPN Monitoring","mosaicLayout":{"columns":12,"tiles":[{"height":4,"widget":{"title":"Number of connections","xyChart":{"chartOptions":{"mode":"COLOR"},"dataSets":[{"minAlignmentPeriod":"60s","plotType":"LINE","targetAxis":"Y1","timeSeriesQuery":{"timeSeriesFilter":{"aggregation":{"perSeriesAligner":"ALIGN_MEAN"},"filter":"metric.type=\"vpn.googleapis.com/gateway/connections\" resource.type=\"vpn_gateway\"","secondaryAggregation":{}},"unitOverride":"1"}}],"timeshiftDuration":"0s","yAxis":{"label":"y1Axis","scale":"LINEAR"}}},"width":4},{"height":4,"widget":{"title":"Tunnel established","xyChart":{"chartOptions":{"mode":"COLOR"},"dataSets":[{"minAlignmentPeriod":"60s","plotType":"LINE","targetAxis":"Y1","timeSeriesQuery":{"timeSeriesFilter":{"aggregation":{"perSeriesAligner":"ALIGN_MEAN"},"filter":"metric.type=\"vpn.googleapis.com/tunnel_established\" @@ -279,9 +273,9 @@ values: source: null temporary_hold: null timeouts: null - ? module.dev-dns-peer-landing-rev-10.google_dns_managed_zone.dns_managed_zone[0] - : cloud_logging_config: - - enable_logging: false + module.dev-dns-peer-landing-rev-10.google_dns_managed_zone.dns_managed_zone[0]: + cloud_logging_config: + - enable_logging: false description: Terraform managed. dns_name: 10.in-addr.arpa. dnssec_config: [] @@ -296,7 +290,7 @@ values: visibility: private module.dev-dns-peer-landing-root.google_dns_managed_zone.dns_managed_zone[0]: cloud_logging_config: - - enable_logging: false + - enable_logging: false description: Terraform managed. dns_name: . dnssec_config: [] @@ -311,7 +305,7 @@ values: visibility: private module.dev-dns-private-zone.google_dns_managed_zone.dns_managed_zone[0]: cloud_logging_config: - - enable_logging: false + - enable_logging: false description: Terraform managed. dns_name: dev.gcp.example.com. dnssec_config: [] @@ -324,23 +318,23 @@ values: service_directory_config: [] timeouts: null visibility: private - ? module.dev-dns-private-zone.google_dns_record_set.dns_record_set["A localhost"] - : managed_zone: dev-gcp-example-com + module.dev-dns-private-zone.google_dns_record_set.dns_record_set["A localhost"]: + managed_zone: dev-gcp-example-com name: localhost.dev.gcp.example.com. project: fast2-dev-net-spoke-0 routing_policy: [] rrdatas: - - 127.0.0.1 + - 127.0.0.1 ttl: 300 type: A - ? module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-allow-composer-nodes"] - : allow: - - ports: - - "80" - - "443" - - "3306" - - "3307" - protocol: tcp + module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-allow-composer-nodes"]: + allow: + - ports: + - '80' + - '443' + - '3306' + - '3307' + protocol: tcp deny: [] description: Allow traffic to Composer nodes. direction: INGRESS @@ -353,17 +347,17 @@ values: source_ranges: null source_service_accounts: null source_tags: - - composer-worker + - composer-worker target_service_accounts: null target_tags: - - composer-worker - timeouts: null - ? module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-allow-dataflow-load"] - : allow: - - ports: - - "12345" - - "12346" - protocol: tcp + - composer-worker + timeouts: null + module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-allow-dataflow-load"]: + allow: + - ports: + - '12345' + - '12346' + protocol: tcp deny: [] description: Allow traffic to Dataflow nodes. direction: INGRESS @@ -376,37 +370,37 @@ values: source_ranges: null source_service_accounts: null source_tags: - - dataflow + - dataflow target_service_accounts: null target_tags: - - dataflow + - dataflow timeouts: null - ? module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-default-deny"] - : allow: [] + module.dev-spoke-firewall.google_compute_firewall.custom-rules["ingress-default-deny"]: + allow: [] deny: - - ports: [] - protocol: all + - ports: [] + protocol: all description: Deny and log any unmatched ingress traffic. direction: INGRESS disabled: false log_config: - - metadata: EXCLUDE_ALL_METADATA + - metadata: EXCLUDE_ALL_METADATA name: ingress-default-deny network: dev-spoke-0 priority: 65535 project: fast2-dev-net-spoke-0 source_ranges: - - 0.0.0.0/0 + - 0.0.0.0/0 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: null timeouts: null - ? module.dev-spoke-project.google_compute_shared_vpc_host_project.shared_vpc_host[0] - : project: fast2-dev-net-spoke-0 + module.dev-spoke-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: fast2-dev-net-spoke-0 timeouts: null - ? module.dev-spoke-project.google_monitoring_monitored_project.primary["fast2-prod-net-landing-0"] - : metrics_scope: fast2-prod-net-landing-0 + module.dev-spoke-project.google_monitoring_monitored_project.primary["fast2-prod-net-landing-0"]: + metrics_scope: fast2-prod-net-landing-0 name: fast2-dev-net-spoke-0 timeouts: null module.dev-spoke-project.google_project.project[0]: @@ -419,69 +413,69 @@ values: project_id: fast2-dev-net-spoke-0 skip_delete: false timeouts: null - ? module.dev-spoke-project.google_project_iam_binding.authoritative["roles/dns.admin"] - : condition: [] + module.dev-spoke-project.google_project_iam_binding.authoritative["roles/dns.admin"]: + condition: [] members: - - serviceAccount:string + - serviceAccount:string project: fast2-dev-net-spoke-0 role: roles/dns.admin - ? module.dev-spoke-project.google_project_iam_binding.bindings["sa_delegated_grants"] - : condition: - - description: Development host project delegated grants. - expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/composer.sharedVpcAgent','roles/compute.networkUser','roles/compute.networkViewer','roles/container.hostServiceAgentUser','roles/multiclusterservicediscovery.serviceAgent','roles/vpcaccess.user']) - title: dev_stage3_sa_delegated_grants + module.dev-spoke-project.google_project_iam_binding.bindings["sa_delegated_grants"]: + condition: + - description: Development host project delegated grants. + expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/composer.sharedVpcAgent','roles/compute.networkUser','roles/compute.networkViewer','roles/container.hostServiceAgentUser','roles/multiclusterservicediscovery.serviceAgent','roles/vpcaccess.user']) + title: dev_stage3_sa_delegated_grants members: - - serviceAccount:string + - serviceAccount:string project: fast2-dev-net-spoke-0 role: roles/resourcemanager.projectIamAdmin module.dev-spoke-project.google_project_iam_member.servicenetworking[0]: condition: [] project: fast2-dev-net-spoke-0 role: roles/servicenetworking.serviceAgent - ? module.dev-spoke-project.google_project_service.project_services["compute.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["compute.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: compute.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["dns.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["dns.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: dns.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["iap.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["iap.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: iap.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["networkmanagement.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["networkmanagement.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: networkmanagement.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["servicenetworking.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["servicenetworking.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: servicenetworking.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["stackdriver.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["stackdriver.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: stackdriver.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service.project_services["vpcaccess.googleapis.com"] - : disable_dependent_services: false + module.dev-spoke-project.google_project_service.project_services["vpcaccess.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-dev-net-spoke-0 service: vpcaccess.googleapis.com timeouts: null - ? module.dev-spoke-project.google_project_service_identity.jit_si["iap.googleapis.com"] - : project: fast2-dev-net-spoke-0 + module.dev-spoke-project.google_project_service_identity.jit_si["iap.googleapis.com"]: + project: fast2-dev-net-spoke-0 service: iap.googleapis.com timeouts: null module.dev-spoke-project.google_project_service_identity.servicenetworking[0]: @@ -525,8 +519,8 @@ values: project: fast2-dev-net-spoke-0 tags: null timeouts: null - ? module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-dataplatform"] - : description: Default subnet for dev Data Platform + module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-dataplatform"]: + description: Default subnet for dev Data Platform ip_cidr_range: 10.68.2.0/24 ipv6_access_type: null log_config: [] @@ -537,13 +531,13 @@ values: region: europe-west1 role: null secondary_ip_range: - - ip_cidr_range: 100.69.0.0/16 - range_name: pods - - ip_cidr_range: 100.71.2.0/24 - range_name: services + - ip_cidr_range: 100.69.0.0/16 + range_name: pods + - ip_cidr_range: 100.71.2.0/24 + range_name: services timeouts: null - ? module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-default"] - : description: Default europe-west1 subnet for dev + module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-default"]: + description: Default europe-west1 subnet for dev ip_cidr_range: 10.68.0.0/24 ipv6_access_type: null log_config: [] @@ -555,8 +549,8 @@ values: role: null secondary_ip_range: [] timeouts: null - ? module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-gke-nodes"] - : description: Default subnet for prod gke nodes + module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/dev-gke-nodes"]: + description: Default subnet for prod gke nodes ip_cidr_range: 10.68.1.0/24 ipv6_access_type: null log_config: [] @@ -567,13 +561,13 @@ values: region: europe-west1 role: null secondary_ip_range: - - ip_cidr_range: 100.68.0.0/16 - range_name: pods - - ip_cidr_range: 100.71.1.0/24 - range_name: services + - ip_cidr_range: 100.68.0.0/16 + range_name: pods + - ip_cidr_range: 100.71.1.0/24 + range_name: services timeouts: null - ? module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west4/dev-default"] - : description: Default europe-west4 subnet for dev + module.dev-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west4/dev-default"]: + description: Default europe-west4 subnet for dev ip_cidr_range: 10.84.0.0/24 ipv6_access_type: null log_config: [] @@ -592,14 +586,14 @@ values: enable_logging: true name: dev-spoke-0 networks: - - {} + - {} project: fast2-dev-net-spoke-0 timeouts: null - ? module.dmz-firewall.google_compute_firewall.custom-rules["allow-hc-nva-ssh-dmz"] - : allow: - - ports: - - "22" - protocol: tcp + module.dmz-firewall.google_compute_firewall.custom-rules["allow-hc-nva-ssh-dmz"]: + allow: + - ports: + - '22' + protocol: tcp deny: [] description: Allow traffic from Google healthchecks to NVA appliances direction: INGRESS @@ -610,20 +604,20 @@ values: priority: 1000 project: fast2-prod-net-landing-0 source_ranges: - - 130.211.0.0/22 - - 209.85.152.0/22 - - 209.85.204.0/22 - - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + - 35.191.0.0/16 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: null timeouts: null - ? module.dmz-firewall.google_compute_firewall.custom-rules["allow-ncc-nva-bgp-dmz"] - : allow: - - ports: - - "179" - protocol: tcp + module.dmz-firewall.google_compute_firewall.custom-rules["allow-ncc-nva-bgp-dmz"]: + allow: + - ports: + - '179' + protocol: tcp deny: [] description: Allow BGP traffic from NCC Cloud Routers to NVAs direction: INGRESS @@ -634,21 +628,21 @@ values: priority: 1000 project: fast2-prod-net-landing-0 source_ranges: - - 10.128.0.201/32 - - 10.128.0.202/32 - - 10.128.32.201/32 - - 10.128.32.202/32 + - 10.128.0.201/32 + - 10.128.0.202/32 + - 10.128.32.201/32 + - 10.128.32.202/32 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: - - nva + - nva timeouts: null - ? module.dmz-firewall.google_compute_firewall.custom-rules["allow-nva-nva-bgp-dmz"] - : allow: - - ports: - - "179" - protocol: tcp + module.dmz-firewall.google_compute_firewall.custom-rules["allow-nva-nva-bgp-dmz"]: + allow: + - ports: + - '179' + protocol: tcp deny: [] description: Allow BGP traffic from cross-regional NVAs direction: INGRESS @@ -661,27 +655,27 @@ values: source_ranges: null source_service_accounts: null source_tags: - - nva + - nva target_service_accounts: null target_tags: - - nva + - nva timeouts: null - ? module.dmz-firewall.google_compute_firewall.custom-rules["dmz-ingress-default-deny"] - : allow: [] + module.dmz-firewall.google_compute_firewall.custom-rules["dmz-ingress-default-deny"]: + allow: [] deny: - - ports: [] - protocol: all + - ports: [] + protocol: all description: Deny and log any unmatched ingress traffic. direction: INGRESS disabled: false log_config: - - metadata: EXCLUDE_ALL_METADATA + - metadata: EXCLUDE_ALL_METADATA name: dmz-ingress-default-deny network: prod-dmz-0 priority: 65535 project: fast2-prod-net-landing-0 source_ranges: - - 0.0.0.0/0 + - 0.0.0.0/0 source_service_accounts: null source_tags: null target_service_accounts: null @@ -698,8 +692,8 @@ values: project: fast2-prod-net-landing-0 routing_mode: GLOBAL timeouts: null - ? module.dmz-vpc.google_compute_subnetwork.subnetwork["europe-west1/dmz-default"] - : description: Default europe-west1 subnet for DMZ + module.dmz-vpc.google_compute_subnetwork.subnetwork["europe-west1/dmz-default"]: + description: Default europe-west1 subnet for DMZ ip_cidr_range: 10.64.128.0/24 ipv6_access_type: null log_config: [] @@ -711,8 +705,8 @@ values: role: null secondary_ip_range: [] timeouts: null - ? module.dmz-vpc.google_compute_subnetwork.subnetwork["europe-west4/dmz-default"] - : description: Default europe-west4 subnet for DMZ + module.dmz-vpc.google_compute_subnetwork.subnetwork["europe-west4/dmz-default"]: + description: Default europe-west4 subnet for DMZ ip_cidr_range: 10.80.128.0/24 ipv6_access_type: null log_config: [] @@ -731,117 +725,117 @@ values: enable_logging: true name: prod-dmz-0 networks: - - {} + - {} project: fast2-prod-net-landing-0 timeouts: null module.firewall-policy-default.google_compute_firewall_policy.hierarchical[0]: description: null short_name: net-default timeouts: null - ? module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-healthchecks"] - : action: allow + module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-healthchecks"]: + action: allow description: Enable SSH, HTTP and HTTPS healthchecks direction: INGRESS disabled: false enable_logging: null match: - - dest_address_groups: null - dest_fqdns: null - dest_ip_ranges: null - dest_region_codes: null - dest_threat_intelligences: null - layer4_configs: - - ip_protocol: tcp - ports: - - "22" - - "80" - - "443" - src_address_groups: null - src_fqdns: null - src_ip_ranges: - - 35.191.0.0/16 - - 130.211.0.0/22 - - 209.85.152.0/22 - - 209.85.204.0/22 - src_region_codes: null - src_threat_intelligences: null + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + - '80' + - '443' + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + src_region_codes: null + src_threat_intelligences: null priority: 1001 target_resources: null target_service_accounts: null timeouts: null - ? module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-icmp"] - : action: allow + module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-icmp"]: + action: allow description: Enable ICMP direction: INGRESS disabled: false enable_logging: null 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: [] - src_address_groups: null - src_fqdns: null - src_ip_ranges: - - 0.0.0.0/0 - src_region_codes: null - src_threat_intelligences: null + - 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: [] + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 0.0.0.0/0 + src_region_codes: null + src_threat_intelligences: null priority: 1003 target_resources: null target_service_accounts: null timeouts: null - ? module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-nat-ranges"] - : action: allow + module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-nat-ranges"]: + action: allow description: Enable NAT ranges for VPC serverless connector direction: INGRESS disabled: false enable_logging: null match: - - dest_address_groups: null - dest_fqdns: null - dest_ip_ranges: null - dest_region_codes: null - dest_threat_intelligences: null - layer4_configs: - - ip_protocol: all - ports: null - src_address_groups: null - src_fqdns: null - src_ip_ranges: - - 107.178.230.64/26 - - 35.199.224.0/19 - src_region_codes: null - src_threat_intelligences: null + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: all + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 107.178.230.64/26 + - 35.199.224.0/19 + src_region_codes: null + src_threat_intelligences: null priority: 1004 target_resources: null target_service_accounts: null timeouts: null - ? module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-ssh-from-iap"] - : action: allow + module.firewall-policy-default.google_compute_firewall_policy_rule.hierarchical["ingress/allow-ssh-from-iap"]: + action: allow description: Enable SSH from IAP 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: tcp - ports: - - "22" - src_address_groups: null - src_fqdns: null - src_ip_ranges: - - 35.235.240.0/20 - src_region_codes: null - src_threat_intelligences: null + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 35.235.240.0/20 + src_region_codes: null + src_threat_intelligences: null priority: 1002 target_resources: null target_service_accounts: null @@ -849,27 +843,27 @@ values: module.folder.google_compute_firewall_policy_association.default[0]: name: default timeouts: null - ? module.folder.google_essential_contacts_contact.contact["gcp-network-admins@fast.example.com"] - : email: gcp-network-admins@fast.example.com + module.folder.google_essential_contacts_contact.contact["gcp-network-admins@fast.example.com"]: + email: gcp-network-admins@fast.example.com language_tag: en notification_category_subscriptions: - - ALL + - ALL timeouts: null module.folder.google_folder.folder[0]: display_name: Networking parent: organizations/123456789012 timeouts: null - ? module.landing-dns-fwd-onprem-example[0].google_dns_managed_zone.dns_managed_zone[0] - : cloud_logging_config: - - enable_logging: false + module.landing-dns-fwd-onprem-example[0].google_dns_managed_zone.dns_managed_zone[0]: + cloud_logging_config: + - enable_logging: false description: Terraform managed. dns_name: onprem.example.com. dnssec_config: [] force_destroy: false forwarding_config: - - target_name_servers: - - forwarding_path: "" - ipv4_address: 10.10.10.10 + - target_name_servers: + - forwarding_path: '' + ipv4_address: 10.10.10.10 labels: null name: example-com peering_config: [] @@ -878,17 +872,17 @@ values: service_directory_config: [] timeouts: null visibility: private - ? module.landing-dns-fwd-onprem-rev-10[0].google_dns_managed_zone.dns_managed_zone[0] - : cloud_logging_config: - - enable_logging: false + module.landing-dns-fwd-onprem-rev-10[0].google_dns_managed_zone.dns_managed_zone[0]: + cloud_logging_config: + - enable_logging: false description: Terraform managed. dns_name: 10.in-addr.arpa. dnssec_config: [] force_destroy: false forwarding_config: - - target_name_servers: - - forwarding_path: "" - ipv4_address: 10.10.10.10 + - target_name_servers: + - forwarding_path: '' + ipv4_address: 10.10.10.10 labels: null name: root-reverse-10 peering_config: [] @@ -901,496 +895,496 @@ values: description: Terraform managed. gke_clusters: [] networks: - - {} - - {} + - {} + - {} project: fast2-prod-net-landing-0 response_policy_name: googleapis timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["accounts"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["accounts"]: + behavior: null dns_name: accounts.google.com. local_data: - - local_datas: - - name: accounts.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: accounts.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: accounts timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-cloud"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-cloud"]: + behavior: null dns_name: backupdr.cloud.google.com. local_data: - - local_datas: - - name: backupdr.cloud.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: backupdr.cloud.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: backupdr-cloud timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-cloud-all"] - : behavior: null - dns_name: "*.backupdr.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-cloud-all"]: + behavior: null + dns_name: '*.backupdr.cloud.google.com.' local_data: - - local_datas: - - name: "*.backupdr.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.backupdr.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: backupdr-cloud-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-gu"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-gu"]: + behavior: null dns_name: backupdr.googleusercontent.google.com. local_data: - - local_datas: - - name: backupdr.googleusercontent.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: backupdr.googleusercontent.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: backupdr-gu timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-gu-all"] - : behavior: null - dns_name: "*.backupdr.googleusercontent.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["backupdr-gu-all"]: + behavior: null + dns_name: '*.backupdr.googleusercontent.google.com.' local_data: - - local_datas: - - name: "*.backupdr.googleusercontent.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.backupdr.googleusercontent.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: backupdr-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["cloudfunctions"] - : behavior: null - dns_name: "*.cloudfunctions.net." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["cloudfunctions"]: + behavior: null + dns_name: '*.cloudfunctions.net.' local_data: - - local_datas: - - name: "*.cloudfunctions.net." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.cloudfunctions.net.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: cloudfunctions timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["cloudproxy"] - : behavior: null - dns_name: "*.cloudproxy.app." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["cloudproxy"]: + behavior: null + dns_name: '*.cloudproxy.app.' local_data: - - local_datas: - - name: "*.cloudproxy.app." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.cloudproxy.app.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: cloudproxy timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["composer-cloud-all"] - : behavior: null - dns_name: "*.composer.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["composer-cloud-all"]: + behavior: null + dns_name: '*.composer.cloud.google.com.' local_data: - - local_datas: - - name: "*.composer.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.composer.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: composer-cloud-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["composer-gu-all"] - : behavior: null - dns_name: "*.composer.googleusercontent.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["composer-gu-all"]: + behavior: null + dns_name: '*.composer.googleusercontent.com.' local_data: - - local_datas: - - name: "*.composer.googleusercontent.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.composer.googleusercontent.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: composer-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["datafusion-all"] - : behavior: null - dns_name: "*.datafusion.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["datafusion-all"]: + behavior: null + dns_name: '*.datafusion.cloud.google.com.' local_data: - - local_datas: - - name: "*.datafusion.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.datafusion.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: datafusion-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["datafusion-gu-all"] - : behavior: null - dns_name: "*.datafusion.googleusercontent.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["datafusion-gu-all"]: + behavior: null + dns_name: '*.datafusion.googleusercontent.com.' local_data: - - local_datas: - - name: "*.datafusion.googleusercontent.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.datafusion.googleusercontent.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: datafusion-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc"]: + behavior: null dns_name: dataproc.cloud.google.com. local_data: - - local_datas: - - name: dataproc.cloud.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: dataproc.cloud.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: dataproc timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-all"] - : behavior: null - dns_name: "*.dataproc.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-all"]: + behavior: null + dns_name: '*.dataproc.cloud.google.com.' local_data: - - local_datas: - - name: "*.dataproc.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.dataproc.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: dataproc-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-gu"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-gu"]: + behavior: null dns_name: dataproc.googleusercontent.com. local_data: - - local_datas: - - name: dataproc.googleusercontent.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: dataproc.googleusercontent.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: dataproc-gu timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-gu-all"] - : behavior: null - dns_name: "*.dataproc.googleusercontent.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dataproc-gu-all"]: + behavior: null + dns_name: '*.dataproc.googleusercontent.com.' local_data: - - local_datas: - - name: "*.dataproc.googleusercontent.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.dataproc.googleusercontent.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: dataproc-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dl"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["dl"]: + behavior: null dns_name: dl.google.com. local_data: - - local_datas: - - name: dl.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: dl.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: dl timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gcr"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gcr"]: + behavior: null dns_name: gcr.io. local_data: - - local_datas: - - name: gcr.io. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: gcr.io. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: gcr timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gcr-all"] - : behavior: null - dns_name: "*.gcr.io." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gcr-all"]: + behavior: null + dns_name: '*.gcr.io.' local_data: - - local_datas: - - name: "*.gcr.io." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.gcr.io.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: gcr-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-all"] - : behavior: null - dns_name: "*.googleapis.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-all"]: + behavior: null + dns_name: '*.googleapis.com.' local_data: - - local_datas: - - name: "*.googleapis.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.googleapis.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: googleapis-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-private"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-private"]: + behavior: null dns_name: private.googleapis.com. local_data: - - local_datas: - - name: private.googleapis.com. - rrdatas: - - 199.36.153.8 - - 199.36.153.9 - - 199.36.153.10 - - 199.36.153.11 - ttl: null - type: A + - local_datas: + - name: private.googleapis.com. + rrdatas: + - 199.36.153.8 + - 199.36.153.9 + - 199.36.153.10 + - 199.36.153.11 + ttl: null + type: A project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: googleapis-private timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-restricted"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["googleapis-restricted"]: + behavior: null dns_name: restricted.googleapis.com. local_data: - - local_datas: - - name: restricted.googleapis.com. - rrdatas: - - 199.36.153.4 - - 199.36.153.5 - - 199.36.153.6 - - 199.36.153.7 - ttl: null - type: A + - local_datas: + - name: restricted.googleapis.com. + rrdatas: + - 199.36.153.4 + - 199.36.153.5 + - 199.36.153.6 + - 199.36.153.7 + ttl: null + type: A project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: googleapis-restricted timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gstatic-all"] - : behavior: null - dns_name: "*.gstatic.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["gstatic-all"]: + behavior: null + dns_name: '*.gstatic.com.' local_data: - - local_datas: - - name: "*.gstatic.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.gstatic.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: gstatic-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["kernels-gu"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["kernels-gu"]: + behavior: null dns_name: kernels.googleusercontent.com. local_data: - - local_datas: - - name: kernels.googleusercontent.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: kernels.googleusercontent.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: kernels-gu timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["kernels-gu-all"] - : behavior: null - dns_name: "*.kernels.googleusercontent.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["kernels-gu-all"]: + behavior: null + dns_name: '*.kernels.googleusercontent.com.' local_data: - - local_datas: - - name: "*.kernels.googleusercontent.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.kernels.googleusercontent.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: kernels-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["notebooks-all"] - : behavior: null - dns_name: "*.notebooks.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["notebooks-all"]: + behavior: null + dns_name: '*.notebooks.cloud.google.com.' local_data: - - local_datas: - - name: "*.notebooks.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.notebooks.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: notebooks-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["notebooks-gu-all"] - : behavior: null - dns_name: "*.notebooks.googleusercontent.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["notebooks-gu-all"]: + behavior: null + dns_name: '*.notebooks.googleusercontent.com.' local_data: - - local_datas: - - name: "*.notebooks.googleusercontent.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.notebooks.googleusercontent.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: notebooks-gu-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["packages-cloud"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["packages-cloud"]: + behavior: null dns_name: packages.cloud.google.com. local_data: - - local_datas: - - name: packages.cloud.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: packages.cloud.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: packages-cloud timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["packages-cloud-all"] - : behavior: null - dns_name: "*.packages.cloud.google.com." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["packages-cloud-all"]: + behavior: null + dns_name: '*.packages.cloud.google.com.' local_data: - - local_datas: - - name: "*.packages.cloud.google.com." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.packages.cloud.google.com.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: packages-cloud-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkgdev"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkgdev"]: + behavior: null dns_name: pkg.dev. local_data: - - local_datas: - - name: pkg.dev. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: pkg.dev. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: pkgdev timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkgdev-all"] - : behavior: null - dns_name: "*.pkg.dev." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkgdev-all"]: + behavior: null + dns_name: '*.pkg.dev.' local_data: - - local_datas: - - name: "*.pkg.dev." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.pkg.dev.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: pkgdev-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkigoog"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkigoog"]: + behavior: null dns_name: pki.goog. local_data: - - local_datas: - - name: pki.goog. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: pki.goog. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: pkigoog timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkigoog-all"] - : behavior: null - dns_name: "*.pki.goog." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["pkigoog-all"]: + behavior: null + dns_name: '*.pki.goog.' local_data: - - local_datas: - - name: "*.pki.goog." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.pki.goog.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: pkigoog-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["run-all"] - : behavior: null - dns_name: "*.run.app." + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["run-all"]: + behavior: null + dns_name: '*.run.app.' local_data: - - local_datas: - - name: "*.run.app." - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: '*.run.app.' + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: run-all timeouts: null - ? module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["source"] - : behavior: null + module.landing-dns-policy-googleapis.google_dns_response_policy_rule.default["source"]: + behavior: null dns_name: source.developers.google.com. local_data: - - local_datas: - - name: source.developers.google.com. - rrdatas: - - private.googleapis.com. - ttl: null - type: CNAME + - local_datas: + - name: source.developers.google.com. + rrdatas: + - private.googleapis.com. + ttl: null + type: CNAME project: fast2-prod-net-landing-0 response_policy: googleapis rule_name: source timeouts: null module.landing-dns-priv-gcp.google_dns_managed_zone.dns_managed_zone[0]: cloud_logging_config: - - enable_logging: false + - enable_logging: false description: Terraform managed. dns_name: gcp.example.com. dnssec_config: [] @@ -1403,20 +1397,20 @@ values: service_directory_config: [] timeouts: null visibility: private - ? module.landing-dns-priv-gcp.google_dns_record_set.dns_record_set["A localhost"] - : managed_zone: gcp-example-com + module.landing-dns-priv-gcp.google_dns_record_set.dns_record_set["A localhost"]: + managed_zone: gcp-example-com name: localhost.gcp.example.com. project: fast2-prod-net-landing-0 routing_policy: [] rrdatas: - - 127.0.0.1 + - 127.0.0.1 ttl: 300 type: A - ? module.landing-firewall.google_compute_firewall.custom-rules["allow-hc-nva-ssh-landing"] - : allow: - - ports: - - "22" - protocol: tcp + module.landing-firewall.google_compute_firewall.custom-rules["allow-hc-nva-ssh-landing"]: + allow: + - ports: + - '22' + protocol: tcp deny: [] description: Allow traffic from Google healthchecks to NVA appliances direction: INGRESS @@ -1427,20 +1421,20 @@ values: priority: 1000 project: fast2-prod-net-landing-0 source_ranges: - - 130.211.0.0/22 - - 209.85.152.0/22 - - 209.85.204.0/22 - - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + - 35.191.0.0/16 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: null timeouts: null - ? module.landing-firewall.google_compute_firewall.custom-rules["allow-ncc-nva-bgp-landing"] - : allow: - - ports: - - "179" - protocol: tcp + module.landing-firewall.google_compute_firewall.custom-rules["allow-ncc-nva-bgp-landing"]: + allow: + - ports: + - '179' + protocol: tcp deny: [] description: Allow BGP traffic from NCC Cloud Routers to NVAs direction: INGRESS @@ -1451,21 +1445,21 @@ values: priority: 1000 project: fast2-prod-net-landing-0 source_ranges: - - 10.128.64.201/32 - - 10.128.64.202/32 - - 10.128.96.201/32 - - 10.128.96.202/32 + - 10.128.64.201/32 + - 10.128.64.202/32 + - 10.128.96.201/32 + - 10.128.96.202/32 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: - - nva + - nva timeouts: null - ? module.landing-firewall.google_compute_firewall.custom-rules["allow-onprem-probes-landing-example"] - : allow: - - ports: - - "12345" - protocol: tcp + module.landing-firewall.google_compute_firewall.custom-rules["allow-onprem-probes-landing-example"]: + allow: + - ports: + - '12345' + protocol: tcp deny: [] description: Allow traffic from onprem probes direction: INGRESS @@ -1476,28 +1470,28 @@ values: priority: 1000 project: fast2-prod-net-landing-0 source_ranges: - - 10.255.255.254/32 + - 10.255.255.254/32 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: null timeouts: null - ? module.landing-firewall.google_compute_firewall.custom-rules["landing-ingress-default-deny"] - : allow: [] + module.landing-firewall.google_compute_firewall.custom-rules["landing-ingress-default-deny"]: + allow: [] deny: - - ports: [] - protocol: all + - ports: [] + protocol: all description: Deny and log any unmatched ingress traffic. direction: INGRESS disabled: false log_config: - - metadata: EXCLUDE_ALL_METADATA + - metadata: EXCLUDE_ALL_METADATA name: landing-ingress-default-deny network: prod-landing-0 priority: 65535 project: fast2-prod-net-landing-0 source_ranges: - - 0.0.0.0/0 + - 0.0.0.0/0 source_service_accounts: null source_tags: null target_service_accounts: null @@ -1518,8 +1512,8 @@ values: enable_endpoint_independent_mapping: true icmp_idle_timeout_sec: 30 log_config: - - enable: false - filter: ALL + - enable: false + filter: ALL max_ports_per_vm: null name: ew1 nat_ip_allocate_option: AUTO_ONLY @@ -1534,6 +1528,7 @@ values: tcp_time_wait_timeout_sec: 120 tcp_transitory_idle_timeout_sec: 30 timeouts: null + type: PUBLIC udp_idle_timeout_sec: 30 module.landing-nat-secondary[0].google_compute_router.router[0]: bgp: [] @@ -1550,8 +1545,8 @@ values: enable_endpoint_independent_mapping: true icmp_idle_timeout_sec: 30 log_config: - - enable: false - filter: ALL + - enable: false + filter: ALL max_ports_per_vm: null name: ew4 nat_ip_allocate_option: AUTO_ONLY @@ -1566,9 +1561,10 @@ values: tcp_time_wait_timeout_sec: 120 tcp_transitory_idle_timeout_sec: 30 timeouts: null + type: PUBLIC udp_idle_timeout_sec: 30 - ? module.landing-project.google_compute_shared_vpc_host_project.shared_vpc_host[0] - : project: fast2-prod-net-landing-0 + module.landing-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: fast2-prod-net-landing-0 timeouts: null module.landing-project.google_project.project[0]: auto_create_network: false @@ -1580,70 +1576,70 @@ values: project_id: fast2-prod-net-landing-0 skip_delete: false timeouts: null - ? module.landing-project.google_project_iam_binding.authoritative["organizations/123456789012/roles/foo"] - : condition: [] + module.landing-project.google_project_iam_binding.authoritative["organizations/123456789012/roles/foo"]: + condition: [] members: - - serviceAccount:string + - serviceAccount:string project: fast2-prod-net-landing-0 role: organizations/123456789012/roles/foo - ? module.landing-project.google_project_iam_binding.authoritative["roles/dns.admin"] - : condition: [] + module.landing-project.google_project_iam_binding.authoritative["roles/dns.admin"]: + condition: [] members: - - serviceAccount:string + - serviceAccount:string project: fast2-prod-net-landing-0 role: roles/dns.admin - ? module.landing-project.google_project_service.project_services["compute.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["compute.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: compute.googleapis.com timeouts: null - ? module.landing-project.google_project_service.project_services["dns.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["dns.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: dns.googleapis.com timeouts: null - ? module.landing-project.google_project_service.project_services["iap.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["iap.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: iap.googleapis.com timeouts: null - ? module.landing-project.google_project_service.project_services["networkconnectivity.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["networkconnectivity.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: networkconnectivity.googleapis.com timeouts: null - ? module.landing-project.google_project_service.project_services["networkmanagement.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["networkmanagement.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: networkmanagement.googleapis.com timeouts: null - ? module.landing-project.google_project_service.project_services["stackdriver.googleapis.com"] - : disable_dependent_services: false + module.landing-project.google_project_service.project_services["stackdriver.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-landing-0 service: stackdriver.googleapis.com timeouts: null - ? module.landing-project.google_project_service_identity.jit_si["iap.googleapis.com"] - : project: fast2-prod-net-landing-0 + module.landing-project.google_project_service_identity.jit_si["iap.googleapis.com"]: + project: fast2-prod-net-landing-0 service: iap.googleapis.com timeouts: null - ? module.landing-to-onprem-primary-vpn[0].google_compute_external_vpn_gateway.external_gateway["default"] - : description: Terraform managed external VPN gateway + module.landing-to-onprem-primary-vpn[0].google_compute_external_vpn_gateway.external_gateway["default"]: + description: Terraform managed external VPN gateway interface: - - id: 0 - ip_address: 8.8.8.8 + - id: 0 + ip_address: 8.8.8.8 labels: null name: vpn-to-onprem-ew1-default project: fast2-prod-net-landing-0 redundancy_type: SINGLE_IP_INTERNALLY_REDUNDANT timeouts: null - ? module.landing-to-onprem-primary-vpn[0].google_compute_ha_vpn_gateway.ha_gateway[0] - : description: Terraform managed external VPN gateway + module.landing-to-onprem-primary-vpn[0].google_compute_ha_vpn_gateway.ha_gateway[0]: + description: Terraform managed external VPN gateway name: vpn-to-onprem-ew1 project: fast2-prod-net-landing-0 region: europe-west1 @@ -1651,25 +1647,25 @@ values: timeouts: null module.landing-to-onprem-primary-vpn[0].google_compute_router.router[0]: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: gcp - range: 10.1.0.0/16 - - description: gcp-restricted - range: 199.36.153.4/30 - - description: gcp-dns - range: 35.199.192.0/19 - asn: 65501 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: gcp + range: 10.1.0.0/16 + - description: gcp-restricted + range: 199.36.153.4/30 + - description: gcp-dns + range: 35.199.192.0/19 + asn: 65501 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: vpn-vpn-to-onprem-ew1 project: fast2-prod-net-landing-0 region: europe-west1 timeouts: null - ? module.landing-to-onprem-primary-vpn[0].google_compute_router_interface.router_interface["0"] - : interconnect_attachment: null + module.landing-to-onprem-primary-vpn[0].google_compute_router_interface.router_interface["0"]: + interconnect_attachment: null ip_range: 169.254.1.2/30 name: vpn-to-onprem-ew1-0 private_ip_address: null @@ -1679,8 +1675,8 @@ values: subnetwork: null timeouts: null vpn_tunnel: vpn-to-onprem-ew1-0 - ? module.landing-to-onprem-primary-vpn[0].google_compute_router_interface.router_interface["1"] - : interconnect_attachment: null + module.landing-to-onprem-primary-vpn[0].google_compute_router_interface.router_interface["1"]: + interconnect_attachment: null ip_range: 169.254.2.2/30 name: vpn-to-onprem-ew1-1 private_ip_address: null @@ -1690,8 +1686,8 @@ values: subnetwork: null timeouts: null vpn_tunnel: vpn-to-onprem-ew1-1 - ? module.landing-to-onprem-primary-vpn[0].google_compute_router_peer.bgp_peer["0"] - : advertise_mode: DEFAULT + module.landing-to-onprem-primary-vpn[0].google_compute_router_peer.bgp_peer["0"]: + advertise_mode: DEFAULT advertised_groups: [] advertised_ip_ranges: [] advertised_route_priority: 1000 @@ -1707,8 +1703,8 @@ values: router: vpn-vpn-to-onprem-ew1 router_appliance_instance: null timeouts: null - ? module.landing-to-onprem-primary-vpn[0].google_compute_router_peer.bgp_peer["1"] - : advertise_mode: DEFAULT + module.landing-to-onprem-primary-vpn[0].google_compute_router_peer.bgp_peer["1"]: + advertise_mode: DEFAULT advertised_groups: [] advertised_ip_ranges: [] advertised_route_priority: 1000 @@ -1724,8 +1720,8 @@ values: router: vpn-vpn-to-onprem-ew1 router_appliance_instance: null timeouts: null - ? module.landing-to-onprem-primary-vpn[0].google_compute_vpn_tunnel.tunnels["0"] - : description: null + module.landing-to-onprem-primary-vpn[0].google_compute_vpn_tunnel.tunnels["0"]: + description: null ike_version: 2 labels: null name: vpn-to-onprem-ew1-0 @@ -1738,8 +1734,8 @@ values: target_vpn_gateway: null timeouts: null vpn_gateway_interface: 0 - ? module.landing-to-onprem-primary-vpn[0].google_compute_vpn_tunnel.tunnels["1"] - : description: null + module.landing-to-onprem-primary-vpn[0].google_compute_vpn_tunnel.tunnels["1"]: + description: null ike_version: 2 labels: null name: vpn-to-onprem-ew1-1 @@ -1756,18 +1752,18 @@ values: byte_length: 8 keepers: null prefix: null - ? module.landing-to-onprem-secondary-vpn[0].google_compute_external_vpn_gateway.external_gateway["default"] - : description: Terraform managed external VPN gateway + module.landing-to-onprem-secondary-vpn[0].google_compute_external_vpn_gateway.external_gateway["default"]: + description: Terraform managed external VPN gateway interface: - - id: 0 - ip_address: 8.8.4.4 + - id: 0 + ip_address: 8.8.4.4 labels: null name: vpn-to-onprem-ew4-default project: fast2-prod-net-landing-0 redundancy_type: SINGLE_IP_INTERNALLY_REDUNDANT timeouts: null - ? module.landing-to-onprem-secondary-vpn[0].google_compute_ha_vpn_gateway.ha_gateway[0] - : description: Terraform managed external VPN gateway + module.landing-to-onprem-secondary-vpn[0].google_compute_ha_vpn_gateway.ha_gateway[0]: + description: Terraform managed external VPN gateway name: vpn-to-onprem-ew4 project: fast2-prod-net-landing-0 region: europe-west4 @@ -1775,25 +1771,25 @@ values: timeouts: null module.landing-to-onprem-secondary-vpn[0].google_compute_router.router[0]: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: gcp - range: 10.1.0.0/16 - - description: gcp-restricted - range: 199.36.153.4/30 - - description: gcp-dns - range: 35.199.192.0/19 - asn: 65501 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: gcp + range: 10.1.0.0/16 + - description: gcp-restricted + range: 199.36.153.4/30 + - description: gcp-dns + range: 35.199.192.0/19 + asn: 65501 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: vpn-vpn-to-onprem-ew4 project: fast2-prod-net-landing-0 region: europe-west4 timeouts: null - ? module.landing-to-onprem-secondary-vpn[0].google_compute_router_interface.router_interface["0"] - : interconnect_attachment: null + module.landing-to-onprem-secondary-vpn[0].google_compute_router_interface.router_interface["0"]: + interconnect_attachment: null ip_range: 169.254.3.2/30 name: vpn-to-onprem-ew4-0 private_ip_address: null @@ -1803,8 +1799,8 @@ values: subnetwork: null timeouts: null vpn_tunnel: vpn-to-onprem-ew4-0 - ? module.landing-to-onprem-secondary-vpn[0].google_compute_router_interface.router_interface["1"] - : interconnect_attachment: null + module.landing-to-onprem-secondary-vpn[0].google_compute_router_interface.router_interface["1"]: + interconnect_attachment: null ip_range: 169.254.4.2/30 name: vpn-to-onprem-ew4-1 private_ip_address: null @@ -1814,8 +1810,8 @@ values: subnetwork: null timeouts: null vpn_tunnel: vpn-to-onprem-ew4-1 - ? module.landing-to-onprem-secondary-vpn[0].google_compute_router_peer.bgp_peer["0"] - : advertise_mode: DEFAULT + module.landing-to-onprem-secondary-vpn[0].google_compute_router_peer.bgp_peer["0"]: + advertise_mode: DEFAULT advertised_groups: [] advertised_ip_ranges: [] advertised_route_priority: 1000 @@ -1831,8 +1827,8 @@ values: router: vpn-vpn-to-onprem-ew4 router_appliance_instance: null timeouts: null - ? module.landing-to-onprem-secondary-vpn[0].google_compute_router_peer.bgp_peer["1"] - : advertise_mode: DEFAULT + module.landing-to-onprem-secondary-vpn[0].google_compute_router_peer.bgp_peer["1"]: + advertise_mode: DEFAULT advertised_groups: [] advertised_ip_ranges: [] advertised_route_priority: 1000 @@ -1848,8 +1844,8 @@ values: router: vpn-vpn-to-onprem-ew4 router_appliance_instance: null timeouts: null - ? module.landing-to-onprem-secondary-vpn[0].google_compute_vpn_tunnel.tunnels["0"] - : description: null + module.landing-to-onprem-secondary-vpn[0].google_compute_vpn_tunnel.tunnels["0"]: + description: null ike_version: 2 labels: null name: vpn-to-onprem-ew4-0 @@ -1862,8 +1858,8 @@ values: target_vpn_gateway: null timeouts: null vpn_gateway_interface: 0 - ? module.landing-to-onprem-secondary-vpn[0].google_compute_vpn_tunnel.tunnels["1"] - : description: null + module.landing-to-onprem-secondary-vpn[0].google_compute_vpn_tunnel.tunnels["1"]: + description: null ike_version: 2 labels: null name: vpn-to-onprem-ew4-1 @@ -1917,8 +1913,8 @@ values: project: fast2-prod-net-landing-0 tags: null timeouts: null - ? module.landing-vpc.google_compute_subnetwork.subnetwork["europe-west1/landing-default"] - : description: Default europe-west1 subnet for landing + module.landing-vpc.google_compute_subnetwork.subnetwork["europe-west1/landing-default"]: + description: Default europe-west1 subnet for landing ip_cidr_range: 10.64.0.0/24 ipv6_access_type: null log_config: [] @@ -1930,8 +1926,8 @@ values: role: null secondary_ip_range: [] timeouts: null - ? module.landing-vpc.google_compute_subnetwork.subnetwork["europe-west4/landing-default"] - : description: Default europe-west4 subnet for landing + module.landing-vpc.google_compute_subnetwork.subnetwork["europe-west4/landing-default"]: + description: Default europe-west4 subnet for landing ip_cidr_range: 10.80.0.0/24 ipv6_access_type: null log_config: [] @@ -1950,7 +1946,7 @@ values: enable_logging: null name: prod-landing-0 networks: - - {} + - {} project: fast2-prod-net-landing-0 timeouts: null module.nva["primary-b"].google_compute_instance.default[0]: @@ -1958,15 +1954,15 @@ values: allow_stopping_for_update: true attached_disk: [] boot_disk: - - auto_delete: true - disk_encryption_key_raw: null - initialize_params: - - enable_confidential_compute: null - image: projects/cos-cloud/global/images/family/cos-stable - resource_manager_tags: null - size: 10 - type: pd-balanced - mode: READ_WRITE + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - enable_confidential_compute: null + image: projects/cos-cloud/global/images/family/cos-stable + resource_manager_tags: null + size: 10 + type: pd-balanced + mode: READ_WRITE can_ip_forward: true deletion_protection: false description: Managed by the compute-vm Terraform module. @@ -1976,8 +1972,7 @@ values: labels: null machine_type: e2-standard-2 metadata: - user-data: - "#cloud-config\n\n# Copyright 2023 Google LLC\n#\n# Licensed under\ + user-data: "#cloud-config\n\n# Copyright 2024 Google LLC\n#\n# Licensed under\ \ the Apache License, Version 2.0 (the \"License\");\n# you may not use this\ \ file except in compliance with the License.\n# You may obtain a copy of\ \ the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n\ @@ -2148,7 +2143,9 @@ values: \ permissions: 0744\n owner: root\n content: |\n iptables --policy\ \ FORWARD ACCEPT\n /var/run/nva/policy_based_routing.sh eth0 &>/dev/null\ \ &\n iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n ip\ - \ route add 10.64.127.0/17 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ route add 35.235.240.0/20 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.64.127.0/17\ + \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.80.127.0/17\ \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n /var/run/nva/policy_based_routing.sh\ @@ -2162,44 +2159,44 @@ values: metadata_startup_script: null name: nva-ew1-b network_interface: - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.64.128.101 - nic_type: null - queue_count: null - security_policy: null - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.64.0.101 - nic_type: null - queue_count: null - security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.64.128.101 + nic_type: null + queue_count: null + security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.64.0.101 + nic_type: null + queue_count: null + security_policy: null network_performance_config: [] params: [] project: fast2-prod-net-landing-0 resource_policies: null scheduling: - - automatic_restart: true - instance_termination_action: null - local_ssd_recovery_timeout: [] - maintenance_interval: null - max_run_duration: [] - min_node_cpus: null - node_affinities: [] - on_host_maintenance: MIGRATE - preemptible: false - provisioning_model: STANDARD + - automatic_restart: true + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + preemptible: false + provisioning_model: STANDARD 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 + - 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: - - nva + - nva timeouts: null zone: europe-west1-b module.nva["primary-c"].google_compute_instance.default[0]: @@ -2207,15 +2204,15 @@ values: allow_stopping_for_update: true attached_disk: [] boot_disk: - - auto_delete: true - disk_encryption_key_raw: null - initialize_params: - - enable_confidential_compute: null - image: projects/cos-cloud/global/images/family/cos-stable - resource_manager_tags: null - size: 10 - type: pd-balanced - mode: READ_WRITE + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - enable_confidential_compute: null + image: projects/cos-cloud/global/images/family/cos-stable + resource_manager_tags: null + size: 10 + type: pd-balanced + mode: READ_WRITE can_ip_forward: true deletion_protection: false description: Managed by the compute-vm Terraform module. @@ -2225,8 +2222,7 @@ values: labels: null machine_type: e2-standard-2 metadata: - user-data: - "#cloud-config\n\n# Copyright 2023 Google LLC\n#\n# Licensed under\ + user-data: "#cloud-config\n\n# Copyright 2024 Google LLC\n#\n# Licensed under\ \ the Apache License, Version 2.0 (the \"License\");\n# you may not use this\ \ file except in compliance with the License.\n# You may obtain a copy of\ \ the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n\ @@ -2397,7 +2393,9 @@ values: \ permissions: 0744\n owner: root\n content: |\n iptables --policy\ \ FORWARD ACCEPT\n /var/run/nva/policy_based_routing.sh eth0 &>/dev/null\ \ &\n iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n ip\ - \ route add 10.64.127.0/17 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ route add 35.235.240.0/20 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.64.127.0/17\ + \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.80.127.0/17\ \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n /var/run/nva/policy_based_routing.sh\ @@ -2411,44 +2409,44 @@ values: metadata_startup_script: null name: nva-ew1-c network_interface: - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.64.128.102 - nic_type: null - queue_count: null - security_policy: null - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.64.0.102 - nic_type: null - queue_count: null - security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.64.128.102 + nic_type: null + queue_count: null + security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.64.0.102 + nic_type: null + queue_count: null + security_policy: null network_performance_config: [] params: [] project: fast2-prod-net-landing-0 resource_policies: null scheduling: - - automatic_restart: true - instance_termination_action: null - local_ssd_recovery_timeout: [] - maintenance_interval: null - max_run_duration: [] - min_node_cpus: null - node_affinities: [] - on_host_maintenance: MIGRATE - preemptible: false - provisioning_model: STANDARD + - automatic_restart: true + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + preemptible: false + provisioning_model: STANDARD 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 + - 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: - - nva + - nva timeouts: null zone: europe-west1-c module.nva["secondary-b"].google_compute_instance.default[0]: @@ -2456,15 +2454,15 @@ values: allow_stopping_for_update: true attached_disk: [] boot_disk: - - auto_delete: true - disk_encryption_key_raw: null - initialize_params: - - enable_confidential_compute: null - image: projects/cos-cloud/global/images/family/cos-stable - resource_manager_tags: null - size: 10 - type: pd-balanced - mode: READ_WRITE + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - enable_confidential_compute: null + image: projects/cos-cloud/global/images/family/cos-stable + resource_manager_tags: null + size: 10 + type: pd-balanced + mode: READ_WRITE can_ip_forward: true deletion_protection: false description: Managed by the compute-vm Terraform module. @@ -2474,8 +2472,7 @@ values: labels: null machine_type: e2-standard-2 metadata: - user-data: - "#cloud-config\n\n# Copyright 2023 Google LLC\n#\n# Licensed under\ + user-data: "#cloud-config\n\n# Copyright 2024 Google LLC\n#\n# Licensed under\ \ the Apache License, Version 2.0 (the \"License\");\n# you may not use this\ \ file except in compliance with the License.\n# You may obtain a copy of\ \ the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n\ @@ -2646,7 +2643,9 @@ values: \ permissions: 0744\n owner: root\n content: |\n iptables --policy\ \ FORWARD ACCEPT\n /var/run/nva/policy_based_routing.sh eth0 &>/dev/null\ \ &\n iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n ip\ - \ route add 10.64.127.0/17 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ route add 35.235.240.0/20 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.64.127.0/17\ + \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.80.127.0/17\ \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n /var/run/nva/policy_based_routing.sh\ @@ -2660,44 +2659,44 @@ values: metadata_startup_script: null name: nva-ew4-b network_interface: - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.80.128.101 - nic_type: null - queue_count: null - security_policy: null - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.80.0.101 - nic_type: null - queue_count: null - security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.80.128.101 + nic_type: null + queue_count: null + security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.80.0.101 + nic_type: null + queue_count: null + security_policy: null network_performance_config: [] params: [] project: fast2-prod-net-landing-0 resource_policies: null scheduling: - - automatic_restart: true - instance_termination_action: null - local_ssd_recovery_timeout: [] - maintenance_interval: null - max_run_duration: [] - min_node_cpus: null - node_affinities: [] - on_host_maintenance: MIGRATE - preemptible: false - provisioning_model: STANDARD + - automatic_restart: true + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + preemptible: false + provisioning_model: STANDARD 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 + - 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: - - nva + - nva timeouts: null zone: europe-west4-b module.nva["secondary-c"].google_compute_instance.default[0]: @@ -2705,15 +2704,15 @@ values: allow_stopping_for_update: true attached_disk: [] boot_disk: - - auto_delete: true - disk_encryption_key_raw: null - initialize_params: - - enable_confidential_compute: null - image: projects/cos-cloud/global/images/family/cos-stable - resource_manager_tags: null - size: 10 - type: pd-balanced - mode: READ_WRITE + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - enable_confidential_compute: null + image: projects/cos-cloud/global/images/family/cos-stable + resource_manager_tags: null + size: 10 + type: pd-balanced + mode: READ_WRITE can_ip_forward: true deletion_protection: false description: Managed by the compute-vm Terraform module. @@ -2723,8 +2722,7 @@ values: labels: null machine_type: e2-standard-2 metadata: - user-data: - "#cloud-config\n\n# Copyright 2023 Google LLC\n#\n# Licensed under\ + user-data: "#cloud-config\n\n# Copyright 2024 Google LLC\n#\n# Licensed under\ \ the Apache License, Version 2.0 (the \"License\");\n# you may not use this\ \ file except in compliance with the License.\n# You may obtain a copy of\ \ the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n\ @@ -2895,7 +2893,9 @@ values: \ permissions: 0744\n owner: root\n content: |\n iptables --policy\ \ FORWARD ACCEPT\n /var/run/nva/policy_based_routing.sh eth0 &>/dev/null\ \ &\n iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n ip\ - \ route add 10.64.127.0/17 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ route add 35.235.240.0/20 via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ + \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.64.127.0/17\ + \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n ip route add 10.80.127.0/17\ \ via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/gateway\ \ -H \"Metadata-Flavor:Google\"` dev eth0\n /var/run/nva/policy_based_routing.sh\ @@ -2909,44 +2909,44 @@ values: metadata_startup_script: null name: nva-ew4-c network_interface: - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.80.128.102 - nic_type: null - queue_count: null - security_policy: null - - access_config: [] - alias_ip_range: [] - ipv6_access_config: [] - network_ip: 10.80.0.102 - nic_type: null - queue_count: null - security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.80.128.102 + nic_type: null + queue_count: null + security_policy: null + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network_ip: 10.80.0.102 + nic_type: null + queue_count: null + security_policy: null network_performance_config: [] params: [] project: fast2-prod-net-landing-0 resource_policies: null scheduling: - - automatic_restart: true - instance_termination_action: null - local_ssd_recovery_timeout: [] - maintenance_interval: null - max_run_duration: [] - min_node_cpus: null - node_affinities: [] - on_host_maintenance: MIGRATE - preemptible: false - provisioning_model: STANDARD + - automatic_restart: true + instance_termination_action: null + local_ssd_recovery_timeout: [] + maintenance_interval: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + preemptible: false + provisioning_model: STANDARD 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 + - 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: - - nva + - nva timeouts: null zone: europe-west4-c module.peering-dev.google_compute_network_peering.local_network_peering: @@ -2977,9 +2977,9 @@ values: import_subnet_routes_with_public_ip: null stack_type: IPV4_ONLY timeouts: null - ? module.prod-dns-peer-landing-rev-10.google_dns_managed_zone.dns_managed_zone[0] - : cloud_logging_config: - - enable_logging: false + module.prod-dns-peer-landing-rev-10.google_dns_managed_zone.dns_managed_zone[0]: + cloud_logging_config: + - enable_logging: false description: Terraform managed. dns_name: 10.in-addr.arpa. dnssec_config: [] @@ -2994,7 +2994,7 @@ values: visibility: private module.prod-dns-peer-landing-root.google_dns_managed_zone.dns_managed_zone[0]: cloud_logging_config: - - enable_logging: false + - enable_logging: false description: Terraform managed. dns_name: . dnssec_config: [] @@ -3009,7 +3009,7 @@ values: visibility: private module.prod-dns-private-zone.google_dns_managed_zone.dns_managed_zone[0]: cloud_logging_config: - - enable_logging: false + - enable_logging: false description: Terraform managed. dns_name: prod.gcp.example.com. dnssec_config: [] @@ -3022,41 +3022,41 @@ values: service_directory_config: [] timeouts: null visibility: private - ? module.prod-dns-private-zone.google_dns_record_set.dns_record_set["A localhost"] - : managed_zone: prod-gcp-example-com + module.prod-dns-private-zone.google_dns_record_set.dns_record_set["A localhost"]: + managed_zone: prod-gcp-example-com name: localhost.prod.gcp.example.com. project: fast2-prod-net-spoke-0 routing_policy: [] rrdatas: - - 127.0.0.1 + - 127.0.0.1 ttl: 300 type: A - ? module.prod-spoke-firewall.google_compute_firewall.custom-rules["ingress-default-deny"] - : allow: [] + module.prod-spoke-firewall.google_compute_firewall.custom-rules["ingress-default-deny"]: + allow: [] deny: - - ports: [] - protocol: all + - ports: [] + protocol: all description: Deny and log any unmatched ingress traffic. direction: INGRESS disabled: false log_config: - - metadata: EXCLUDE_ALL_METADATA + - metadata: EXCLUDE_ALL_METADATA name: ingress-default-deny network: prod-spoke-0 priority: 65535 project: fast2-prod-net-spoke-0 source_ranges: - - 0.0.0.0/0 + - 0.0.0.0/0 source_service_accounts: null source_tags: null target_service_accounts: null target_tags: null timeouts: null - ? module.prod-spoke-project.google_compute_shared_vpc_host_project.shared_vpc_host[0] - : project: fast2-prod-net-spoke-0 + module.prod-spoke-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: fast2-prod-net-spoke-0 timeouts: null - ? module.prod-spoke-project.google_monitoring_monitored_project.primary["fast2-prod-net-landing-0"] - : metrics_scope: fast2-prod-net-landing-0 + module.prod-spoke-project.google_monitoring_monitored_project.primary["fast2-prod-net-landing-0"]: + metrics_scope: fast2-prod-net-landing-0 name: fast2-prod-net-spoke-0 timeouts: null module.prod-spoke-project.google_project.project[0]: @@ -3069,73 +3069,73 @@ values: project_id: fast2-prod-net-spoke-0 skip_delete: false timeouts: null - ? module.prod-spoke-project.google_project_iam_binding.authoritative["roles/dns.admin"] - : condition: [] + module.prod-spoke-project.google_project_iam_binding.authoritative["roles/dns.admin"]: + condition: [] members: - - serviceAccount:string + - serviceAccount:string project: fast2-prod-net-spoke-0 role: roles/dns.admin - ? module.prod-spoke-project.google_project_iam_binding.bindings["sa_delegated_grants"] - : condition: - - description: Production host project delegated grants. - expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/composer.sharedVpcAgent','roles/compute.networkUser','roles/compute.networkViewer','roles/container.hostServiceAgentUser','roles/multiclusterservicediscovery.serviceAgent','roles/vpcaccess.user']) - title: prod_stage3_sa_delegated_grants + module.prod-spoke-project.google_project_iam_binding.bindings["sa_delegated_grants"]: + condition: + - description: Production host project delegated grants. + expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/composer.sharedVpcAgent','roles/compute.networkUser','roles/compute.networkViewer','roles/container.hostServiceAgentUser','roles/multiclusterservicediscovery.serviceAgent','roles/vpcaccess.user']) + title: prod_stage3_sa_delegated_grants members: - - serviceAccount:string + - serviceAccount:string project: fast2-prod-net-spoke-0 role: roles/resourcemanager.projectIamAdmin module.prod-spoke-project.google_project_iam_member.servicenetworking[0]: condition: [] project: fast2-prod-net-spoke-0 role: roles/servicenetworking.serviceAgent - ? module.prod-spoke-project.google_project_service.project_services["compute.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["compute.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: compute.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["dns.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["dns.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: dns.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["iap.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["iap.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: iap.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["networkmanagement.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["networkmanagement.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: networkmanagement.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["servicenetworking.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["servicenetworking.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: servicenetworking.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["stackdriver.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["stackdriver.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: stackdriver.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service.project_services["vpcaccess.googleapis.com"] - : disable_dependent_services: false + module.prod-spoke-project.google_project_service.project_services["vpcaccess.googleapis.com"]: + disable_dependent_services: false disable_on_destroy: false project: fast2-prod-net-spoke-0 service: vpcaccess.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service_identity.jit_si["iap.googleapis.com"] - : project: fast2-prod-net-spoke-0 + module.prod-spoke-project.google_project_service_identity.jit_si["iap.googleapis.com"]: + project: fast2-prod-net-spoke-0 service: iap.googleapis.com timeouts: null - ? module.prod-spoke-project.google_project_service_identity.servicenetworking[0] - : project: fast2-prod-net-spoke-0 + module.prod-spoke-project.google_project_service_identity.servicenetworking[0]: + project: fast2-prod-net-spoke-0 service: servicenetworking.googleapis.com timeouts: null module.prod-spoke-vpc.google_compute_network.network[0]: @@ -3175,8 +3175,8 @@ values: project: fast2-prod-net-spoke-0 tags: null timeouts: null - ? module.prod-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/prod-default"] - : description: Default europe-west1 subnet for prod + module.prod-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west1/prod-default"]: + description: Default europe-west1 subnet for prod ip_cidr_range: 10.72.0.0/24 ipv6_access_type: null log_config: [] @@ -3188,8 +3188,8 @@ values: role: null secondary_ip_range: [] timeouts: null - ? module.prod-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west4/prod-default"] - : description: Default europe-west4 subnet for prod + module.prod-spoke-vpc.google_compute_subnetwork.subnetwork["europe-west4/prod-default"]: + description: Default europe-west4 subnet for prod ip_cidr_range: 10.88.0.0/24 ipv6_access_type: null log_config: [] @@ -3208,18 +3208,18 @@ values: enable_logging: true name: prod-spoke-0 networks: - - {} + - {} project: fast2-prod-net-spoke-0 timeouts: null module.spokes-dmz["primary"].google_compute_router.cr: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: Default route. - range: 0.0.0.0/0 - asn: 64512 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: Default route. + range: 0.0.0.0/0 + asn: 64512 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: prod-spoke-dmz-ew1-cr @@ -3306,10 +3306,10 @@ values: labels: null linked_interconnect_attachments: [] linked_router_appliance_instances: - - instances: - - {} - - {} - site_to_site_data_transfer: false + - instances: + - {} + - {} + site_to_site_data_transfer: false linked_vpc_network: [] linked_vpn_tunnels: [] location: europe-west1 @@ -3318,13 +3318,13 @@ values: timeouts: null module.spokes-dmz["secondary"].google_compute_router.cr: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: Default route. - range: 0.0.0.0/0 - asn: 64512 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: Default route. + range: 0.0.0.0/0 + asn: 64512 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: prod-spoke-dmz-ew4-cr @@ -3411,10 +3411,10 @@ values: labels: null linked_interconnect_attachments: [] linked_router_appliance_instances: - - instances: - - {} - - {} - site_to_site_data_transfer: false + - instances: + - {} + - {} + site_to_site_data_transfer: false linked_vpc_network: [] linked_vpn_tunnels: [] location: europe-west4 @@ -3423,23 +3423,23 @@ values: timeouts: null module.spokes-landing["primary"].google_compute_router.cr: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: GCP landing primary. - range: 10.64.0.0/17 - - description: GCP dev primary. - range: 10.68.0.0/16 - - description: GCP prod primary. - range: 10.72.0.0/16 - - description: GCP landing secondary. - range: 10.80.0.0/17 - - description: GCP dev secondary. - range: 10.84.0.0/16 - - description: GCP prod secondary. - range: 10.88.0.0/16 - asn: 64515 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: GCP landing primary. + range: 10.64.0.0/17 + - description: GCP dev primary. + range: 10.68.0.0/16 + - description: GCP prod primary. + range: 10.72.0.0/16 + - description: GCP landing secondary. + range: 10.80.0.0/17 + - description: GCP dev secondary. + range: 10.84.0.0/16 + - description: GCP prod secondary. + range: 10.88.0.0/16 + asn: 64515 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: prod-spoke-landing-ew1-cr @@ -3526,10 +3526,10 @@ values: labels: null linked_interconnect_attachments: [] linked_router_appliance_instances: - - instances: - - {} - - {} - site_to_site_data_transfer: false + - instances: + - {} + - {} + site_to_site_data_transfer: false linked_vpc_network: [] linked_vpn_tunnels: [] location: europe-west1 @@ -3538,23 +3538,23 @@ values: timeouts: null module.spokes-landing["secondary"].google_compute_router.cr: bgp: - - advertise_mode: CUSTOM - advertised_groups: [] - advertised_ip_ranges: - - description: GCP landing primary. - range: 10.64.0.0/17 - - description: GCP dev primary. - range: 10.68.0.0/16 - - description: GCP prod primary. - range: 10.72.0.0/16 - - description: GCP landing secondary. - range: 10.80.0.0/17 - - description: GCP dev secondary. - range: 10.84.0.0/16 - - description: GCP prod secondary. - range: 10.88.0.0/16 - asn: 64515 - keepalive_interval: 20 + - advertise_mode: CUSTOM + advertised_groups: [] + advertised_ip_ranges: + - description: GCP landing primary. + range: 10.64.0.0/17 + - description: GCP dev primary. + range: 10.68.0.0/16 + - description: GCP prod primary. + range: 10.72.0.0/16 + - description: GCP landing secondary. + range: 10.80.0.0/17 + - description: GCP dev secondary. + range: 10.84.0.0/16 + - description: GCP prod secondary. + range: 10.88.0.0/16 + asn: 64515 + keepalive_interval: 20 description: null encrypted_interconnect_router: null name: prod-spoke-landing-ew4-cr @@ -3641,10 +3641,10 @@ values: labels: null linked_interconnect_attachments: [] linked_router_appliance_instances: - - instances: - - {} - - {} - site_to_site_data_transfer: false + - instances: + - {} + - {} + site_to_site_data_transfer: false linked_vpc_network: [] linked_vpn_tunnels: [] location: europe-west4 @@ -3702,3 +3702,4 @@ outputs: shared_vpc_self_links: __missing__ tfvars: __missing__ vpn_gateway_endpoints: __missing__ + From 01533a4a66da722574991229c4803bf426dd9908 Mon Sep 17 00:00:00 2001 From: Ludo Date: Fri, 10 May 2024 07:56:11 +0200 Subject: [PATCH 19/29] update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac785d99a9..6c2177cdbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#2243](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2243)] Added new attributes Apigee organization and bumped up providers version ([apichick](https://github.com/apichick)) +- [[#2239](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2239)] Update README.md ([vicenteg](https://github.com/vicenteg)) +- [[#2230](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2230)] docs: :memo: fix error in phpIPAM terraform config by updating VPC pe… ([PapaPeskwo](https://github.com/PapaPeskwo)) - [[#2227](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2227)] Bump golang.org/x/net from 0.17.0 to 0.23.0 in /blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker ([dependabot[bot]](https://github.com/dependabot[bot])) - [[#2228](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2228)] Bump golang.org/x/net from 0.17.0 to 0.23.0 in /blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter ([dependabot[bot]](https://github.com/dependabot[bot])) - [[#2226](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2226)] fix cloud sql PSA after module upgrade ([simonebruzzechesse](https://github.com/simonebruzzechesse)) @@ -29,6 +32,10 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#2260](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2260)] Remove data source from folder module ([ludoo](https://github.com/ludoo)) +- [[#2253](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2253)] Misc FAST fixes ([juliocc](https://github.com/juliocc)) +- [[#2235](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2235)] Update FAST logging ([juliocc](https://github.com/juliocc)) +- [[#2233](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2233)] Fix permissions for branch network dev - read sa ([LucaPrete](https://github.com/LucaPrete)) - [[#2221](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2221)] Enable TFLint in FAST stages ([juliocc](https://github.com/juliocc)) - [[#2220](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2220)] Add tflint to pipelines ([juliocc](https://github.com/juliocc)) - [[#2218](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2218)] **incompatible change:** Allow multiple PSA service providers in net-vpc module ([ludoo](https://github.com/ludoo)) @@ -46,6 +53,19 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#2262](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2262)] Make Simple NVA route IAP traffic through NIC 0 ([juliocc](https://github.com/juliocc)) +- [[#2261](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2261)] Add Hybrid NAT support ([juliocc](https://github.com/juliocc)) +- [[#2260](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2260)] Remove data source from folder module ([ludoo](https://github.com/ludoo)) +- [[#2247](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2247)] Fix workstation-cluster module for private deployment ([simonebruzzechesse](https://github.com/simonebruzzechesse)) +- [[#2252](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2252)] Add support for labels to GKE backup plans ([ludoo](https://github.com/ludoo)) +- [[#2251](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2251)] Fix factory ingress policy services in vpc-sc module ([ludoo](https://github.com/ludoo)) +- [[#2248](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2248)] Added missing identity when connectors API is enabled ([jnahelou](https://github.com/jnahelou)) +- [[#2246](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2246)] Fixed issue with service networking DNS peering ([apichick](https://github.com/apichick)) +- [[#2243](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2243)] Added new attributes Apigee organization and bumped up providers version ([apichick](https://github.com/apichick)) +- [[#2244](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2244)] **incompatible change:** Removed BFD settings from net-vpn-ha module as it is not supported ([apichick](https://github.com/apichick)) +- [[#2241](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2241)] Use default labels on pubsub subscription when no override is provided ([wiktorn](https://github.com/wiktorn)) +- [[#2238](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2238)] fix: allow disabling node autoprovisioning ([kumadee](https://github.com/kumadee)) +- [[#2234](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2234)] Added build environment variables in cloud function v1 ([luigi-bitonti](https://github.com/luigi-bitonti)) - [[#2229](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2229)] **incompatible change:** Refactor vpc-sc support in project module, add support for dry run ([ludoo](https://github.com/ludoo)) - [[#2226](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2226)] fix cloud sql PSA after module upgrade ([simonebruzzechesse](https://github.com/simonebruzzechesse)) - [[#2224](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2224)] added missing option for exclusion scope ([cmalpe](https://github.com/cmalpe)) From 2b6c81f73d1851068e1eab027130c1457ec2a170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Fri, 10 May 2024 09:54:19 +0200 Subject: [PATCH 20/29] Update docs - gcp-network-admins -> gcp-vpc-network-admins --- fast/stages/0-bootstrap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md index 5d456302f5..758eac468f 100644 --- a/fast/stages/0-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -276,7 +276,7 @@ Before the first run, the following IAM groups must exist to allow IAM bindings - `gcp-billing-admins` - `gcp-devops` -- `gcp-network-admins` +- `gcp-vpc-network-admins` - `gcp-organization-admins` - `gcp-security-admins` From 5b3ed10cdadbd7f082b8b13b94c297de5c2ee536 Mon Sep 17 00:00:00 2001 From: Jan Van Bruggen Date: Fri, 10 May 2024 16:19:35 -0600 Subject: [PATCH 21/29] Fix bug from output typo in new project-factory module (#2264) `local.folders` is just a map of var-based keys to string manipulations on those keys, while `local.hierarchy` is the seemingly-intended map of var-based keys to generated IDs/numbers. see https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/modules/project-factory/factory-folders.tf#L32 vs. https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/modules/project-factory/factory-folders.tf#L39 Thank you for recently developing this convenient module! --- modules/project-factory/outputs.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/project-factory/outputs.tf b/modules/project-factory/outputs.tf index 9f49ebe986..5040e76671 100644 --- a/modules/project-factory/outputs.tf +++ b/modules/project-factory/outputs.tf @@ -16,7 +16,7 @@ output "folders" { description = "Folder ids." - value = local.folders + value = local.hierarchy } output "projects" { From 35a17a46ba99b4845fcc9a7ea6b140c254ea142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sat, 11 May 2024 15:19:09 +0000 Subject: [PATCH 22/29] Fix failing E2E tests --- modules/net-vpc/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index d29251cc96..fb115804c2 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -299,10 +299,12 @@ module "vpc" { { ranges = { myrange = "10.0.1.0/24" } # service_producer = "servicenetworking.googleapis.com" # default value + deletion_policy = "ABANDON" }, { ranges = { netapp = "10.0.2.0/24" } service_producer = "netapp.servicenetworking.goog" + deletion_policy = "ABANDON" } ] } From 6a3c7fe4445ca3f801db6a7d19cdd47587fe6a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 May 2024 12:00:39 +0200 Subject: [PATCH 23/29] CloudSQL PSC Endpoints support (#2242) * Add PSC endpoints consumers to net-address * Cloud SQL E2E tests --- modules/cloudsql-instance/README.md | 175 ++++++++++-------- modules/cloudsql-instance/variables.tf | 2 +- modules/net-address/README.md | 25 ++- modules/net-address/main.tf | 42 +---- modules/net-address/psc.tf | 102 ++++++++++ modules/net-address/variables.tf | 26 ++- tests/collectors.py | 11 +- tests/examples/variables.tf | 2 +- .../setup_module/e2e_tests.tfvars.tftpl | 6 + tests/examples_e2e/setup_module/main.tf | 46 ++++- tests/fixtures.py | 11 +- tests/fixtures/cloudsql-instance.tf | 33 ++++ tests/fixtures/cloudsql-kms-iam-grant.tf | 23 +++ .../cloudsql_instance/examples/insights.yaml | 4 +- .../cloudsql_instance/examples/psc.yaml | 38 ++++ .../cloudsql_instance/examples/public-ip.yaml | 25 +-- .../cloudsql_instance/examples/replicas.yaml | 2 +- .../cloudsql_instance/examples/simple.yaml | 40 ++-- .../examples/psc-service-attachment.yaml | 29 +++ 19 files changed, 464 insertions(+), 178 deletions(-) create mode 100644 modules/net-address/psc.tf create mode 100644 tests/fixtures/cloudsql-instance.tf create mode 100644 tests/fixtures/cloudsql-kms-iam-grant.tf create mode 100644 tests/modules/cloudsql_instance/examples/psc.yaml create mode 100644 tests/modules/net_address/examples/psc-service-attachment.yaml diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md index c1474cec1a..6ecf42a32d 100644 --- a/modules/cloudsql-instance/README.md +++ b/modules/cloudsql-instance/README.md @@ -1,4 +1,4 @@ -# Cloud SQL instance with read replicas +# Cloud SQL instance module This module manages the creation of Cloud SQL instances with potential read replicas in other regions. It can also create an initial set of users and databases via the `users` and `databases` parameters. @@ -6,7 +6,24 @@ Note that this module assumes that some options are the same for both the primar *Warning:* if you use the `users` field, you terraform state will contain each user's password in plain text. -## Simple example + +- [Examples](#examples) + - [Simple example](#simple-example) + - [Cross-regional read replica](#cross-regional-read-replica) + - [Custom flags, databases and users](#custom-flags-databases-and-users) + - [CMEK encryption](#cmek-encryption) + - [Instance with PSC enabled](#instance-with-psc-enabled) + - [Enable public IP](#enable-public-ip) + - [Query Insights](#query-insights) + - [Maintenance Config](#maintenance-config) + - [SSL Config](#ssl-config) +- [Variables](#variables) +- [Outputs](#outputs) +- [Fixtures](#fixtures) + + +## Examples +### Simple example This example shows how to setup a project, VPC and a standalone Cloud SQL instance. @@ -14,10 +31,12 @@ This example shows how to setup a project, VPC and a standalone Cloud SQL instan module "project" { source = "./fabric/modules/project" billing_account = var.billing_account_id - parent = var.organization_id - name = "my-db-project" + parent = var.folder_id + name = "db-prj" + prefix = var.prefix services = [ - "servicenetworking.googleapis.com" + "servicenetworking.googleapis.com", + "sqladmin.googleapis.com", ] } @@ -25,9 +44,18 @@ module "vpc" { source = "./fabric/modules/net-vpc" project_id = module.project.project_id name = "my-network" + # need only one - psa_config or subnets_psc psa_configs = [{ - ranges = { cloud-sql = "10.60.0.0/16" } + ranges = { cloud-sql = "10.60.0.0/16" } + deletion_policy = "ABANDON" }] + subnets_psc = [ + { + ip_cidr_range = "10.0.3.0/24" + name = "psc" + region = var.region + } + ] } module "db" { @@ -38,17 +66,20 @@ module "db" { psa_config = { private_network = module.vpc.self_link } + # psc_allowed_consumer_projects = [var.project_id] } } - name = "db" - region = "europe-west1" - database_version = "POSTGRES_13" - tier = "db-g1-small" + name = "db" + region = var.region + database_version = "POSTGRES_13" + tier = "db-g1-small" + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=3 resources=11 inventory=simple.yaml +# tftest modules=3 resources=14 inventory=simple.yaml e2e ``` -## Cross-regional read replica +### Cross-regional read replica ```hcl module "db" { @@ -61,21 +92,23 @@ module "db" { } } } - prefix = "myprefix" name = "db" - region = "europe-west1" + prefix = "myprefix" + region = var.region database_version = "POSTGRES_13" tier = "db-g1-small" replicas = { - replica1 = { region = "europe-west3", encryption_key_name = null } - replica2 = { region = "us-central1", encryption_key_name = null } + replica1 = { region = "europe-west3" } + replica2 = { region = "us-central1" } } + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=3 inventory=replicas.yaml +# tftest modules=1 resources=3 inventory=replicas.yaml e2e ``` -## Custom flags, databases and users +### Custom flags, databases and users ```hcl module "db" { @@ -89,7 +122,7 @@ module "db" { } } name = "db" - region = "europe-west1" + region = var.region database_version = "MYSQL_8_0" tier = "db-g1-small" @@ -112,47 +145,19 @@ module "db" { password = "mypassword" } } + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=6 inventory=custom.yaml +# tftest modules=1 resources=6 inventory=custom.yaml e2e ``` ### CMEK encryption ```hcl - -module "project" { - source = "./fabric/modules/project" - billing_account = var.billing_account_id - parent = var.organization_id - name = "my-db-project" - services = [ - "servicenetworking.googleapis.com", - "sqladmin.googleapis.com", - ] -} - -module "kms" { - source = "./fabric/modules/kms" - project_id = module.project.project_id - keyring = { - name = "keyring" - location = var.region - } - keys = { - key-sql = { - iam = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project.service_accounts.robots.sqladmin}" - ] - } - } - } -} - module "db" { source = "./fabric/modules/cloudsql-instance" - project_id = module.project.project_id - encryption_key_name = module.kms.keys["key-sql"].id + project_id = var.project_id + encryption_key_name = var.kms_key.id network_config = { connectivity = { psa_config = { @@ -160,13 +165,15 @@ module "db" { } } } - name = "db" - region = var.region - database_version = "POSTGRES_13" - tier = "db-g1-small" + name = "db" + region = var.region + database_version = "POSTGRES_13" + tier = "db-g1-small" + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=3 resources=10 +# tftest modules=1 resources=2 fixtures=fixtures/cloudsql-kms-iam-grant.tf e2e ``` ### Instance with PSC enabled @@ -177,22 +184,25 @@ module "db" { project_id = var.project_id network_config = { connectivity = { - psc_allowed_consumer_projects = ["my-project-id"] + psc_allowed_consumer_projects = [var.project_id] } } prefix = "myprefix" name = "db" - region = "europe-west1" + region = var.region availability_type = "REGIONAL" database_version = "POSTGRES_13" tier = "db-g1-small" + + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=psc.yaml e2e ``` ### Enable public IP -Use `ipv_enabled` to create instances with a public IP. +Use `public_ipv4` to create instances with a public IP. ```hcl module "db" { @@ -206,15 +216,14 @@ module "db" { } } } - name = "db" - region = "europe-west1" - tier = "db-g1-small" - database_version = "MYSQL_8_0" - replicas = { - replica1 = { region = "europe-west3", encryption_key_name = null } - } + name = "db" + region = var.region + tier = "db-g1-small" + database_version = "MYSQL_8_0" + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=2 inventory=public-ip.yaml +# tftest modules=1 resources=1 inventory=public-ip.yaml e2e ``` ### Query Insights @@ -233,15 +242,17 @@ module "db" { } } name = "db" - region = "europe-west1" + region = var.region database_version = "POSTGRES_13" tier = "db-g1-small" insights_config = { query_string_length = 2048 } + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=1 inventory=insights.yaml +# tftest modules=1 resources=1 inventory=insights.yaml e2e ``` ### Maintenance Config @@ -260,13 +271,15 @@ module "db" { } } name = "db" - region = "europe-west1" + region = var.region database_version = "POSTGRES_13" tier = "db-g1-small" - maintenance_config = {} + maintenance_config = {} + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 e2e ``` ### SSL Config @@ -285,13 +298,15 @@ module "db" { } } name = "db" - region = "europe-west1" + region = var.region database_version = "POSTGRES_13" tier = "db-g1-small" - ssl = {} + ssl = {} + gcp_deletion_protection = false + terraform_deletion_protection = false } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 e2e ``` ## Variables @@ -322,7 +337,7 @@ module "db" { | [labels](variables.tf#L140) | Labels to be attached to all instances. | map(string) | | null | | [maintenance_config](variables.tf#L146) | Set maintenance window configuration and maintenance deny period (up to 90 days). Date format: 'yyyy-mm-dd'. | object({…}) | | {} | | [prefix](variables.tf#L207) | Optional prefix used to generate instance names. | string | | null | -| [replicas](variables.tf#L227) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | +| [replicas](variables.tf#L227) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | | [root_password](variables.tf#L236) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string | | null | | [ssl](variables.tf#L242) | Setting to enable SSL, set config and certificates. | object({…}) | | {} | | [terraform_deletion_protection](variables.tf#L258) | Prevent terraform from deleting instances. | bool | | true | @@ -350,4 +365,8 @@ module "db" { | [self_link](outputs.tf#L114) | Self link of the primary instance. | | | [self_links](outputs.tf#L119) | Self links of all instances. | | | [user_passwords](outputs.tf#L127) | Map of containing the password of all users created through terraform. | ✓ | + +## Fixtures + +- [cloudsql-kms-iam-grant.tf](../../tests/fixtures/cloudsql-kms-iam-grant.tf) diff --git a/modules/cloudsql-instance/variables.tf b/modules/cloudsql-instance/variables.tf index e67dd3979a..613e106e74 100644 --- a/modules/cloudsql-instance/variables.tf +++ b/modules/cloudsql-instance/variables.tf @@ -228,7 +228,7 @@ variable "replicas" { description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation." type = map(object({ region = string - encryption_key_name = string + encryption_key_name = optional(string) })) default = {} } diff --git a/modules/net-address/README.md b/modules/net-address/README.md index 55b84108a2..5f53063368 100644 --- a/modules/net-address/README.md +++ b/modules/net-address/README.md @@ -122,6 +122,26 @@ module "addresses" { # tftest modules=1 resources=1 inventory=psc.yaml e2e ``` +To create PSC address targeting a service regional provider use the `service_attachment` property. +```hcl +module "addresses" { + source = "./fabric/modules/net-address" + project_id = var.project_id + psc_addresses = { + cloudsql-one = { + address = "10.0.16.32" + subnet_self_link = var.subnet.self_link + region = var.region + service_attachment = { + psc_service_attachment_link = module.cloudsql-instance.psc_service_attachment_link + } + } + } +} +# tftest modules=2 resources=3 fixtures=fixtures/cloudsql-instance.tf inventory=psc-service-attachment.yaml e2e +``` + + ### IPSec Interconnect addresses ```hcl @@ -176,8 +196,8 @@ module "addresses" { | [internal_addresses](variables.tf#L50) | Map of internal addresses to create, keyed by name. | map(object({…})) | | {} | | [ipsec_interconnect_addresses](variables.tf#L65) | Map of internal addresses used for HPA VPN over Cloud Interconnect. | map(object({…})) | | {} | | [network_attachments](variables.tf#L84) | PSC network attachments, names as keys. | map(object({…})) | | {} | -| [psa_addresses](variables.tf#L102) | Map of internal addresses used for Private Service Access. | map(object({…})) | | {} | -| [psc_addresses](variables.tf#L115) | Map of internal addresses used for Private Service Connect. | map(object({…})) | | {} | +| [psa_addresses](variables.tf#L102) | Map of internal addresses used for Private Service Access. | map(object({…})) | | {} | +| [psc_addresses](variables.tf#L114) | Map of internal addresses used for Private Service Connect. | map(object({…})) | | {} | ## Outputs @@ -193,5 +213,6 @@ module "addresses" { ## Fixtures +- [cloudsql-instance.tf](../../tests/fixtures/cloudsql-instance.tf) - [net-vpc-ipv6.tf](../../tests/fixtures/net-vpc-ipv6.tf) diff --git a/modules/net-address/main.tf b/modules/net-address/main.tf index 20b8b6a16e..e7c69413cd 100644 --- a/modules/net-address/main.tf +++ b/modules/net-address/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +14,6 @@ * limitations under the License. */ -locals { - network_attachments = { - for k, v in var.network_attachments : k => merge(v, { - region = regex("regions/([^/]+)", v.subnet_self_link)[0] - # not using the full self link generates a permadiff - subnet_self_link = ( - startswith(v.subnet_self_link, "https://") - ? v.subnet_self_link - : "https://www.googleapis.com/compute/v1/${v.subnet_self_link}" - ) - }) - } -} - resource "google_compute_global_address" "global" { for_each = var.global_addresses project = var.project_id @@ -66,18 +52,6 @@ resource "google_compute_address" "internal" { subnetwork = each.value.subnetwork } -resource "google_compute_global_address" "psc" { - for_each = var.psc_addresses - project = var.project_id - name = coalesce(each.value.name, each.key) - description = each.value.description - address = try(each.value.address, null) - address_type = "INTERNAL" - network = each.value.network - purpose = "PRIVATE_SERVICE_CONNECT" - # labels = lookup(var.internal_address_labels, each.key, {}) -} - resource "google_compute_global_address" "psa" { for_each = var.psa_addresses project = var.project_id @@ -104,17 +78,3 @@ resource "google_compute_address" "ipsec_interconnect" { purpose = "IPSEC_INTERCONNECT" } -resource "google_compute_network_attachment" "default" { - provider = google-beta - for_each = local.network_attachments - project = var.project_id - region = each.value.region - name = each.key - description = each.value.description - connection_preference = ( - each.value.automatic_connection ? "ACCEPT_AUTOMATIC" : "ACCEPT_MANUAL" - ) - subnetworks = [each.value.subnet_self_link] - producer_accept_lists = each.value.producer_accept_lists - producer_reject_lists = each.value.producer_reject_lists -} diff --git a/modules/net-address/psc.tf b/modules/net-address/psc.tf new file mode 100644 index 0000000000..2fbf75788f --- /dev/null +++ b/modules/net-address/psc.tf @@ -0,0 +1,102 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + network_attachments = { + for k, v in var.network_attachments : k => merge(v, { + region = regex("regions/([^/]+)", v.subnet_self_link)[0] + # not using the full self link generates a permadiff + subnet_self_link = ( + startswith(v.subnet_self_link, "https://") + ? v.subnet_self_link + : "https://www.googleapis.com/compute/v1/${v.subnet_self_link}" + ) + }) + } + regional_psc = { + for name, psc in var.psc_addresses : name => psc if psc.region != null + + } + global_psc = { + for name, psc in var.psc_addresses : name => psc if psc.region == null + } +} + +resource "google_compute_network_attachment" "default" { + provider = google-beta + for_each = local.network_attachments + project = var.project_id + region = each.value.region + name = each.key + description = each.value.description + connection_preference = ( + each.value.automatic_connection ? "ACCEPT_AUTOMATIC" : "ACCEPT_MANUAL" + ) + subnetworks = [each.value.subnet_self_link] + producer_accept_lists = each.value.producer_accept_lists + producer_reject_lists = each.value.producer_reject_lists +} + +# global PSC services +resource "google_compute_global_address" "psc" { + for_each = local.global_psc + project = var.project_id + name = coalesce(each.value.name, each.key) + description = each.value.description + address = try(each.value.address, null) + address_type = "INTERNAL" + network = each.value.network + purpose = "PRIVATE_SERVICE_CONNECT" + # labels = lookup(var.internal_address_labels, each.key, {}) +} + +resource "google_compute_global_forwarding_rule" "psc_consumer" { + for_each = { for name, psc in local.global_psc : name => psc if psc.service_attachment != null } + name = coalesce(each.value.name, each.key) + project = var.project_id + subnetwork = each.value.subnet_self_link + ip_address = google_compute_global_address.psc[each.key].self_link + load_balancing_scheme = "" + target = each.value.service_attachment.psc_service_attachment_link +} + +# regional PSC services +resource "google_compute_address" "psc" { + for_each = local.regional_psc + project = var.project_id + name = coalesce(each.value.name, each.key) + address = try(each.value.address, null) + address_type = "INTERNAL" + description = each.value.description + network = each.value.network + # purpose not applicable for regional address + # purpose = "PRIVATE_SERVICE_CONNECT" + region = each.value.region + subnetwork = each.value.subnet_self_link + # labels = lookup(var.internal_address_labels, each.key, {}) +} + +resource "google_compute_forwarding_rule" "psc_consumer" { + for_each = { for name, psc in local.regional_psc : name => psc if psc.service_attachment != null } + name = coalesce(each.value.name, each.key) + project = var.project_id + region = each.value.region + subnetwork = each.value.subnet_self_link + ip_address = google_compute_address.psc[each.key].self_link + load_balancing_scheme = "" + recreate_closed_psc = true + target = each.value.service_attachment.psc_service_attachment_link +} diff --git a/modules/net-address/variables.tf b/modules/net-address/variables.tf index 190bffdde6..236c23960a 100644 --- a/modules/net-address/variables.tf +++ b/modules/net-address/variables.tf @@ -107,7 +107,6 @@ variable "psa_addresses" { prefix_length = number description = optional(string, "Terraform managed.") name = optional(string) - })) default = {} } @@ -115,10 +114,27 @@ variable "psa_addresses" { variable "psc_addresses" { description = "Map of internal addresses used for Private Service Connect." type = map(object({ - address = string - network = string - description = optional(string, "Terraform managed.") - name = optional(string) + address = string + description = optional(string, "Terraform managed.") + name = optional(string) + network = optional(string) + region = optional(string) + subnet_self_link = optional(string) + service_attachment = optional(object({ # so we can safely check if service_attachemnt != null in for_each + psc_service_attachment_link = string + })) })) default = {} + validation { + condition = alltrue([for key, value in var.psc_addresses : (value.region != null || (value.region == null && value.network != null))]) + error_message = "Provide network if creating global PSC addresses / endpoints." + } + validation { + condition = alltrue([for key, value in var.psc_addresses : (value.region == null || (value.region != null && value.subnet_self_link != null))]) + error_message = "Provide subnet_self_link if creating regional PSC addresses / endpoints." + } + validation { + condition = alltrue([for key, value in var.psc_addresses : !(value.subnet_self_link != null && value.network != null)]) + error_message = "Do not provide network and subnet_self_link at the same time" + } } diff --git a/tests/collectors.py b/tests/collectors.py index 749e192c50..310b8151cc 100644 --- a/tests/collectors.py +++ b/tests/collectors.py @@ -87,8 +87,15 @@ def __init__(self, name, parent, module, inventory, tf_var_files, self.extra_files = extra_files def runtest(self): - s = plan_validator(self.module, self.inventory, self.parent.path.parent, - self.tf_var_files, self.extra_files) + try: + summary = plan_validator(self.module, self.inventory, self.parent.path.parent, + self.tf_var_files, self.extra_files) + except AssertionError: + def full_paths(x): + return [(self.parent.path.parent / x ) for x in x] + print(f'Error in inventory file: {" ".join(full_paths(self.inventory))}') + print(f'To regenerate inventory run: python tools/plan_summary.py {self.module} {" ".join(full_paths(self.tf_var_files))}') + raise def reportinfo(self): return self.path, None, self.name diff --git a/tests/examples/variables.tf b/tests/examples/variables.tf index 29dc8d8940..d999609b0e 100644 --- a/tests/examples/variables.tf +++ b/tests/examples/variables.tf @@ -84,7 +84,7 @@ variable "subnet_psc_1" { name = "subnet_name" region = "subnet_region" cidr = "subnet_cidr" - self_link = "subnet_self_link" + self_link = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west8/subnetworks/subnet" } } diff --git a/tests/examples_e2e/setup_module/e2e_tests.tfvars.tftpl b/tests/examples_e2e/setup_module/e2e_tests.tfvars.tftpl index 5bb6d779ec..fd63d450fe 100644 --- a/tests/examples_e2e/setup_module/e2e_tests.tfvars.tftpl +++ b/tests/examples_e2e/setup_module/e2e_tests.tfvars.tftpl @@ -37,6 +37,12 @@ subnet = { cidr = "${subnet.ip_cidr_range}" self_link = "${subnet.self_link}" } +subnet_psc_1 = { + name = "${subnet_psc_1.name}" + region = "${subnet_psc_1.region}" + cidr = "${subnet_psc_1.ip_cidr_range}" + self_link = "${subnet_psc_1.self_link}" +} vpc = { name = "${vpc.name}" self_link = "${vpc.self_link}" diff --git a/tests/examples_e2e/setup_module/main.tf b/tests/examples_e2e/setup_module/main.tf index bd48e4fbd9..4e1d1931bf 100644 --- a/tests/examples_e2e/setup_module/main.tf +++ b/tests/examples_e2e/setup_module/main.tf @@ -15,7 +15,8 @@ locals { prefix = "${var.prefix}-${var.timestamp}${var.suffix}" jit_services = [ - "storage.googleapis.com", # no permissions granted by default + "storage.googleapis.com", # no permissions granted by default + "sqladmin.googleapis.com", # roles/cloudsql.serviceAgent ] services = [ # trimmed down list of services, to be extended as needed @@ -35,6 +36,7 @@ locals { "secretmanager.googleapis.com", "servicenetworking.googleapis.com", "serviceusage.googleapis.com", + "sqladmin.googleapis.com", "stackdriver.googleapis.com", "storage-component.googleapis.com", "storage.googleapis.com", @@ -114,6 +116,36 @@ resource "google_compute_subnetwork" "proxy_only_regional" { role = "ACTIVE" } +resource "google_compute_subnetwork" "psc" { + project = google_project.project.project_id + network = google_compute_network.network.name + name = "psc-regional" + region = var.region + ip_cidr_range = "10.0.19.0/24" + purpose = "PRIVATE_SERVICE_CONNECT" +} + +### PSA ### + +resource "google_compute_global_address" "psa_ranges" { + project = google_project.project.project_id + network = google_compute_network.network.id + name = "psa-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + address = "10.0.20.0" + prefix_length = 22 +} + +resource "google_service_networking_connection" "psa_connection" { + network = google_compute_network.network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.psa_ranges.name] + deletion_policy = "ABANDON" +} + +### END OF PSA + resource "google_service_account" "service_account" { account_id = "e2e-service-account" project = google_project.project.project_id @@ -141,6 +173,12 @@ resource "google_project_service_identity" "jit_si" { depends_on = [google_project_service.project_service] } +resource "google_project_iam_binding" "cloudsql_agent" { + members = ["serviceAccount:service-${google_project.project.number}@gcp-sa-cloud-sql.iam.gserviceaccount.com"] + project = google_project.project.project_id + role = "roles/cloudsql.serviceAgent" + depends_on = [google_project_service_identity.jit_si] +} resource "local_file" "terraform_tfvars" { filename = "e2e_tests.tfvars" @@ -168,6 +206,12 @@ resource "local_file" "terraform_tfvars" { ip_cidr_range = google_compute_subnetwork.subnetwork.ip_cidr_range self_link = google_compute_subnetwork.subnetwork.self_link } + subnet_psc_1 = { + name = google_compute_subnetwork.psc.name + region = google_compute_subnetwork.psc.region + ip_cidr_range = google_compute_subnetwork.psc.ip_cidr_range + self_link = google_compute_subnetwork.psc.self_link + } vpc = { name = google_compute_network.network.name self_link = google_compute_network.network.self_link diff --git a/tests/fixtures.py b/tests/fixtures.py index 50b6d8ad6a..1d725135f4 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -186,8 +186,13 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, # print(yaml.dump({'counts': summary.counts})) if 'values' in inventory: - validate_plan_object(inventory['values'], summary.values, relative_path, - "") + try: + validate_plan_object(inventory['values'], summary.values, relative_path, + "") + except AssertionError: + print(f'\n{path}') + print(yaml.dump({'values': summary.values})) + raise if 'counts' in inventory: try: @@ -199,6 +204,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, assert plan_count == expected_count, \ f'{relative_path}: count of {type_} resources failed. Got {plan_count}, expected {expected_count}' except AssertionError: + print(f'\n{path}') print(yaml.dump({'counts': summary.counts})) raise @@ -218,6 +224,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, f'{relative_path}: output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`' except AssertionError: if _buffer: + print(f'\n{path}') print(yaml.dump(_buffer)) raise return summary diff --git a/tests/fixtures/cloudsql-instance.tf b/tests/fixtures/cloudsql-instance.tf new file mode 100644 index 0000000000..6fed8d6ada --- /dev/null +++ b/tests/fixtures/cloudsql-instance.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2024 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 "cloudsql-instance" { + source = "./fabric/modules/cloudsql-instance" + project_id = var.project_id + network_config = { + connectivity = { + psc_allowed_consumer_projects = [var.project_id] + } + } + ## define a consumer project with an endpoint within the project + name = "db" + region = var.region + availability_type = "REGIONAL" + database_version = "POSTGRES_13" + tier = "db-g1-small" + gcp_deletion_protection = false + terraform_deletion_protection = false +} diff --git a/tests/fixtures/cloudsql-kms-iam-grant.tf b/tests/fixtures/cloudsql-kms-iam-grant.tf new file mode 100644 index 0000000000..9ea53a0268 --- /dev/null +++ b/tests/fixtures/cloudsql-kms-iam-grant.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2024 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_kms_crypto_key_iam_binding" "encrypt_decrypt" { + crypto_key_id = var.kms_key.id + members = [ + "serviceAccount:service-${var.project_number}@gcp-sa-cloud-sql.iam.gserviceaccount.com" + ] + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" +} diff --git a/tests/modules/cloudsql_instance/examples/insights.yaml b/tests/modules/cloudsql_instance/examples/insights.yaml index 84a4245ddf..a2c46f5b9b 100644 --- a/tests/modules/cloudsql_instance/examples/insights.yaml +++ b/tests/modules/cloudsql_instance/examples/insights.yaml @@ -17,11 +17,11 @@ values: database_version: POSTGRES_13 name: db project: project-id - region: europe-west1 + region: europe-west8 settings: - activation_policy: ALWAYS availability_type: ZONAL - deletion_protection_enabled: true + deletion_protection_enabled: false disk_autoresize: true disk_type: PD_SSD insights_config: diff --git a/tests/modules/cloudsql_instance/examples/psc.yaml b/tests/modules/cloudsql_instance/examples/psc.yaml new file mode 100644 index 0000000000..722700d7bd --- /dev/null +++ b/tests/modules/cloudsql_instance/examples/psc.yaml @@ -0,0 +1,38 @@ +# Copyright 2024 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.db.google_sql_database_instance.primary: + database_version: POSTGRES_13 + deletion_protection: false + name: myprefix-db + project: project-id + region: europe-west8 + settings: + - activation_policy: ALWAYS + availability_type: REGIONAL + deletion_protection_enabled: false + ip_configuration: + - ipv4_enabled: false + private_network: null + psc_config: + - allowed_consumer_projects: + - project-id + psc_enabled: true + tier: db-g1-small + +counts: + google_sql_database_instance: 1 + modules: 1 + resources: 1 diff --git a/tests/modules/cloudsql_instance/examples/public-ip.yaml b/tests/modules/cloudsql_instance/examples/public-ip.yaml index f3ca86d3a5..e216ee3b0f 100644 --- a/tests/modules/cloudsql_instance/examples/public-ip.yaml +++ b/tests/modules/cloudsql_instance/examples/public-ip.yaml @@ -17,30 +17,11 @@ values: database_version: MYSQL_8_0 name: db project: project-id - region: europe-west1 + region: europe-west8 settings: - activation_policy: ALWAYS availability_type: ZONAL - deletion_protection_enabled: true - disk_autoresize: true - disk_type: PD_SSD - insights_config: [] - ip_configuration: - - allocated_ip_range: null - authorized_networks: [] - ipv4_enabled: true - private_network: projects/xxx/global/networks/aaa - tier: db-g1-small - module.db.google_sql_database_instance.replicas["replica1"]: - database_version: MYSQL_8_0 - master_instance_name: db - name: replica1 - project: project-id - region: europe-west3 - settings: - - activation_policy: ALWAYS - availability_type: ZONAL - deletion_protection_enabled: true + deletion_protection_enabled: false disk_autoresize: true disk_type: PD_SSD insights_config: [] @@ -52,4 +33,4 @@ values: tier: db-g1-small counts: - google_sql_database_instance: 2 + google_sql_database_instance: 1 diff --git a/tests/modules/cloudsql_instance/examples/replicas.yaml b/tests/modules/cloudsql_instance/examples/replicas.yaml index 1ed30f9bc9..bb337d4457 100644 --- a/tests/modules/cloudsql_instance/examples/replicas.yaml +++ b/tests/modules/cloudsql_instance/examples/replicas.yaml @@ -18,7 +18,7 @@ values: database_version: POSTGRES_13 name: myprefix-db project: project-id - region: europe-west1 + region: europe-west8 module.db.google_sql_database_instance.replicas["replica1"]: clone: [] database_version: POSTGRES_13 diff --git a/tests/modules/cloudsql_instance/examples/simple.yaml b/tests/modules/cloudsql_instance/examples/simple.yaml index 5103c12b4e..4221eb288c 100644 --- a/tests/modules/cloudsql_instance/examples/simple.yaml +++ b/tests/modules/cloudsql_instance/examples/simple.yaml @@ -16,10 +16,10 @@ values: module.db.google_sql_database_instance.primary: clone: [] database_version: POSTGRES_13 - deletion_protection: true + deletion_protection: false name: db - project: my-db-project - region: europe-west1 + project: test-db-prj + region: europe-west8 restore_backup_context: [] root_password: null settings: @@ -30,7 +30,7 @@ values: collation: null data_cache_config: [] database_flags: [] - deletion_protection_enabled: true + deletion_protection_enabled: false deny_maintenance_period: [] disk_autoresize: true disk_autoresize_limit: 0 @@ -54,25 +54,25 @@ values: module.project.google_project.project[0]: auto_create_network: false billing_account: 123456-123456-123456 - folder_id: null + folder_id: '1122334455' labels: null - name: my-db-project - org_id: '1122334455' - project_id: my-db-project + name: test-db-prj + org_id: null + project_id: test-db-prj skip_delete: false timeouts: null module.project.google_project_iam_member.servicenetworking[0]: condition: [] - project: my-db-project + project: test-db-prj role: roles/servicenetworking.serviceAgent module.project.google_project_service.project_services["servicenetworking.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false - project: my-db-project + project: test-db-prj service: servicenetworking.googleapis.com timeouts: null module.project.google_project_service_identity.servicenetworking[0]: - project: my-db-project + project: test-db-prj service: servicenetworking.googleapis.com timeouts: null module.vpc.google_compute_global_address.psa_ranges["servicenetworking-googleapis-com-cloud-sql"]: @@ -82,7 +82,7 @@ values: ip_version: null name: servicenetworking-googleapis-com-cloud-sql prefix_length: 16 - project: my-db-project + project: test-db-prj purpose: VPC_PEERING timeouts: null module.vpc.google_compute_network.network[0]: @@ -92,14 +92,14 @@ values: enable_ula_internal_ipv6: null name: my-network network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL - project: my-db-project + project: test-db-prj routing_mode: GLOBAL timeouts: null module.vpc.google_compute_network_peering_routes_config.psa_routes["servicenetworking.googleapis.com"]: export_custom_routes: false import_custom_routes: false network: my-network - project: my-db-project + project: test-db-prj timeouts: null module.vpc.google_compute_route.gateway["private-googleapis"]: description: Terraform-managed. @@ -111,7 +111,7 @@ values: next_hop_instance: null next_hop_vpn_tunnel: null priority: 1000 - project: my-db-project + project: test-db-prj tags: null timeouts: null module.vpc.google_compute_route.gateway["restricted-googleapis"]: @@ -124,11 +124,11 @@ values: next_hop_instance: null next_hop_vpn_tunnel: null priority: 1000 - project: my-db-project + project: test-db-prj tags: null timeouts: null module.vpc.google_service_networking_connection.psa_connection["servicenetworking.googleapis.com"]: - deletion_policy: null + deletion_policy: ABANDON reserved_peering_ranges: - servicenetworking-googleapis-com-cloud-sql service: servicenetworking.googleapis.com @@ -141,11 +141,11 @@ counts: google_compute_route: 2 google_project: 1 google_project_iam_member: 1 - google_project_service: 1 - google_project_service_identity: 1 + google_project_service: 2 + google_project_service_identity: 2 google_service_networking_connection: 1 google_sql_database_instance: 1 modules: 3 - resources: 11 + resources: 14 outputs: {} diff --git a/tests/modules/net_address/examples/psc-service-attachment.yaml b/tests/modules/net_address/examples/psc-service-attachment.yaml new file mode 100644 index 0000000000..7c3eaca02a --- /dev/null +++ b/tests/modules/net_address/examples/psc-service-attachment.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 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.addresses.google_compute_forwarding_rule.psc_consumer["cloudsql-one"]: + load_balancing_scheme: '' + name: cloudsql-one + project: project-id + recreate_closed_psc: true + region: europe-west8 + subnetwork: subnet_self_link + module.addresses.google_compute_address.psc["cloudsql-one"]: + address: 10.0.16.32 + address_type: INTERNAL + description: Terraform managed. + name: cloudsql-one + project: project-id + subnetwork: subnet_self_link From af253c97022068c8c0500988b45e1ef0f727c7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Niesiob=C4=99dzki?= Date: Sun, 12 May 2024 21:02:04 +0200 Subject: [PATCH 24/29] Fix 0-bootstrap iam_by_principals not taking into account all principals (#2267) * Fix 0-bootstrap iam_by_principals not taking into account all principals * Add test-case for iam_by_principals for 0-bootstrap stage --------- Co-authored-by: Ludovico Magnocavallo --- fast/stages/0-bootstrap/organization.tf | 10 +++++++-- tests/collectors.py | 2 +- .../s0_bootstrap/iam_by_principals.tfvars | 20 +++++++++++++++++ .../s0_bootstrap/iam_by_principals.yaml | 22 +++++++++++++++++++ tests/fast/stages/s0_bootstrap/tftest.yaml | 2 ++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/fast/stages/s0_bootstrap/iam_by_principals.tfvars create mode 100644 tests/fast/stages/s0_bootstrap/iam_by_principals.yaml diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf index f91b4e8c14..fdd08937a7 100644 --- a/fast/stages/0-bootstrap/organization.tf +++ b/fast/stages/0-bootstrap/organization.tf @@ -138,8 +138,14 @@ module "organization" { organization_id = module.organization-logging.id # human (groups) IAM bindings iam_by_principals = { - for k, v in local.iam_principals : - k => distinct(concat(v, lookup(var.iam_by_principals, k, []))) + for key in distinct(concat( + keys(local.iam_principals), + keys(var.iam_by_principals), + )) : + key => distinct(concat( + lookup(local.iam_principals, key, []), + lookup(var.iam_by_principals, key, []), + )) } # machine (service accounts) IAM bindings iam = merge( diff --git a/tests/collectors.py b/tests/collectors.py index 310b8151cc..9e26c06774 100644 --- a/tests/collectors.py +++ b/tests/collectors.py @@ -92,7 +92,7 @@ def runtest(self): self.tf_var_files, self.extra_files) except AssertionError: def full_paths(x): - return [(self.parent.path.parent / x ) for x in x] + return [str(self.parent.path.parent / x ) for x in x] print(f'Error in inventory file: {" ".join(full_paths(self.inventory))}') print(f'To regenerate inventory run: python tools/plan_summary.py {self.module} {" ".join(full_paths(self.tf_var_files))}') raise diff --git a/tests/fast/stages/s0_bootstrap/iam_by_principals.tfvars b/tests/fast/stages/s0_bootstrap/iam_by_principals.tfvars new file mode 100644 index 0000000000..4ceaffb6ac --- /dev/null +++ b/tests/fast/stages/s0_bootstrap/iam_by_principals.tfvars @@ -0,0 +1,20 @@ +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +billing_account = { + id = "000000-111111-222222" +} +essential_contacts = "gcp-organization-admins@fast.example.com" +iam_by_principals = { + "user:other@fast.example.com" = ["roles/browser"] +} +prefix = "fast" +org_policies_config = { + import_defaults = false +} +outputs_location = "/fast-config" +groups = { + gcp-support = "group:gcp-support@example.com" +} diff --git a/tests/fast/stages/s0_bootstrap/iam_by_principals.yaml b/tests/fast/stages/s0_bootstrap/iam_by_principals.yaml new file mode 100644 index 0000000000..83b965d302 --- /dev/null +++ b/tests/fast/stages/s0_bootstrap/iam_by_principals.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 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.organization.google_organization_iam_binding.authoritative["roles/browser"]: + condition: [] + members: + - domain:fast.example.com + - user:other@fast.example.com + org_id: '123456789012' + role: roles/browser diff --git a/tests/fast/stages/s0_bootstrap/tftest.yaml b/tests/fast/stages/s0_bootstrap/tftest.yaml index 8f415a219a..2643714eb9 100644 --- a/tests/fast/stages/s0_bootstrap/tftest.yaml +++ b/tests/fast/stages/s0_bootstrap/tftest.yaml @@ -25,3 +25,5 @@ tests: - simple.yaml - simple_projects.yaml - simple_sas.yaml + + iam_by_principals: From 604920dec9ceb39e20b9768f482807f2675092ff Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 13 May 2024 09:24:17 +0200 Subject: [PATCH 25/29] add logging settings to folder module (#2268) --- modules/folder/README.md | 18 +++--- modules/folder/logging.tf | 7 ++ modules/folder/variables-logging.tf | 78 +++++++++++++++++++++++ modules/folder/variables.tf | 53 --------------- modules/organization/README.md | 15 +++-- modules/organization/variables-logging.tf | 78 +++++++++++++++++++++++ modules/organization/variables.tf | 63 ------------------ 7 files changed, 181 insertions(+), 131 deletions(-) create mode 100644 modules/folder/variables-logging.tf create mode 100644 modules/organization/variables-logging.tf diff --git a/modules/folder/README.md b/modules/folder/README.md index 2efb626b97..ff242aeec0 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -345,12 +345,13 @@ module "folder" { | name | description | resources | |---|---|---| | [iam.tf](./iam.tf) | IAM bindings. | google_folder_iam_binding · google_folder_iam_member | -| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_folder_iam_audit_config · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_folder_iam_audit_config · google_logging_folder_exclusion · google_logging_folder_settings · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_compute_firewall_policy_association · google_essential_contacts_contact · google_folder | | [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables-iam.tf](./variables-iam.tf) | None | | +| [variables-logging.tf](./variables-logging.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -367,13 +368,14 @@ module "folder" { | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [id](variables.tf#L48) | Folder ID in case you use folder_create=false. | string | | null | -| [logging_data_access](variables.tf#L54) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L69) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L76) | Logging sinks to create for the folder. | map(object({…})) | | {} | -| [name](variables.tf#L107) | Folder name. | string | | null | -| [org_policies](variables.tf#L113) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [parent](variables.tf#L140) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L150) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables-logging.tf#L32) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_settings](variables-logging.tf#L39) | Default settings for logging resources. | object({…}) | | null | +| [logging_sinks](variables-logging.tf#L49) | Logging sinks to create for the folder. | map(object({…})) | | {} | +| [name](variables.tf#L54) | Folder name. | string | | null | +| [org_policies](variables.tf#L60) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L87) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L97) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/logging.tf b/modules/folder/logging.tf index 817c4d1e06..bb42983c22 100644 --- a/modules/folder/logging.tf +++ b/modules/folder/logging.tf @@ -35,6 +35,13 @@ locals { } } +resource "google_logging_folder_settings" "default" { + count = var.logging_settings != null ? 1 : 0 + folder = local.folder_id + disable_default_sink = var.logging_settings.disable_default_sink + storage_location = var.logging_settings.storage_location +} + resource "google_folder_iam_audit_config" "default" { for_each = var.logging_data_access folder = local.folder_id diff --git a/modules/folder/variables-logging.tf b/modules/folder/variables-logging.tf new file mode 100644 index 0000000000..89685a6de9 --- /dev/null +++ b/modules/folder/variables-logging.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2024 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. + */ + +variable "logging_data_access" { + description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services." + type = map(map(list(string))) + nullable = false + default = {} + validation { + condition = alltrue(flatten([ + for k, v in var.logging_data_access : [ + for kk, vv in v : contains(["DATA_READ", "DATA_WRITE", "ADMIN_READ"], kk) + ] + ])) + error_message = "Log type keys for each service can only be one of 'DATA_READ', 'DATA_WRITE', 'ADMIN_READ'." + } +} + +variable "logging_exclusions" { + description = "Logging exclusions for this folder in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_settings" { + description = "Default settings for logging resources." + type = object({ + # TODO: add support for CMEK + disable_default_sink = optional(bool) + storage_location = optional(string) + }) + default = null +} + +variable "logging_sinks" { + description = "Logging sinks to create for the folder." + type = map(object({ + bq_partitioned_table = optional(bool, false) + description = optional(string) + destination = string + disabled = optional(bool, false) + exclusions = optional(map(string), {}) + filter = optional(string) + iam = optional(bool, true) + include_children = optional(bool, true) + type = string + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + contains(["bigquery", "logging", "project", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'project', 'pubsub', 'storage'." + } + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + v.bq_partitioned_table != true || v.type == "bigquery" + ]) + error_message = "Can only set bq_partitioned_table when type is `bigquery`." + } +} diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 6da2685a71..e5f6d548be 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -51,59 +51,6 @@ variable "id" { default = null } -variable "logging_data_access" { - description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services." - type = map(map(list(string))) - nullable = false - default = {} - validation { - condition = alltrue(flatten([ - for k, v in var.logging_data_access : [ - for kk, vv in v : contains(["DATA_READ", "DATA_WRITE", "ADMIN_READ"], kk) - ] - ])) - error_message = "Log type keys for each service can only be one of 'DATA_READ', 'DATA_WRITE', 'ADMIN_READ'." - } -} - -variable "logging_exclusions" { - description = "Logging exclusions for this folder in the form {NAME -> FILTER}." - type = map(string) - default = {} - nullable = false -} - -variable "logging_sinks" { - description = "Logging sinks to create for the folder." - type = map(object({ - bq_partitioned_table = optional(bool, false) - description = optional(string) - destination = string - disabled = optional(bool, false) - exclusions = optional(map(string), {}) - filter = optional(string) - iam = optional(bool, true) - include_children = optional(bool, true) - type = string - })) - default = {} - nullable = false - validation { - condition = alltrue([ - for k, v in var.logging_sinks : - contains(["bigquery", "logging", "project", "pubsub", "storage"], v.type) - ]) - error_message = "Type must be one of 'bigquery', 'logging', 'project', 'pubsub', 'storage'." - } - validation { - condition = alltrue([ - for k, v in var.logging_sinks : - v.bq_partitioned_table != true || v.type == "bigquery" - ]) - error_message = "Can only set bq_partitioned_table when type is `bigquery`." - } -} - variable "name" { description = "Folder name." type = string diff --git a/modules/organization/README.md b/modules/organization/README.md index cc602a6f12..1b004aa623 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -500,6 +500,7 @@ module "org" { | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | | [variables-iam.tf](./variables-iam.tf) | None | | +| [variables-logging.tf](./variables-logging.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -508,7 +509,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L155) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L92) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | @@ -517,13 +518,13 @@ module "org" { | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | -| [logging_data_access](variables.tf#L51) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L66) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_settings](variables.tf#L73) | Default settings for logging resources. | object({…}) | | null | -| [logging_sinks](variables.tf#L83) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables-logging.tf#L32) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_settings](variables-logging.tf#L39) | Default settings for logging resources. | object({…}) | | null | +| [logging_sinks](variables-logging.tf#L49) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L114) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L141) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L51) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L78) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L45) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L52) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | diff --git a/modules/organization/variables-logging.tf b/modules/organization/variables-logging.tf new file mode 100644 index 0000000000..210352f081 --- /dev/null +++ b/modules/organization/variables-logging.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2024 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. + */ + +variable "logging_data_access" { + description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services." + type = map(map(list(string))) + nullable = false + default = {} + validation { + condition = alltrue(flatten([ + for k, v in var.logging_data_access : [ + for kk, vv in v : contains(["DATA_READ", "DATA_WRITE", "ADMIN_READ"], kk) + ] + ])) + error_message = "Log type keys for each service can only be one of 'DATA_READ', 'DATA_WRITE', 'ADMIN_READ'." + } +} + +variable "logging_exclusions" { + description = "Logging exclusions for this organization in the form {NAME -> FILTER}." + type = map(string) + default = {} + nullable = false +} + +variable "logging_settings" { + description = "Default settings for logging resources." + type = object({ + # TODO: add support for CMEK + disable_default_sink = optional(bool) + storage_location = optional(string) + }) + default = null +} + +variable "logging_sinks" { + description = "Logging sinks to create for the organization." + type = map(object({ + bq_partitioned_table = optional(bool, false) + description = optional(string) + destination = string + disabled = optional(bool, false) + exclusions = optional(map(string), {}) + filter = optional(string) + iam = optional(bool, true) + include_children = optional(bool, true) + type = string + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + contains(["bigquery", "logging", "project", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'project', 'pubsub', 'storage'." + } + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + v.bq_partitioned_table != true || v.type == "bigquery" + ]) + error_message = "Can only set bq_partitioned_table when type is `bigquery`." + } +} diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 4464558e69..146cf9b14a 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -48,69 +48,6 @@ variable "firewall_policy" { default = null } -variable "logging_data_access" { - description = "Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services." - type = map(map(list(string))) - nullable = false - default = {} - validation { - condition = alltrue(flatten([ - for k, v in var.logging_data_access : [ - for kk, vv in v : contains(["DATA_READ", "DATA_WRITE", "ADMIN_READ"], kk) - ] - ])) - error_message = "Log type keys for each service can only be one of 'DATA_READ', 'DATA_WRITE', 'ADMIN_READ'." - } -} - -variable "logging_exclusions" { - description = "Logging exclusions for this organization in the form {NAME -> FILTER}." - type = map(string) - default = {} - nullable = false -} - -variable "logging_settings" { - description = "Default settings for logging resources." - type = object({ - # TODO: add support for CMEK - disable_default_sink = optional(bool) - storage_location = optional(string) - }) - default = null -} - -variable "logging_sinks" { - description = "Logging sinks to create for the organization." - type = map(object({ - bq_partitioned_table = optional(bool, false) - description = optional(string) - destination = string - disabled = optional(bool, false) - exclusions = optional(map(string), {}) - filter = optional(string) - iam = optional(bool, true) - include_children = optional(bool, true) - type = string - })) - default = {} - nullable = false - validation { - condition = alltrue([ - for k, v in var.logging_sinks : - contains(["bigquery", "logging", "project", "pubsub", "storage"], v.type) - ]) - error_message = "Type must be one of 'bigquery', 'logging', 'project', 'pubsub', 'storage'." - } - validation { - condition = alltrue([ - for k, v in var.logging_sinks : - v.bq_partitioned_table != true || v.type == "bigquery" - ]) - error_message = "Can only set bq_partitioned_table when type is `bigquery`." - } -} - variable "org_policies" { description = "Organization policies applied to this organization keyed by policy name." type = map(object({ From e4941c27f2afa053a8dd8a839b026963858a1642 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 13 May 2024 20:18:51 +0200 Subject: [PATCH 26/29] Implement the full IAM interface for tags (#2269) * IAM authoritative bindings in org module * remove extra newline * organization module * project module * tfdoc --- modules/kms/README.md | 6 +- modules/kms/variables.tf | 1 - modules/organization/README.md | 47 ++++- modules/organization/tags.tf | 166 ++++++++++++++---- modules/organization/variables-tags.tf | 80 ++++++++- modules/project/README.md | 8 +- modules/project/tags.tf | 166 ++++++++++++++---- modules/project/variables-tags.tf | 83 ++++++++- tests/modules/organization/examples/tags.yaml | 27 ++- 9 files changed, 492 insertions(+), 92 deletions(-) diff --git a/modules/kms/README.md b/modules/kms/README.md index 4782d8240d..90621bef37 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -120,14 +120,14 @@ module "kms" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [keyring](variables.tf#L64) | Keyring attributes. | object({…}) | ✓ | | -| [project_id](variables.tf#L115) | Project id where the keyring will be created. | string | ✓ | | +| [project_id](variables.tf#L114) | Project id where the keyring will be created. | string | ✓ | | | [iam](variables.tf#L17) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [import_job](variables.tf#L54) | Keyring import job attributes. | object({…}) | | null | | [keyring_create](variables.tf#L72) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | -| [keys](variables.tf#L78) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L120) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | +| [keys](variables.tf#L78) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L119) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf index 2708a7f7cf..d0d335674b 100644 --- a/modules/kms/variables.tf +++ b/modules/kms/variables.tf @@ -87,7 +87,6 @@ variable "keys" { algorithm = string protection_level = optional(string, "SOFTWARE") })) - iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ members = list(string) diff --git a/modules/organization/README.md b/modules/organization/README.md index 1b004aa623..f0253d11b3 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -440,12 +440,44 @@ module "org" { iam = { "roles/resourcemanager.tagAdmin" = ["group:${var.group_email}"] } + iam_bindings = { + viewer = { + role = "roles/resourcemanager.tagViewer" + members = ["group:gcp-support@example.org"] + } + } + iam_bindings_additive = { + user_app1 = { + role = "roles/resourcemanager.tagUser" + member = "group:app1-team@example.org" + } + } values = { - dev = {} + dev = { + iam_bindings_additive = { + user_app2 = { + role = "roles/resourcemanager.tagUser" + member = "group:app2-team@example.org" + } + } + } prod = { description = "Environment: production." iam = { - "roles/resourcemanager.tagViewer" = ["group:${var.group_email}"] + "roles/resourcemanager.tagViewer" = ["group:app1-team@example.org"] + } + iam_bindings = { + admin = { + role = "roles/resourcemanager.tagAdmin" + members = ["group:gcp-support@example.org"] + condition = { + title = "gcp_support" + expression = <<-END + request.time.getHours("Europe/Berlin") <= 9 && + request.time.getHours("Europe/Berlin") >= 17 + END + } + } } } } @@ -455,8 +487,9 @@ module "org" { env-prod = module.org.tag_values["environment/prod"].id } } -# tftest modules=1 resources=6 inventory=tags.yaml e2e serial +# tftest modules=1 resources=10 inventory=tags.yaml ``` + You can also define network tags, through a dedicated variable *network_tags*: @@ -498,7 +531,7 @@ module "org" { | [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint | | [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | -| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-logging.tf](./variables-logging.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | @@ -522,11 +555,11 @@ module "org" { | [logging_exclusions](variables-logging.tf#L32) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L39) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L49) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [org_policies](variables.tf#L51) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | | [org_policy_custom_constraints](variables.tf#L78) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | -| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | -| [tags](variables-tags.tf#L52) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [tag_bindings](variables-tags.tf#L81) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | +| [tags](variables-tags.tf#L88) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | ## Outputs diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index d25757c243..0321818a5a 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -15,50 +15,96 @@ */ locals { - _tag_values = flatten([ - for tag, attrs in local.tags : [ - for value, value_attrs in attrs.values : { - description = value_attrs.description, - key = "${tag}/${value}" - id = try(value_attrs.id, null) - name = value - roles = keys(value_attrs.iam) - tag = tag - tag_id = attrs.id - tag_network = try(attrs.network, null) != null + _tag_iam = flatten([ + for k, v in local.tags : [ + for role in keys(v.iam) : { + # we cycle on keys here so we don't risk injecting dynamic values + role = role + tag = k + tag_id = v.id } ] ]) - _tag_values_iam = flatten([ - for key, value_attrs in local.tag_values : [ - for role in value_attrs.roles : { - id = value_attrs.id - key = value_attrs.key - name = value_attrs.name + _tag_value_iam = flatten([ + for k, v in local.tag_values : [ + for role in v.roles : { + id = v.id + key = v.key + name = v.name role = role - tag = value_attrs.tag + tag = v.tag } ] ]) - _tags_iam = flatten([ - for tag, attrs in local.tags : [ - for role in keys(attrs.iam) : { - role = role - tag = tag - tag_id = attrs.id + _tag_values = flatten([ + for k, v in local.tags : [ + for vk, vv in v.values : { + description = vv.description, + key = "${k}/${vk}" + iam_bindings = keys(vv.iam_bindings) + iam_bindings_additive = keys(vv.iam_bindings_additive) + id = try(vv.id, null) + name = vk + # we only store keys here so we don't risk injecting dynamic values + roles = keys(vv.iam) + tag = k + tag_id = v.id + tag_network = try(v.network, null) != null } ] ]) - tag_values = { - for t in local._tag_values : t.key => t + tag_iam = { + for t in local._tag_iam : "${t.tag}:${t.role}" => t + } + tag_iam_bindings = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_iam_bindings_additive = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings_additive) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_value_iam = { + for v in local._tag_value_iam : "${v.key}:${v.role}" => v } - tag_values_iam = { - for t in local._tag_values_iam : "${t.key}:${t.role}" => t + tag_value_iam_bindings = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_value_iam_bindings_additive = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings_additive : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_values = { + for v in local._tag_values : v.key => v } tags = merge(var.tags, var.network_tags) - tags_iam = { - for t in local._tags_iam : "${t.tag}:${t.role}" => t - } } # keys @@ -82,7 +128,7 @@ resource "google_tags_tag_key" "default" { } resource "google_tags_tag_key_iam_binding" "default" { - for_each = local.tags_iam + for_each = local.tag_iam tag_key = ( each.value.tag_id == null ? google_tags_tag_key.default[each.value.tag].id @@ -94,6 +140,30 @@ resource "google_tags_tag_key_iam_binding" "default" { ) } +resource "google_tags_tag_key_iam_binding" "bindings" { + for_each = local.tag_iam_bindings + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role + members = ( + local.tags[each.value.tag]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_key_iam_member" "bindings" { + for_each = local.tag_iam_bindings_additive + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member +} + # values resource "google_tags_tag_value" "default" { @@ -108,7 +178,7 @@ resource "google_tags_tag_value" "default" { } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam + for_each = local.tag_value_iam tag_value = ( each.value.id == null ? google_tags_tag_value.default[each.value.key].id @@ -121,6 +191,36 @@ resource "google_tags_tag_value_iam_binding" "default" { ) } +resource "google_tags_tag_value_iam_binding" "bindings" { + for_each = local.tag_value_iam_bindings + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role + ) + members = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_value_iam_member" "bindings" { + for_each = local.tag_value_iam_bindings_additive + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role + ) + member = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member + ) +} + # bindings resource "google_tags_tag_binding" "binding" { diff --git a/modules/organization/variables-tags.tf b/modules/organization/variables-tags.tf index 4688504d57..21a0efe6f4 100644 --- a/modules/organization/variables-tags.tf +++ b/modules/organization/variables-tags.tf @@ -19,11 +19,47 @@ variable "network_tags" { type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) - network = string # project_id/vpc_name + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) + network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })), {}) })) nullable = false @@ -54,11 +90,47 @@ variable "tags" { type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/modules/project/README.md b/modules/project/README.md index 75388aefc3..cd92870a47 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -1173,7 +1173,7 @@ module "bucket" { | [quotas.tf](./quotas.tf) | None | google_cloud_quotas_quota_preference | | [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | | [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_compute_subnetwork_iam_member · google_project_iam_member | -| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | +| [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-quotas.tf](./variables-quotas.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | @@ -1204,7 +1204,7 @@ module "bucket" { | [logging_exclusions](variables.tf#L108) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L115) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L146) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [org_policies](variables.tf#L158) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | | [parent](variables.tf#L185) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | | [prefix](variables.tf#L195) | Optional prefix used to generate project id and name. | string | | null | @@ -1216,8 +1216,8 @@ module "bucket" { | [shared_vpc_host_config](variables.tf#L235) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | | [shared_vpc_service_config](variables.tf#L244) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | | [skip_delete](variables.tf#L272) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables-tags.tf#L45) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | -| [tags](variables-tags.tf#L51) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [tag_bindings](variables-tags.tf#L81) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [tags](variables-tags.tf#L88) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [vpc_sc](variables.tf#L278) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs diff --git a/modules/project/tags.tf b/modules/project/tags.tf index 4f2a666459..1f9ff378b5 100644 --- a/modules/project/tags.tf +++ b/modules/project/tags.tf @@ -15,50 +15,96 @@ */ locals { - _tag_values = flatten([ - for tag, attrs in local.tags : [ - for value, value_attrs in attrs.values : { - description = value_attrs.description, - key = "${tag}/${value}" - id = try(value_attrs.id, null) - name = value - roles = keys(value_attrs.iam) - tag = tag - tag_id = attrs.id - tag_network = try(attrs.network, null) != null + _tag_iam = flatten([ + for k, v in local.tags : [ + for role in keys(v.iam) : { + # we cycle on keys here so we don't risk injecting dynamic values + role = role + tag = k + tag_id = v.id } ] ]) - _tag_values_iam = flatten([ - for key, value_attrs in local.tag_values : [ - for role in value_attrs.roles : { - id = value_attrs.id - key = value_attrs.key - name = value_attrs.name + _tag_value_iam = flatten([ + for k, v in local.tag_values : [ + for role in v.roles : { + id = v.id + key = v.key + name = v.name role = role - tag = value_attrs.tag + tag = v.tag } ] ]) - _tags_iam = flatten([ - for tag, attrs in local.tags : [ - for role in keys(attrs.iam) : { - role = role - tag = tag - tag_id = attrs.id + _tag_values = flatten([ + for k, v in local.tags : [ + for vk, vv in v.values : { + description = vv.description, + key = "${k}/${vk}" + iam_bindings = keys(vv.iam_bindings) + iam_bindings_additive = keys(vv.iam_bindings_additive) + id = try(vv.id, null) + name = vk + # we only store keys here so we don't risk injecting dynamic values + roles = keys(vv.iam) + tag = k + tag_id = v.id + tag_network = try(v.network, null) != null } ] ]) - tag_values = { - for t in local._tag_values : t.key => t + tag_iam = { + for t in local._tag_iam : "${t.tag}:${t.role}" => t + } + tag_iam_bindings = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_iam_bindings_additive = merge([ + for k, v in local.tags : { + for bk in keys(v.iam_bindings_additive) : "${k}:${bk}" => { + binding = bk + tag = k + tag_id = v.id + } + } + ]...) + tag_value_iam = { + for v in local._tag_value_iam : "${v.key}:${v.role}" => v } - tag_values_iam = { - for t in local._tag_values_iam : "${t.key}:${t.role}" => t + tag_value_iam_bindings = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_value_iam_bindings_additive = merge([ + for k, v in local.tag_values : { + for bk in v.iam_bindings_additive : "${k}:${bk}" => { + binding = bk + id = v.id + key = k + name = v.name + tag = v.tag + tag_id = v.id + } + } + ]...) + tag_values = { + for v in local._tag_values : v.key => v } tags = merge(var.tags, var.network_tags) - tags_iam = { - for t in local._tags_iam : "${t.tag}:${t.role}" => t - } } # keys @@ -82,7 +128,7 @@ resource "google_tags_tag_key" "default" { } resource "google_tags_tag_key_iam_binding" "default" { - for_each = local.tags_iam + for_each = local.tag_iam tag_key = ( each.value.tag_id == null ? google_tags_tag_key.default[each.value.tag].id @@ -94,6 +140,30 @@ resource "google_tags_tag_key_iam_binding" "default" { ) } +resource "google_tags_tag_key_iam_binding" "bindings" { + for_each = local.tag_iam_bindings + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings"][each.value.binding].role + members = ( + local.tags[each.value.tag]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_key_iam_member" "bindings" { + for_each = local.tag_iam_bindings_additive + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].role + member = local.tags[each.value.tag]["iam_bindings_additive"][each.value.binding].member +} + # values resource "google_tags_tag_value" "default" { @@ -108,7 +178,7 @@ resource "google_tags_tag_value" "default" { } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam + for_each = local.tag_value_iam tag_value = ( each.value.id == null ? google_tags_tag_value.default[each.value.key].id @@ -121,6 +191,36 @@ resource "google_tags_tag_value_iam_binding" "default" { ) } +resource "google_tags_tag_value_iam_binding" "bindings" { + for_each = local.tag_value_iam_bindings + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].role + ) + members = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings"][each.value.binding].members + ) +} + +resource "google_tags_tag_value_iam_member" "bindings" { + for_each = local.tag_value_iam_bindings_additive + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].role + ) + member = ( + local.tags[each.value.tag]["values"][each.value.name]["iam_bindings_additive"][each.value.binding].member + ) +} + # bindings resource "google_tags_tag_binding" "binding" { diff --git a/modules/project/variables-tags.tf b/modules/project/variables-tags.tf index 8914aae6e6..ac73f03fd8 100644 --- a/modules/project/variables-tags.tf +++ b/modules/project/variables-tags.tf @@ -19,11 +19,47 @@ variable "network_tags" { type = map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) - network = string # project_id/vpc_name + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) + network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })), {}) })) nullable = false @@ -45,7 +81,8 @@ variable "network_tags" { variable "tag_bindings" { description = "Tag bindings for this project, in key => tag value id format." type = map(string) - default = null + # we need default null here for the project factory module + default = null } variable "tags" { @@ -53,11 +90,47 @@ variable "tags" { type = map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform project module.") iam = optional(map(list(string)), {}) - id = optional(string) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/tests/modules/organization/examples/tags.yaml b/tests/modules/organization/examples/tags.yaml index bed4b46188..cce3674346 100644 --- a/tests/modules/organization/examples/tags.yaml +++ b/tests/modules/organization/examples/tags.yaml @@ -23,11 +23,20 @@ values: purpose_data: null short_name: environment timeouts: null + module.org.google_tags_tag_key_iam_binding.bindings["environment:viewer"]: + condition: [] + members: + - group:gcp-support@example.org + role: roles/resourcemanager.tagViewer module.org.google_tags_tag_key_iam_binding.default["environment:roles/resourcemanager.tagAdmin"]: condition: [] members: - group:organization-admins@example.org role: roles/resourcemanager.tagAdmin + module.org.google_tags_tag_key_iam_member.bindings["environment:user_app1"]: + condition: [] + member: group:app1-team@example.org + role: roles/resourcemanager.tagUser module.org.google_tags_tag_value.default["environment/dev"]: description: Managed by the Terraform organization module. short_name: dev @@ -36,14 +45,28 @@ values: description: 'Environment: production.' short_name: prod timeouts: null + module.org.google_tags_tag_value_iam_binding.bindings["environment/prod:admin"]: + condition: [] + members: + - group:gcp-support@example.org + role: roles/resourcemanager.tagAdmin module.org.google_tags_tag_value_iam_binding.default["environment/prod:roles/resourcemanager.tagViewer"]: condition: [] members: - - group:organization-admins@example.org + - group:app1-team@example.org role: roles/resourcemanager.tagViewer + module.org.google_tags_tag_value_iam_member.bindings["environment/dev:user_app2"]: + condition: [] + member: group:app2-team@example.org + role: roles/resourcemanager.tagUser counts: google_tags_tag_binding: 1 google_tags_tag_key: 1 - google_tags_tag_key_iam_binding: 1 + google_tags_tag_key_iam_binding: 2 + google_tags_tag_key_iam_member: 1 google_tags_tag_value: 2 + google_tags_tag_value_iam_binding: 2 + google_tags_tag_value_iam_member: 1 + modules: 1 + resources: 10 From 4d1d3c6811228fcfc59b5b80d9c6d988494e55e1 Mon Sep 17 00:00:00 2001 From: simonebruzzechesse <60114646+simonebruzzechesse@users.noreply.github.com> Date: Tue, 14 May 2024 14:45:39 +0200 Subject: [PATCH 27/29] New Bindplane cloud-config-container setup (#2272) * new bindplane cloud-config-container setup --- modules/cloud-config-container/README.md | 1 + .../bindplane/README.md | 95 +++++++++++++ .../bindplane/cloud-config.yaml | 128 ++++++++++++++++++ .../bindplane/images/login.png | Bin 0 -> 158968 bytes .../cloud-config-container/bindplane/main.tf | 48 +++++++ .../bindplane/outputs.tf | 20 +++ .../bindplane/variables.tf | 88 ++++++++++++ .../bindplane/versions.tf | 27 ++++ 8 files changed, 407 insertions(+) create mode 100644 modules/cloud-config-container/bindplane/README.md create mode 100644 modules/cloud-config-container/bindplane/cloud-config.yaml create mode 100644 modules/cloud-config-container/bindplane/images/login.png create mode 100644 modules/cloud-config-container/bindplane/main.tf create mode 100644 modules/cloud-config-container/bindplane/outputs.tf create mode 100644 modules/cloud-config-container/bindplane/variables.tf create mode 100644 modules/cloud-config-container/bindplane/versions.tf diff --git a/modules/cloud-config-container/README.md b/modules/cloud-config-container/README.md index d7017dcb77..b41490040b 100644 --- a/modules/cloud-config-container/README.md +++ b/modules/cloud-config-container/README.md @@ -11,6 +11,7 @@ These modules are designed for several use cases: ## Available modules +- [Bindplane](./bindplane) - [CoreDNS](./coredns) - [MySQL](./mysql) - [Nginx](./nginx) diff --git a/modules/cloud-config-container/bindplane/README.md b/modules/cloud-config-container/bindplane/README.md new file mode 100644 index 0000000000..035722521e --- /dev/null +++ b/modules/cloud-config-container/bindplane/README.md @@ -0,0 +1,95 @@ +# Containerized Bindplane on Container Optimized OS + +This module manages a `cloud-config` configuration that starts a containerized [Bindplane](https://observiq.com/solutions) service on Container Optimized OS, using the official [Bindplane EE image](https://hub.docker.com/r/observiq/bindplane-ee) provided by observIQ and documented in the [official documentation page](https://observiq.com/download) when selecting "Docker" as target platform. + +The resulting `cloud-config` can be customized in a number of ways: + +- a custom Bindplane configuration can be set in Docker compose file available in `/run/bindplane/docker-compose.yml` using the `bindplane_config` variable +- additional files can be passed in via the `files` variable +- a completely custom `cloud-config` can be passed in via the `cloud_config` variable, and additional template variables can be passed in via `config_variables` + +The default instance configuration inserts iptables rules to allow traffic on port 3001. + +Logging and monitoring are enabled via the [Google Cloud Logging agent](https://cloud.google.com/container-optimized-os/docs/how-to/logging) configured for the instance via the `google-logging-enabled` metadata property, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot. + +The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata. + +## Setup + +Please refer to the examples below for a sample terraform code for deploying a Bindplane server on GCP Compute VM with COS. After setting up the terraform code run the following commands: + +```bash +terraform init +terraform apply +``` + +Wait for a couple of minutes for the VM to be bootstrapped then connect via IAP tunnel using the following command (substitute the VM name, project and zone according to your configuration): + +```bash +gcloud compute ssh $VM_NAME --project $PROJECT --zone $ZONE -- -L 3001:127.0.0.1:3001 -N -q -f +``` + +Navigate to http://localhost:3001 to access the Bindplane console, the following login page should be displayed. + +

+ Bindplane Login page +

+ +## Examples + +### Default configuration + +This example will create a `cloud-config` that uses the module's defaults, creating a simple bindplane server with default (latest) docker image versions and setting localhost as remote url (suited only for local development). + +```hcl +module "cos-nginx" { + source = "./fabric/modules/cloud-config-container/bindplane" + password = "secret" +} + +module "vm-nginx-tls" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-nginx" + network_interfaces = [{ + network = "default" + subnetwork = "gce" + }] + metadata = { + user-data = module.cos-nginx.cloud_config + google-logging-enabled = true + } + boot_disk = { + initialize_params = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + } + tags = ["http-server", "ssh"] +} +# tftest modules=2 resources=2 +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [password](variables.tf#L63) | Default admin user password. | string | ✓ | | +| [bindplane_config](variables.tf#L17) | Bindplane configurations. | object({…}) | | {} | +| [cloud_config](variables.tf#L29) | Cloud config template path. If null default will be used. | string | | null | +| [config_variables](variables.tf#L35) | Additional variables used to render the cloud-config and Nginx templates. | map(any) | | {} | +| [file_defaults](variables.tf#L41) | Default owner and permissions for files. | object({…}) | | {…} | +| [files](variables.tf#L53) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | +| [runcmd_post](variables.tf#L68) | Extra commands to run after starting nginx. | list(string) | | [] | +| [runcmd_pre](variables.tf#L74) | Extra commands to run before starting nginx. | list(string) | | [] | +| [users](variables.tf#L80) | List of additional usernames to be created. | list(object({…})) | | […] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | + diff --git a/modules/cloud-config-container/bindplane/cloud-config.yaml b/modules/cloud-config-container/bindplane/cloud-config.yaml new file mode 100644 index 0000000000..78f6ea6ec5 --- /dev/null +++ b/modules/cloud-config-container/bindplane/cloud-config.yaml @@ -0,0 +1,128 @@ +#cloud-config + +# Copyright 2024 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 +# +# https://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. + +# https://hub.docker.com/r/nginx/nginx/ +# https://nginx.io/manual/toc/#installation + +users: + - name: bindplane + uid: 2000 + %{ for user in users } + - name: ${user.username} + uid: ${user.uid} + %{ endfor } + +write_files: + - path: /var/lib/docker/daemon.json + permissions: 0644 + owner: root + content: | + { + "live-restore": true, + "storage-driver": "overlay2", + "log-opts": { + "max-size": "1024m" + } + } + + - path: /run/bindplane/docker-compose.yml + permissions: 0644 + owner: root + content: | + version: "3" + + volumes: + bindplane: + prometheus: + + services: + prometheus: + container_name: bindplane-prometheus + restart: always + image: ${bindplane_prometheus_image} + ports: + - "9090:9090" + volumes: + - prometheus:/prometheus + + transform: + container_name: bindplane-transform-agent + restart: always + image: ${bindplane_transform_agent_image} + ports: + - "4568:4568" + + bindplane: + container_name: bindplane-server + restart: always + image: ${bindplane_server_image} + ports: + - "3001:3001" + environment: + - BINDPLANE_USERNAME=admin + - BINDPLANE_PASSWORD=${password} + - BINDPLANE_REMOTE_URL=http://${remote_url}:3001 + - BINDPLANE_SESSION_SECRET=${uuid} + - BINDPLANE_LOG_OUTPUT=stdout + - BINDPLANE_ACCEPT_EULA=true + - BINDPLANE_PROMETHEUS_ENABLE=true + - BINDPLANE_PROMETHEUS_ENABLE_REMOTE=true + - BINDPLANE_PROMETHEUS_HOST=prometheus + - BINDPLANE_PROMETHEUS_PORT=9090 + - BINDPLANE_TRANSFORM_AGENT_ENABLE_REMOTE=true + - BINDPLANE_TRANSFORM_AGENT_REMOTE_AGENTS=transform:4568 + volumes: + - bindplane:/data + depends_on: + - prometheus + - transform + + # bindplane container service + - path: /etc/systemd/system/bindplane.service + permissions: 0644 + owner: root + content: | + [Unit] + Description=Start bindplane containers + After=gcr-online.target docker.socket + Wants=gcr-online.target docker.socket docker-events-collector.service + [Service] + Environment="HOME=/home/bindplane" + ExecStartPre=/usr/bin/docker-credential-gcr configure-docker + ExecStart=/usr/bin/docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /var:/var -v /run:/run -w=/var cryptopants/docker-compose-gcr -f /run/bindplane/docker-compose.yml up + ExecStop=/usr/bin/docker rm -f $(docker ps -a -q) + + %{ for path, data in files } + - path: ${path} + owner: ${lookup(data, "owner", "root")} + permissions: ${lookup(data, "permissions", "0644")} + content: | + ${indent(6, data.content)} + %{ endfor } + +bootcmd: + - systemctl start node-problem-detector + +runcmd: +%{ for cmd in runcmd_pre ~} + - ${cmd} +%{ endfor ~} + - iptables -I INPUT 1 -p tcp -m tcp --dport 3001 -m state --state NEW,ESTABLISHED -j ACCEPT + - systemctl daemon-reload + - systemctl start bindplane +%{ for cmd in runcmd_post ~} + - ${cmd} +%{ endfor ~} diff --git a/modules/cloud-config-container/bindplane/images/login.png b/modules/cloud-config-container/bindplane/images/login.png new file mode 100644 index 0000000000000000000000000000000000000000..3de1e6829ab26c97c9bf8214580ddf3b22987bca GIT binary patch literal 158968 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⁣#RRTHPRz~nCL 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

    8%SDM;z--8OkU70)> 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

    TwNM$WulyQU!* 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

    8(O~S(`*carfOz+TKRGXq(K| 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 literal 0 HcmV?d00001 diff --git a/modules/cloud-config-container/bindplane/main.tf b/modules/cloud-config-container/bindplane/main.tf new file mode 100644 index 0000000000..a2a11f8654 --- /dev/null +++ b/modules/cloud-config-container/bindplane/main.tf @@ -0,0 +1,48 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + cloud_config = templatefile(local.template, merge(var.config_variables, { + bindplane_prometheus_image = var.bindplane_config.bindplane_prometheus_image + bindplane_server_image = var.bindplane_config.bindplane_server_image + bindplane_transform_agent_image = var.bindplane_config.bindplane_transform_agent_image + files = local.files + password = var.password + remote_url = var.bindplane_config.remote_url + runcmd_pre = var.runcmd_pre + runcmd_post = var.runcmd_post + users = var.users + uuid = random_uuid.uuid_secret.result + })) + files = { + for path, attrs in var.files : path => { + content = attrs.content, + owner = attrs.owner == null ? var.file_defaults.owner : attrs.owner, + permissions = ( + attrs.permissions == null + ? var.file_defaults.permissions + : attrs.permissions + ) + } + } + template = ( + var.cloud_config == null + ? "${path.module}/cloud-config.yaml" + : var.cloud_config + ) +} + +resource "random_uuid" "uuid_secret" {} diff --git a/modules/cloud-config-container/bindplane/outputs.tf b/modules/cloud-config-container/bindplane/outputs.tf new file mode 100644 index 0000000000..56e0824296 --- /dev/null +++ b/modules/cloud-config-container/bindplane/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2024 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. + */ + +output "cloud_config" { + description = "Rendered cloud-config file to be passed as user-data instance metadata." + value = local.cloud_config +} diff --git a/modules/cloud-config-container/bindplane/variables.tf b/modules/cloud-config-container/bindplane/variables.tf new file mode 100644 index 0000000000..39856f4585 --- /dev/null +++ b/modules/cloud-config-container/bindplane/variables.tf @@ -0,0 +1,88 @@ +/** + * Copyright 2022 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. + */ + +variable "bindplane_config" { + description = "Bindplane configurations." + type = object({ + remote_url = optional(string, "localhost") + bindplane_server_image = optional(string, "us-central1-docker.pkg.dev/observiq-containers/bindplane/bindplane-ee:latest") + bindplane_transform_agent_image = optional(string, "us-central1-docker.pkg.dev/observiq-containers/bindplane/bindplane-transform-agent:latest") + bindplane_prometheus_image = optional(string, "us-central1-docker.pkg.dev/observiq-containers/bindplane/bindplane-prometheus:1.56.0") + }) + default = {} + nullable = false +} + +variable "cloud_config" { + description = "Cloud config template path. If null default will be used." + type = string + default = null +} + +variable "config_variables" { + description = "Additional variables used to render the cloud-config and Nginx templates." + type = map(any) + default = {} +} + +variable "file_defaults" { + description = "Default owner and permissions for files." + type = object({ + owner = string + permissions = string + }) + default = { + owner = "root" + permissions = "0644" + } +} + +variable "files" { + description = "Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null." + type = map(object({ + content = string + owner = string + permissions = string + })) + default = {} +} + +variable "password" { + description = "Default admin user password." + type = string +} + +variable "runcmd_post" { + description = "Extra commands to run after starting nginx." + type = list(string) + default = [] +} + +variable "runcmd_pre" { + description = "Extra commands to run before starting nginx." + type = list(string) + default = [] +} + +variable "users" { + description = "List of additional usernames to be created." + type = list(object({ + username = string, + uid = number, + })) + default = [ + ] +} diff --git a/modules/cloud-config-container/bindplane/versions.tf b/modules/cloud-config-container/bindplane/versions.tf new file mode 100644 index 0000000000..bc9986b3c7 --- /dev/null +++ b/modules/cloud-config-container/bindplane/versions.tf @@ -0,0 +1,27 @@ +# Copyright 2024 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 +# +# https://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. + +terraform { + required_version = ">= 1.7.4" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 5.26.0, < 6.0.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 5.26.0, < 6.0.0" # tftest + } + } +} From a14ed9add2777769d6b1657ff1b6c44637d2e86f Mon Sep 17 00:00:00 2001 From: luigi-bitonti <93377317+luigi-bitonti@users.noreply.github.com> Date: Tue, 14 May 2024 14:56:10 +0200 Subject: [PATCH 28/29] Cloud function CMEK key support (#2270) * Added support to kms key * Updated doc * Fix variable description. * Updated README * Cloud function v2 integration with kms * Fix variables description --------- Co-authored-by: Ludovico Magnocavallo --- modules/cloud-function-v1/README.md | 47 ++++++++++++++++++++------ modules/cloud-function-v1/main.tf | 14 ++++---- modules/cloud-function-v1/variables.tf | 17 ++++++++++ modules/cloud-function-v2/README.md | 23 +++++++------ modules/cloud-function-v2/main.tf | 11 +++--- modules/cloud-function-v2/variables.tf | 6 ++++ 6 files changed, 85 insertions(+), 33 deletions(-) diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 7099946e2d..ebcf0dd0aa 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -16,6 +16,7 @@ The GCS object used for deployment uses a hash of the bundle zip contents in its - [Private Cloud Build Pool](#private-cloud-build-pool) - [Multiple Cloud Functions within project](#multiple-cloud-functions-within-project) - [Mounting secrets from Secret Manager](#mounting-secrets-from-secret-manager) + - [Using CMEK to encrypt function resources](#using-cmek-to-encrypt-function-resources) - [Variables](#variables) - [Outputs](#outputs) @@ -258,6 +259,28 @@ module "cf-http" { } # tftest modules=1 resources=2 inventory=secrets.yaml ``` + +### Using CMEK to encrypt function resources +This encrypt bucket _gcf-sources-*_ with the provided kms key. The repository has to be encrypted with the same kms key. + +```hcl +module "cf-http" { + source = "./fabric/modules/cloud-function-v1" + project_id = "my-project" + region = "europe-west1" + name = "test-cf-http" + bucket_name = "test-cf-bundles" + bundle_config = { + source_dir = "fabric/assets" + output_path = "bundle.zip" + } + kms_key = "projects/my-project/locations/europe-west1/keyRings/mykeyring/cryptoKeys/mykey" + repository_settings = { + repository = "projects/my-project/locations/europe-west1/repositories/myrepo" + } +} +# tftest modules=1 resources=2 +``` ## Variables @@ -265,9 +288,9 @@ module "cf-http" { |---|---|:---:|:---:|:---:| | [bucket_name](variables.tf#L26) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string | ✓ | | | [bundle_config](variables.tf#L44) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…}) | ✓ | | -| [name](variables.tf#L109) | Name used for cloud function and associated resources. | string | ✓ | | -| [project_id](variables.tf#L124) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L129) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L115) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L130) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L135) | Region used for all resources. | string | ✓ | | | [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) | | null | | [build_environment_variables](variables.tf#L32) | A set of key/value environment variable pairs available during build time. | map(string) | | {} | | [build_worker_pool](variables.tf#L38) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | @@ -277,14 +300,16 @@ module "cf-http" { | [https_security_level](variables.tf#L85) | The security level for the function: Allowed values are SECURE_ALWAYS, SECURE_OPTIONAL. | string | | null | | [iam](variables.tf#L91) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [ingress_settings](variables.tf#L97) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | -| [labels](variables.tf#L103) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L114) | Optional prefix used for resource names. | string | | null | -| [secrets](variables.tf#L134) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | -| [service_account](variables.tf#L146) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L152) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L158) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L168) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L178) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [kms_key](variables.tf#L103) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository field that was created with the same KMS crypto key. | string | | null | +| [labels](variables.tf#L109) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L120) | Optional prefix used for resource names. | string | | null | +| [repository_settings](variables.tf#L140) | Docker Registry to use for storing the function's Docker images and specific repository. If kms_key is provided, the repository must have already been encrypted with the key. | object({…}) | | {…} | +| [secrets](variables.tf#L151) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account](variables.tf#L163) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L169) | Auto-create service account. | bool | | false | +| [trigger_config](variables.tf#L175) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L185) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L195) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index b57d8431cb..7a7fdc9152 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -41,6 +41,7 @@ locals { ) } + resource "google_vpc_access_connector" "connector" { count = try(var.vpc_connector.create, false) == false ? 0 : 1 project = var.project_id @@ -67,12 +68,13 @@ resource "google_cloudfunctions_function" "function" { labels = var.labels trigger_http = var.trigger_config == null ? true : null https_trigger_security_level = var.https_security_level == null ? "SECURE_ALWAYS" : var.https_security_level - - ingress_settings = var.ingress_settings - build_worker_pool = var.build_worker_pool - build_environment_variables = var.build_environment_variables - - vpc_connector = local.vpc_connector + ingress_settings = var.ingress_settings + build_worker_pool = var.build_worker_pool + build_environment_variables = var.build_environment_variables + kms_key_name = var.kms_key + docker_registry = try(var.repository_settings.registry, "ARTIFACT_REGISTRY") + docker_repository = try(var.repository_settings.repository, null) + vpc_connector = local.vpc_connector vpc_connector_egress_settings = try( var.vpc_connector.egress_settings, null ) diff --git a/modules/cloud-function-v1/variables.tf b/modules/cloud-function-v1/variables.tf index 73964d0b18..8dc592addd 100644 --- a/modules/cloud-function-v1/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -100,6 +100,12 @@ variable "ingress_settings" { default = null } +variable "kms_key" { + description = "Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository field that was created with the same KMS crypto key." + type = string + default = null +} + variable "labels" { description = "Resource labels." type = map(string) @@ -131,6 +137,17 @@ variable "region" { type = string } +variable "repository_settings" { + description = "Docker Registry to use for storing the function's Docker images and specific repository. If kms_key is provided, the repository must have already been encrypted with the key." + type = object({ + registry = optional(string) + repository = optional(string) + }) + default = { + registry = "ARTIFACT_REGISTRY" + } +} + variable "secrets" { description = "Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format." type = map(object({ diff --git a/modules/cloud-function-v2/README.md b/modules/cloud-function-v2/README.md index a5bad718e0..f54dd50aec 100644 --- a/modules/cloud-function-v2/README.md +++ b/modules/cloud-function-v2/README.md @@ -281,9 +281,9 @@ module "cf-http" { |---|---|:---:|:---:|:---:| | [bucket_name](variables.tf#L26) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string | ✓ | | | [bundle_config](variables.tf#L38) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…}) | ✓ | | -| [name](variables.tf#L103) | Name used for cloud function and associated resources. | string | ✓ | | -| [project_id](variables.tf#L118) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L123) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L109) | Name used for cloud function and associated resources. | string | ✓ | | +| [project_id](variables.tf#L124) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L129) | Region used for all resources. | string | ✓ | | | [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…}) | | null | | [build_worker_pool](variables.tf#L32) | Build worker pool, in projects//locations//workerPools/ format. | string | | null | | [description](variables.tf#L47) | Optional description. | string | | "Terraform managed." | @@ -292,14 +292,15 @@ module "cf-http" { | [function_config](variables.tf#L65) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…}) | | {…} | | [iam](variables.tf#L85) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [ingress_settings](variables.tf#L91) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string | | null | -| [labels](variables.tf#L97) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L108) | Optional prefix used for resource names. | string | | null | -| [secrets](variables.tf#L128) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | -| [service_account](variables.tf#L140) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L146) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L152) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L170) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L180) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [kms_key](variables.tf#L97) | Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository_id field that was created with the same KMS crypto key. | string | | null | +| [labels](variables.tf#L103) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L114) | Optional prefix used for resource names. | string | | null | +| [secrets](variables.tf#L134) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account](variables.tf#L146) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L152) | Auto-create service account. | bool | | false | +| [trigger_config](variables.tf#L158) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L176) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L186) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function-v2/main.tf b/modules/cloud-function-v2/main.tf index c38f863950..24046eada2 100644 --- a/modules/cloud-function-v2/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -59,11 +59,12 @@ resource "google_vpc_access_connector" "connector" { } resource "google_cloudfunctions2_function" "function" { - provider = google-beta - project = var.project_id - location = var.region - name = "${local.prefix}${var.name}" - description = var.description + provider = google-beta + project = var.project_id + location = var.region + name = "${local.prefix}${var.name}" + description = var.description + kms_key_name = var.kms_key build_config { worker_pool = var.build_worker_pool runtime = var.function_config.runtime diff --git a/modules/cloud-function-v2/variables.tf b/modules/cloud-function-v2/variables.tf index af97650fc3..428ba8dba2 100644 --- a/modules/cloud-function-v2/variables.tf +++ b/modules/cloud-function-v2/variables.tf @@ -94,6 +94,12 @@ variable "ingress_settings" { default = null } +variable "kms_key" { + description = "Resource name of a KMS crypto key (managed by the user) used to encrypt/decrypt function resources in key id format. If specified, you must also provide an artifact registry repository using the docker_repository_id field that was created with the same KMS crypto key." + type = string + default = null +} + variable "labels" { description = "Resource labels." type = map(string) From c854057bef191748c6fd66329dc9392c2152405e Mon Sep 17 00:00:00 2001 From: Ludo Date: Tue, 14 May 2024 15:01:43 +0200 Subject: [PATCH 29/29] update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2177cdbc..8a34721182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#2267](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2267)] Fix 0-bootstrap iam_by_principals not taking into account all principals ([wiktorn](https://github.com/wiktorn)) +- [[#2263](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2263)] Update docs - gcp-network-admins -> gcp-vpc-network-admins ([wiktorn](https://github.com/wiktorn)) - [[#2260](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2260)] Remove data source from folder module ([ludoo](https://github.com/ludoo)) - [[#2253](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2253)] Misc FAST fixes ([juliocc](https://github.com/juliocc)) - [[#2235](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2235)] Update FAST logging ([juliocc](https://github.com/juliocc)) @@ -53,6 +55,13 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#2270](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2270)] Cloud function CMEK key support ([luigi-bitonti](https://github.com/luigi-bitonti)) +- [[#2272](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2272)] New Bindplane cloud-config-container setup ([simonebruzzechesse](https://github.com/simonebruzzechesse)) +- [[#2269](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2269)] Implement the full IAM interface for tags ([ludoo](https://github.com/ludoo)) +- [[#2268](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2268)] Add logging settings to folder module ([ludoo](https://github.com/ludoo)) +- [[#2242](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2242)] CloudSQL PSC Endpoints support ([wiktorn](https://github.com/wiktorn)) +- [[#2265](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2265)] Fix failing E2E net-vpc test ([wiktorn](https://github.com/wiktorn)) +- [[#2264](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2264)] Fix bug from output typo in new project-factory module ([JanCVanB](https://github.com/JanCVanB)) - [[#2262](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2262)] Make Simple NVA route IAP traffic through NIC 0 ([juliocc](https://github.com/juliocc)) - [[#2261](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2261)] Add Hybrid NAT support ([juliocc](https://github.com/juliocc)) - [[#2260](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/2260)] Remove data source from folder module ([ludoo](https://github.com/ludoo))