diff --git a/CHANGELOG.md b/CHANGELOG.md index d438ea2a08..1d058a002c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [[#939](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/939)] Temporarily duplicate cloud armor example ([ludoo](https://github.com/ludoo)) ### BLUEPRINTS +- [[#952](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/952)] Remove duplicate GLB+CA blueprint folder ([ludoo](https://github.com/ludoo)) +- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#945](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/945)] Org policy factory ([juliocc](https://github.com/juliocc)) +- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#936](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/936)] Enable org policy service and add README notice to modules ([ludoo](https://github.com/ludoo)) - [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#932](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/932)] feat(project-factory): introduce additive iam bindings to project-fac… ([Malet](https://github.com/Malet)) - [[#925](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/925)] Network dashboard: update main.tf and README following #922 ([brianhmj](https://github.com/brianhmj)) @@ -46,6 +52,9 @@ All notable changes to this project will be documented in this file. ### DOCUMENTATION +- [[#961](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/961)] Remove extra file from root ([ludoo](https://github.com/ludoo)) +- [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos)) +- [[#937](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/937)] Fix typos in blueprints README.md ([kumar-dhanagopal](https://github.com/kumar-dhanagopal)) - [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo)) - [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc)) - [[#878](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/878)] chore: update cft and fabric ([bharathkkb](https://github.com/bharathkkb)) @@ -54,6 +63,12 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#956](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/956)] FAST: bootstrap and extra stage CI/CD improvements and fixes ([ludoo](https://github.com/ludoo)) +- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos)) +- [[#948](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/948)] Use display_name instead of description for FAST service accounts ([juliocc](https://github.com/juliocc)) +- [[#947](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/947)] Use org policy factory for resman stage ([juliocc](https://github.com/juliocc)) +- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#935](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/935)] FAST: enable org policy API, fix run.allowedIngress value ([ludoo](https://github.com/ludoo)) - [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc)) @@ -77,6 +92,16 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#958](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/958)] Add support for org policy custom constraints ([averbuks](https://github.com/averbuks)) +- [[#960](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/960)] Fix README typo in firewall module ([valeriobponza](https://github.com/valeriobponza)) +- [[#953](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/953)] Added IAM Additive and converted some outputs to static ([muresan](https://github.com/muresan)) +- [[#951](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/951)] cloud-functions v2 - fix reference to bucket_name ([wiktorn](https://github.com/wiktorn)) +- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#946](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/946)] **incompatible change:** Deprecate organization-policy module ([juliocc](https://github.com/juliocc)) +- [[#945](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/945)] Org policy factory ([juliocc](https://github.com/juliocc)) +- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#940](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/940)] Ensure the implementation of org policies is consistent ([juliocc](https://github.com/juliocc)) +- [[#936](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/936)] Enable org policy service and add README notice to modules ([ludoo](https://github.com/ludoo)) - [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc)) - [[#926](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/926)] Fix backwards compatibility for vpc subnet descriptions ([ludoo](https://github.com/ludoo)) @@ -119,6 +144,8 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#950](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/950)] Add a pytest fixture to convert tfvars to yaml ([ludoo](https://github.com/ludoo)) +- [[#942](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/942)] Bump tftest and improve dns tests ([juliocc](https://github.com/juliocc)) - [[#919](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/919)] Rename workflow names ([juliocc](https://github.com/juliocc)) - [[#902](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/902)] Bring back sorted variables check ([juliocc](https://github.com/juliocc)) - [[#887](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/887)] Disable parallel execution of tests and plugin cache ([ludoo](https://github.com/ludoo)) diff --git a/README.md b/README.md index 6aa292d76a..ee10367a27 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The current list of modules supports most of the core foundational and networkin Currently available modules: -- **foundational** - [billing budget](./modules/billing-budget), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [organization-policy](./modules/organization-policy), [project](./modules/project), [projects-data-source](./modules/projects-data-source) +- **foundational** - [billing budget](./modules/billing-budget), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [project](./modules/project), [projects-data-source](./modules/projects-data-source) - **networking** - [DNS](./modules/dns), [Cloud Endpoints](./modules/endpoints), [address reservation](./modules/net-address), [NAT](./modules/net-cloudnat), [Global Load Balancer (classic)](./modules/net-glb/), [L4 ILB](./modules/net-ilb), [L7 ILB](./modules/net-ilb-l7), [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [VPN static](./modules/net-vpn-static), [Service Directory](./modules/service-directory) - **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool) - **data** - [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Datafusion](./modules/datafusion), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub) diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index c378723a42..e3c51510e8 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -29,11 +29,11 @@ Clone this repository, then go through the following steps to create resources: Note: Org level viewing permission is required for some metrics such as firewall policies. -Once the resources are deployed, go to the following page to see the dashboard: https://console.cloud.google.com/monitoring/dashboards?project=. +Once the resources are deployed, go to the following page to see the dashboard: https://console.cloud.google.com/monitoring/dashboards?project= (or if populated) A dashboard called "quotas-utilization" should be created. The Cloud Function runs every 10 minutes by default so you should start getting some data points after a few minutes. -You can use the metric explorer to view the data points for the different custom metrics created: https://console.cloud.google.com/monitoring/metrics-explorer?project=. +You can use the metric explorer to view the data points for the different custom metrics created: https://console.cloud.google.com/monitoring/metrics-explorer?project= (or if populated). You can change this frequency by modifying the "schedule_cron" variable in variables.tf. Note that some charts in the dashboard align values over 1h so you might need to wait 1h to see charts on the dashboard views. @@ -70,7 +70,6 @@ Note that metrics are created in the cloud-function/metrics.yaml file. You can a - The CF assumes custom routes importing/exporting is ON, this impacts static and dynamic routes usage calculation - The CF assumes all networks in peering groups have the same global routing and custom routes sharing configuration - ## Next steps and ideas In a future release, we could support: - Google managed VPCs that are peered with PSA (such as Cloud SQL or Memorystore) @@ -88,13 +87,15 @@ If you are interested in this and/or would like to contribute, please contact le |---|---|:---:|:---:|:---:| | [billing_account](variables.tf#L17) | The ID of the billing account to associate this project with | | ✓ | | | [monitored_projects_list](variables.tf#L36) | ID of the projects to be monitored (where limits and quotas data will be pulled) | list(string) | ✓ | | -| [organization_id](variables.tf#L47) | The organization id for the associated services | | ✓ | | -| [prefix](variables.tf#L51) | Customer name to use as prefix for monitoring project | | ✓ | | +| [organization_id](variables.tf#L54) | The organization id for the associated services | | ✓ | | +| [prefix](variables.tf#L58) | Customer name to use as prefix for monitoring project | | ✓ | | | [cf_version](variables.tf#L21) | Cloud Function version 2nd Gen or 1st Gen. Possible options: 'V1' or 'V2'.Use CFv2 if your Cloud Function timeouts after 9 minutes. By default it is using CFv1. | | | V1 | +| [metrics_project_id](variables.tf#L46) | Optional, populate to write metrics and deploy the dashboard in a separated project | | | | | [monitored_folders_list](variables.tf#L30) | ID of the projects to be monitored (where limits and quotas data will be pulled) | list(string) | | [] | -| [monitoring_project_id](variables.tf#L41) | Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string | | | | -| [project_monitoring_services](variables.tf#L55) | Service APIs enabled in the monitoring project if it will be created. | | | […] | -| [region](variables.tf#L75) | Region used to deploy the cloud functions and scheduler | | | europe-west1 | -| [schedule_cron](variables.tf#L80) | Cron format schedule to run the Cloud Function. Default is every 10 minutes. | | | */10 * * * * | +| [monitoring_project_id](variables.tf#L41) | Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string, if metrics_project_id is provided, metrics and dashboard will be deployed there | | | | +| [project_monitoring_services](variables.tf#L63) | Service APIs enabled in the monitoring project if it will be created. | | | […] | +| [region](variables.tf#L88) | Region used to deploy the cloud functions and scheduler | | | europe-west1 | +| [schedule_cron](variables.tf#L93) | Cron format schedule to run the Cloud Function. Default is every 10 minutes. | | | */10 * * * * | +| [vpc_connector_name](variables.tf#L99) | Serverless VPC connection name for the Cloud Function | | | | diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py index f47629fedb..cb16c9c87d 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py @@ -25,7 +25,6 @@ def get_all_subnets(config): ''' Returns a dictionary with subnet level informations (such as IP utilization) - Parameters: config (dict): The dict containing config like clients and limits Returns: @@ -83,19 +82,16 @@ def get_all_subnets(config): return subnet_dict -def compute_subnet_utilization(config, all_subnets_dict): +def compute_subnet_utilization_vms(config, read_mask, all_subnets_dict): ''' - Counts resources (VMs, ILBs, reserved IPs) using private IPs in the different subnets. + Counts VMs using private IPs in the different subnets. Parameters: config (dict): Dict containing config like clients and limits + read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: None ''' - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - response_vm = config["clients"]["asset_client"].search_all_resources( request={ "scope": f"organizations/{config['organization']}", @@ -123,6 +119,17 @@ def compute_subnet_utilization(config, all_subnets_dict): all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ 'used_ip_addresses'] += 1 + +def compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict): + ''' + Counts ILBs using private IPs in the different subnets. + Parameters: + config (dict): Dict containing config like clients and limits + read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory + all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization + Returns: + None + ''' response_ilb = config["clients"]["asset_client"].search_all_resources( request={ "scope": f"organizations/{config['organization']}", @@ -131,7 +138,6 @@ def compute_subnet_utilization(config, all_subnets_dict): "page_size": config["page_size"], }) - # Counting IP addresses for GCE Internal Load Balancers for asset in response_ilb: internal = False psc = False @@ -139,9 +145,11 @@ def compute_subnet_utilization(config, all_subnets_dict): subnet_name = '' subnet_region = '' address = '' + network = '' for versioned in asset.versioned_resources: for field_name, field_value in versioned.resource.items(): - if 'loadBalancingScheme' in field_name and field_value in ['INTERNAL', 'INTERNAL_MANAGED']: + if 'loadBalancingScheme' in field_name and field_value in [ + 'INTERNAL', 'INTERNAL_MANAGED']: internal = True # We want to count only accepted PSC endpoint Forwarding Rule # If the PSC endpoint Forwarding Rule is pending, we will count it in the reserved IP addresses @@ -151,6 +159,7 @@ def compute_subnet_utilization(config, all_subnets_dict): address = field_value elif field_name == 'network': project_id = field_value.split('/')[6] + network = field_value.split('/')[-1] elif 'subnetwork' in field_name: subnet_name = field_value.split('/')[-1] subnet_region = field_value.split('/')[-3] @@ -163,9 +172,21 @@ def compute_subnet_utilization(config, all_subnets_dict): # We need to find the correct subnet with IP address matching ip_address = ipaddress.ip_address(address) for subnet_key, subnet_dict in all_subnets_dict[project_id].items(): - if ip_address in ipaddress.ip_network(subnet_dict['ip_cidr_range']): - all_subnets_dict[project_id][subnet_key]['used_ip_addresses'] += 1 + if subnet_dict["network_name"] == network: + if ip_address in ipaddress.ip_network(subnet_dict['ip_cidr_range']): + all_subnets_dict[project_id][subnet_key]['used_ip_addresses'] += 1 + +def compute_subnet_utilization_addresses(config, read_mask, all_subnets_dict): + ''' + Counts reserved IP addresses in the different subnets. + Parameters: + config (dict): Dict containing config like clients and limits + read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory + all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization + Returns: + None + ''' response_reserved_ips = config["clients"][ "asset_client"].search_all_resources( request={ @@ -185,8 +206,11 @@ def compute_subnet_utilization(config, all_subnets_dict): subnet_region = "" address = "" prefixLength = "" + address_name = "" for versioned in asset.versioned_resources: for field_name, field_value in versioned.resource.items(): + if field_name == 'name': + address_name = field_value if field_name == 'purpose': purpose = field_value elif field_name == 'region': @@ -214,9 +238,90 @@ def compute_subnet_utilization(config, all_subnets_dict): 'used_ip_addresses'] += 1 # PSA Range for Cloud SQL, MemoryStore, etc. elif purpose == "VPC_PEERING": - # TODO: PSA range to be handled later - # print("PSA range to be handled later:", address, prefixLength, network_name) - continue + ip_range = f"{address}/{int(prefixLength)}" + net = ipaddress.ip_network(ip_range) + # Note that 4 IP addresses are reserved by GCP in all subnets + # Source: https://cloud.google.com/vpc/docs/subnets#reserved_ip_addresses_in_every_subnet + total_ip_addresses = int(net.num_addresses) - 4 + all_subnets_dict[project_id][f"psa/{address_name}"] = { + 'name': f"psa/{address_name}", + 'region': subnet_region, + 'ip_cidr_range': ip_range, + 'total_ip_addresses': total_ip_addresses, + 'used_ip_addresses': 0, + 'network_name': network_name + } + + +def compute_subnet_utilization_redis(config, read_mask, all_subnets_dict): + ''' + Counts Redis (Memorystore) instances using private IPs in the different subnets. + Parameters: + config (dict): Dict containing config like clients and limits + read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory + all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization + Returns: + None + ''' + response_redis = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ["redis.googleapis.com/Instance"], + "read_mask": read_mask, + "page_size": config["page_size"], + }) + + for asset in response_redis: + ip_range = "" + connect_mode = "" + network_name = "" + project_id = "" + region = "" + for versioned in asset.versioned_resources: + for field_name, field_value in versioned.resource.items(): + if field_name == 'locationId': + region = field_value[0:-2] + if field_name == 'authorizedNetwork': + network_name = field_value.split('/')[-1] + project_id = field_value.split('/')[1] + if field_name == 'reservedIpRange': + ip_range = field_value + if field_name == 'connectMode': + connect_mode = field_value + + # Only handling PSA for Redis for now + if connect_mode == "PRIVATE_SERVICE_ACCESS": + redis_ip_range = ipaddress.ip_network(ip_range) + for subnet_key, subnet_dict in all_subnets_dict[project_id].items(): + if subnet_dict["network_name"] == network_name: + # Reddis instance asset doesn't contain the subnet information in Asset Inventory + # We need to find the correct subnet range with IP address matching to compute the utilization + if redis_ip_range.overlaps( + ipaddress.ip_network(subnet_dict['ip_cidr_range'])): + all_subnets_dict[project_id][subnet_key][ + 'used_ip_addresses'] += redis_ip_range.num_addresses + all_subnets_dict[project_id][subnet_key]['region'] = region + + +def compute_subnet_utilization(config, all_subnets_dict): + ''' + Counts resources (VMs, ILBs, reserved IPs) using private IPs in the different subnets. + Parameters: + config (dict): Dict containing config like clients and limits + all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization + Returns: + None + ''' + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + + compute_subnet_utilization_vms(config, read_mask, all_subnets_dict) + compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict) + compute_subnet_utilization_addresses(config, read_mask, all_subnets_dict) + # TODO: Other PSA services such as FileStore, Cloud SQL + compute_subnet_utilization_redis(config, read_mask, all_subnets_dict) + + # TODO: Handle secondary ranges and count GKE pods def get_subnets(config, metrics_dict): diff --git a/blueprints/cloud-operations/network-dashboard/main.tf b/blueprints/cloud-operations/network-dashboard/main.tf index 5710f25c1d..2b8acbbf1a 100644 --- a/blueprints/cloud-operations/network-dashboard/main.tf +++ b/blueprints/cloud-operations/network-dashboard/main.tf @@ -21,6 +21,7 @@ locals { folder_ids = toset(var.monitored_folders_list) folders = join(",", local.folder_ids) monitoring_project = var.monitoring_project_id == "" ? module.project-monitoring[0].project_id : var.monitoring_project_id + metrics_project = var.metrics_project_id == "" ? (var.monitoring_project_id == "" ? module.project-monitoring[0].project_id : var.monitoring_project_id) : var.metrics_project_id } ################################################ @@ -60,7 +61,7 @@ module "service-account-function" { } iam_project_roles = { - "${local.monitoring_project}" = [ + "${local.metrics_project}" = [ "roles/monitoring.metricWriter", ] } @@ -141,6 +142,13 @@ module "cloud-function" { lifecycle_delete_age = null } region = var.region + vpc_connector = (var.vpc_connector_name != "" ? + { + create = false + name = var.vpc_connector_name + egress_settings = "ALL_TRAFFIC" + } : null) + bundle_config = { source_dir = "cloud-function" @@ -160,7 +168,7 @@ module "cloud-function" { environment_variables = { MONITORED_PROJECTS_LIST = local.projects MONITORED_FOLDERS_LIST = local.folders - MONITORING_PROJECT_ID = local.monitoring_project + MONITORING_PROJECT_ID = local.metrics_project ORGANIZATION_ID = var.organization_id CF_VERSION = var.cf_version } @@ -182,5 +190,9 @@ module "cloud-function" { resource "google_monitoring_dashboard" "dashboard" { dashboard_json = file("${path.module}/dashboards/quotas-utilization.json") +<<<<<<< HEAD + project = local.metrics_project +======= project = local.monitoring_project +>>>>>>> b7bfcf3575cda18a2fdd2862c72e33c1648c0aa4 } diff --git a/blueprints/cloud-operations/network-dashboard/variables.tf b/blueprints/cloud-operations/network-dashboard/variables.tf index b370cd08f6..6100d4a7d9 100644 --- a/blueprints/cloud-operations/network-dashboard/variables.tf +++ b/blueprints/cloud-operations/network-dashboard/variables.tf @@ -39,10 +39,17 @@ variable "monitored_projects_list" { } variable "monitoring_project_id" { - description = "Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string" + description = "Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string, if metrics_project_id is provided, metrics and dashboard will be deployed there " default = "" } +variable "metrics_project_id" { + description = "Optional, populate to write metrics and deploy the dashboard in a separated project" + default = "" +} + + + variable "organization_id" { description = "The organization id for the associated services" @@ -88,3 +95,13 @@ variable "schedule_cron" { description = "Cron format schedule to run the Cloud Function. Default is every 10 minutes." default = "*/10 * * * *" } + + +variable "vpc_connector_name" { + description = "Serverless VPC connection name for the Cloud Function" + default = "" +} + + + + diff --git a/blueprints/cloud-operations/packer-image-builder/main.tf b/blueprints/cloud-operations/packer-image-builder/main.tf index f8ea374f73..f6de3af518 100644 --- a/blueprints/cloud-operations/packer-image-builder/main.tf +++ b/blueprints/cloud-operations/packer-image-builder/main.tf @@ -67,17 +67,16 @@ module "firewall" { source = "../../../modules/net-vpc-firewall" project_id = module.project.project_id network = module.vpc.name - custom_rules = { + ingress_rules = { image-builder-ingress-builder-vm = { description = "Allow image builder vm ingress traffic" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = var.packer_source_cidrs + source_ranges = var.packer_source_cidrs targets = [module.service-account-image-builder-vm.email] use_service_accounts = true - rules = [{ protocol = "tcp", ports = [22, 5985, 5986] }] - extra_attributes = {} + rules = [{ + protocol = "tcp" + ports = [22, 5985, 5986] + }] } } } diff --git a/blueprints/cloud-operations/vm-migration/single-project/main.tf b/blueprints/cloud-operations/vm-migration/single-project/main.tf index acb06a91bf..a6fee84308 100644 --- a/blueprints/cloud-operations/vm-migration/single-project/main.tf +++ b/blueprints/cloud-operations/vm-migration/single-project/main.tf @@ -66,24 +66,7 @@ module "landing-vpc" { } module "landing-vpc-firewall" { - source = "../../../../modules/net-vpc-firewall" - project_id = module.landing-project.project_id - network = module.landing-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - custom_rules = { - allow-ssh = { - description = "Allow SSH from IAP" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["35.235.240.0/20"] - targets = [] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = ["22"] }] - extra_attributes = {} - } - } + source = "../../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-vpc.name } diff --git a/blueprints/data-solutions/cloudsql-multiregion/main.tf b/blueprints/data-solutions/cloudsql-multiregion/main.tf index 41a111fedb..1fd76aa3f4 100644 --- a/blueprints/data-solutions/cloudsql-multiregion/main.tf +++ b/blueprints/data-solutions/cloudsql-multiregion/main.tf @@ -128,11 +128,13 @@ module "vpc" { } module "firewall" { - source = "../../../modules/net-vpc-firewall" - count = local.use_shared_vpc ? 0 : 1 - project_id = module.project.project_id - network = module.vpc.0.name - admin_ranges = ["10.0.0.0/20"] + source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 + project_id = module.project.project_id + network = module.vpc.0.name + default_rules_config = { + admin_ranges = ["10.0.0.0/20"] + } } module "nat" { diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf index d507bfdff0..fb7f9fdd16 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf @@ -59,10 +59,12 @@ module "vpc" { } module "vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project-service.project_id - network = module.vpc.name - admin_ranges = [var.vpc_ip_cidr_range] + source = "../../../modules/net-vpc-firewall" + project_id = module.project-service.project_id + network = module.vpc.name + default_rules_config = { + admin_ranges = [var.vpc_ip_cidr_range] + } } ############################################################################### diff --git a/blueprints/data-solutions/data-platform-foundations/02-load.tf b/blueprints/data-solutions/data-platform-foundations/02-load.tf index 5c61da9f83..74cb9f8b0c 100644 --- a/blueprints/data-solutions/data-platform-foundations/02-load.tf +++ b/blueprints/data-solutions/data-platform-foundations/02-load.tf @@ -118,11 +118,13 @@ module "load-vpc" { } module "load-vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - count = local.use_shared_vpc ? 0 : 1 - project_id = module.load-project.project_id - network = module.load-vpc.0.name - admin_ranges = ["10.10.0.0/24"] + source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 + project_id = module.load-project.project_id + network = module.load-vpc.0.name + default_rules_config = { + admin_ranges = ["10.10.0.0/24"] + } } module "load-nat" { diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index 2990a2c57d..2974c12270 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -133,11 +133,13 @@ module "orch-vpc" { } module "orch-vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - count = local.use_shared_vpc ? 0 : 1 - project_id = module.orch-project.project_id - network = module.orch-vpc.0.name - admin_ranges = ["10.10.0.0/24"] + source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 + project_id = module.orch-project.project_id + network = module.orch-vpc.0.name + default_rules_config = { + admin_ranges = ["10.10.0.0/24"] + } } module "orch-nat" { diff --git a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf index e696bec6bc..3d3a818c57 100644 --- a/blueprints/data-solutions/data-platform-foundations/04-transformation.tf +++ b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf @@ -142,11 +142,13 @@ module "transf-vpc" { } module "transf-vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - count = local.use_shared_vpc ? 0 : 1 - project_id = module.transf-project.project_id - network = module.transf-vpc.0.name - admin_ranges = ["10.10.0.0/24"] + source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 + project_id = module.transf-project.project_id + network = module.transf-vpc.0.name + default_rules_config = { + admin_ranges = ["10.10.0.0/24"] + } } module "transf-nat" { diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index bcdea5dfa5..a561c1d63a 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -72,22 +72,19 @@ module "vpc" { } module "vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project.project_id - network = module.vpc.name - admin_ranges = [var.vpc_config.ip_cidr_range] - custom_rules = { + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc.name + default_rules_config = { + admin_ranges = [var.vpc_config.ip_cidr_range] + } + ingress_rules = { #TODO Remove and rely on 'ssh' tag once terraform-provider-google/issues/9273 is fixed ("${var.prefix}-iap") = { - description = "Enable SSH from IAP on Notebooks." - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["35.235.240.0/20"] - targets = ["notebook-instance"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [22] }] - extra_attributes = {} + description = "Enable SSH from IAP on Notebooks." + source_ranges = ["35.235.240.0/20"] + targets = ["notebook-instance"] + rules = [{ protocol = "tcp", ports = [22] }] } } } diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf index aab9a657bb..fd47952bfb 100644 --- a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf +++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf @@ -27,11 +27,13 @@ module "vpc" { } module "vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - count = local.use_shared_vpc ? 0 : 1 - project_id = module.project.project_id - network = module.vpc[0].name - admin_ranges = [var.vpc_subnet_range] + source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 + project_id = module.project.project_id + network = module.vpc[0].name + default_rules_config = { + admin_ranges = [var.vpc_subnet_range] + } } module "nat" { diff --git a/blueprints/data-solutions/sqlserver-alwayson/main.tf b/blueprints/data-solutions/sqlserver-alwayson/main.tf index 3d391c5f3d..6485b46dd0 100644 --- a/blueprints/data-solutions/sqlserver-alwayson/main.tf +++ b/blueprints/data-solutions/sqlserver-alwayson/main.tf @@ -13,25 +13,54 @@ # limitations under the License. locals { - prefix = var.prefix != "" ? format("%s-", var.prefix) : "" - vpc_project = var.shared_vpc_project_id != null ? var.shared_vpc_project_id : module.project.project_id - - network = module.vpc.self_link - subnetwork = var.project_create != null ? module.vpc.subnet_self_links[format("%s/%s", var.region, var.subnetwork)] : data.google_compute_subnetwork.subnetwork[0].self_link - - node_base = format("%s%s", local.prefix, var.node_name) - node_prefix = length(local.node_base) > 12 ? substr(local.node_base, 0, 12) : local.node_base - node_netbios_names = [for idx in range(1, 3) : format("%s-%02d", local.node_prefix, idx)] - witness_name = format("%s%s", local.prefix, var.witness_name) - witness_netbios_name = length(local.witness_name) > 15 ? substr(local.witness_name, 0, 15) : local.witness_name - zones = var.project_create == null ? data.google_compute_zones.zones[0].names : formatlist("${var.region}-%s", ["a", "b", "c"]) - node_zones = merge({ for idx, node_name in local.node_netbios_names : node_name => local.zones[idx] }, - { (local.witness_netbios_name) = local.zones[length(local.zones) - 1] }) - - cluster_full_name = format("%s%s", local.prefix, var.cluster_name) - cluster_netbios_name = length(local.cluster_full_name) > 15 ? substr(local.cluster_full_name, 0, 15) : local.cluster_full_name - - ad_user_password_secret = format("%s%s-password", local.prefix, var.cluster_name) + ad_user_password_secret = "${local.cluster_full_name}-password" + cluster_full_name = "${local.prefix}${var.cluster_name}" + cluster_netbios_name = ( + length(local.cluster_full_name) > 15 + ? substr(local.cluster_full_name, 0, 15) + : local.cluster_full_name + ) + network = module.vpc.self_link + node_base = "${local.prefix}${var.node_name}" + node_prefix = ( + length(local.node_base) > 12 + ? substr(local.node_base, 0, 12) + : local.node_base + ) + node_netbios_names = [ + for idx in range(1, 3) : format("%s-%02d", local.node_prefix, idx) + ] + node_zones = merge( + { + for idx, node_name in local.node_netbios_names : + node_name => local.zones[idx] + }, + { + (local.witness_netbios_name) = local.zones[length(local.zones) - 1] + } + ) + prefix = var.prefix != "" ? "${var.prefix}-" : "" + subnetwork = ( + var.project_create != null + ? module.vpc.subnet_self_links["${var.region}/${var.subnetwork}"] + : data.google_compute_subnetwork.subnetwork[0].self_link + ) + vpc_project = ( + var.shared_vpc_project_id != null + ? var.shared_vpc_project_id + : module.project.project_id + ) + witness_name = "${local.prefix}${var.witness_name}" + witness_netbios_name = ( + length(local.witness_name) > 15 + ? substr(local.witness_name, 0, 15) + : local.witness_name + ) + zones = ( + var.project_create == null + ? data.google_compute_zones.zones[0].names + : formatlist("${var.region}-%s", ["a", "b", "c"]) + ) } module "project" { diff --git a/blueprints/data-solutions/sqlserver-alwayson/vpc.tf b/blueprints/data-solutions/sqlserver-alwayson/vpc.tf index dbbf38a79b..6f0b9120e1 100644 --- a/blueprints/data-solutions/sqlserver-alwayson/vpc.tf +++ b/blueprints/data-solutions/sqlserver-alwayson/vpc.tf @@ -15,26 +15,36 @@ # tfdoc:file:description Creates the VPC and manages the firewall rules and ILB. locals { - listeners = { for aog in var.always_on_groups : format("%slb-%s", local.prefix, aog) => { - region = var.region - subnetwork = local.subnetwork - } - } - node_ips = { for node_name in local.node_netbios_names : node_name => { - region = var.region - subnetwork = local.subnetwork + internal_addresses = merge( + local.listeners, + local.node_ips, + { + "${local.prefix}cluster" = { + region = var.region + subnetwork = local.subnetwork + } + (local.witness_netbios_name) = { + region = var.region + subnetwork = local.subnetwork + } } + ) + internal_address_ips = { + for k, v in module.ip-addresses.internal_addresses : + k => v.address } - internal_addresses = merge({ - format("%scluster", local.prefix) = { + listeners = { + for aog in var.always_on_groups : "${local.prefix}lb-${aog}" => { region = var.region subnetwork = local.subnetwork } - (local.witness_netbios_name) = { + } + node_ips = { + for node_name in local.node_netbios_names : node_name => { region = var.region subnetwork = local.subnetwork } - }, local.listeners, local.node_ips) + } } data "google_compute_zones" "zones" { @@ -50,7 +60,6 @@ data "google_compute_subnetwork" "subnetwork" { region = var.region } -# Create VPC if required module "vpc" { source = "../../../modules/net-vpc" @@ -66,108 +75,82 @@ module "vpc" { vpc_create = var.project_create != null ? true : false } -# Firewall rules required for WSFC nodes module "firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = local.vpc_project - network = local.network - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - custom_rules = { - format("%sallow-all-between-wsfc-nodes", local.prefix) = { + source = "../../../modules/net-vpc-firewall" + project_id = local.vpc_project + network = local.network + default_rules_config = { + disabled = true + } + ingress_rules = { + "${local.prefix}allow-all-between-wsfc-nodes" = { description = "Allow all between WSFC nodes" - direction = "INGRESS" - action = "allow" sources = [module.compute-service-account.email] targets = [module.compute-service-account.email] - ranges = [] use_service_accounts = true rules = [ - { protocol = "tcp", ports = [] }, - { protocol = "udp", ports = [] }, - { protocol = "icmp", ports = [] } + { protocol = "tcp" }, + { protocol = "udp" }, + { protocol = "icmp" } ] - extra_attributes = {} } - format("%sallow-all-between-wsfc-witness", local.prefix) = { + "${local.prefix}allow-all-between-wsfc-witness" = { description = "Allow all between WSFC witness nodes" - direction = "INGRESS" - action = "allow" sources = [module.compute-service-account.email] targets = [module.witness-service-account.email] - ranges = [] use_service_accounts = true rules = [ - { protocol = "tcp", ports = [] }, - { protocol = "udp", ports = [] }, - { protocol = "icmp", ports = [] } + { protocol = "tcp" }, + { protocol = "udp" }, + { protocol = "icmp" } ] - extra_attributes = {} } - format("%sallow-sql-to-wsfc-nodes", local.prefix) = { + "${local.prefix}allow-sql-to-wsfc-nodes" = { description = "Allow SQL connections to WSFC nodes" - direction = "INGRESS" - action = "allow" - sources = [] targets = [module.compute-service-account.email] ranges = var.sql_client_cidrs use_service_accounts = true rules = [ { protocol = "tcp", ports = [1433] }, ] - extra_attributes = {} } - format("%sallow-health-check-to-wsfc-nodes", local.prefix) = { + "${local.prefix}allow-health-check-to-wsfc-nodes" = { description = "Allow health checks to WSFC nodes" - direction = "INGRESS" - action = "allow" - sources = [] targets = [module.compute-service-account.email] ranges = var.health_check_ranges use_service_accounts = true rules = [ - { protocol = "tcp", ports = [] }, + { protocol = "tcp" } ] - extra_attributes = {} } } } -# IP Address reservation for cluster and listener module "ip-addresses" { - source = "../../../modules/net-address" - project_id = local.vpc_project - + source = "../../../modules/net-address" + project_id = local.vpc_project internal_addresses = local.internal_addresses } -# L4 Internal Load Balancer for SQL Listener module "listener-ilb" { - source = "../../../modules/net-ilb" - for_each = toset(var.always_on_groups) - - project_id = var.project_id - region = var.region - - name = format("%s-%s-ilb", var.prefix, each.value) - service_label = format("%s-%s-ilb", var.prefix, each.value) - - address = module.ip-addresses.internal_addresses[format("%slb-%s", local.prefix, each.value)].address - network = local.network - subnetwork = local.subnetwork - + source = "../../../modules/net-ilb" + for_each = toset(var.always_on_groups) + project_id = var.project_id + region = var.region + name = "${var.prefix}-${each.value}-ilb" + service_label = "${var.prefix}-${each.value}-ilb" + address = local.internal_address_ips["${local.prefix}lb-${each.value}"] + vpc_config = { + network = local.network + subnetwork = local.subnetwork + } backends = [for k, node in module.nodes : { - failover = false - group = node.group.self_link - balancing_mode = "CONNECTION" + group = node.group.self_link }] - health_check_config = { - type = "tcp", - check = { port = var.health_check_port }, - config = var.health_check_config, - logging = true + enable_logging = true + tcp = { + port = var.health_check_port + } } } diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf index 6c09546a22..604797be7b 100644 --- a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf +++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf @@ -40,36 +40,36 @@ module "firewall" { source = "../../../modules/net-vpc-firewall" project_id = module.host_project.project_id network = module.svpc.name - custom_rules = merge({ allow-mesh = { - description = "Allow " - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [for k, v in var.clusters_config : v.pods_cidr_block] - targets = [for k, v in var.clusters_config : "${k}-node"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = null }, - { protocol = "udp", ports = null }, - { protocol = "icmp", ports = null }, - { protocol = "esp", ports = null }, - { protocol = "ah", ports = null }, - { protocol = "sctp", ports = null }] - extra_attributes = { - priority = 900 - } - } }, - { for k, v in var.clusters_config : "allow-${k}-istio" => { - description = "Allow " - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [v.master_cidr_block] - targets = ["${k}-node"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [8080, 15014, 15017] }] - extra_attributes = { - priority = 1000 + ingress_rules = merge( + { + allow-mesh = { + description = "Allow mesh." + priority = 900 + source_ranges = [ + for k, v in var.clusters_config : v.pods_cidr_block + ] + targets = [ + for k, v in var.clusters_config : "${k}-node" + ] + rules = [ + { protocol = "tcp" }, + { protocol = "udp" }, + { protocol = "icmp" }, + { protocol = "esp" }, + { protocol = "ah" }, + { protocol = "sctp" } + ] } + }, + { + for k, v in var.clusters_config : "allow-${k}-istio" => { + description = "Allow istio." + source_ranges = [v.master_cidr_block] + targets = ["${k}-node"] + rules = [{ + protocol = "tcp" + ports = [8080, 15014, 15017] + }] } } ) diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf index ca998bf92e..06efa81475 100644 --- a/blueprints/networking/filtering-proxy/main.tf +++ b/blueprints/networking/filtering-proxy/main.tf @@ -74,17 +74,18 @@ module "firewall" { source = "../../../modules/net-vpc-firewall" project_id = module.project-host.project_id network = module.vpc.name - custom_rules = { + ingress_rules = { allow-ingress-squid = { - description = "Allow squid ingress traffic" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [var.cidrs.apps, "35.191.0.0/16", "130.211.0.0/22"] + description = "Allow squid ingress traffic" + source_ranges = [ + var.cidrs.apps, "35.191.0.0/16", "130.211.0.0/22" + ] targets = [module.service-account-squid.email] use_service_accounts = true - rules = [{ protocol = "tcp", ports = [3128] }] - extra_attributes = {} + rules = [{ + protocol = "tcp" + ports = [3128] + }] } } } @@ -199,20 +200,20 @@ module "squid-ilb" { project_id = module.project-host.project_id region = var.region name = "squid-ilb" - service_label = "squid-ilb" - network = module.vpc.self_link - subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"] ports = [3128] + service_label = "squid-ilb" + vpc_config = { + network = module.vpc.self_link + subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"] + } backends = [{ - failover = false - group = module.squid-mig.0.group_manager.instance_group - balancing_mode = "CONNECTION" + group = module.squid-mig.0.group_manager.instance_group }] health_check_config = { - type = "tcp" - check = { port = 3128 } - config = {} - logging = true + enable_logging = true + tcp = { + port = 3128 + } } } diff --git a/blueprints/networking/hub-and-spoke-peering/main.tf b/blueprints/networking/hub-and-spoke-peering/main.tf index 525e20d0fc..7fa8142e47 100644 --- a/blueprints/networking/hub-and-spoke-peering/main.tf +++ b/blueprints/networking/hub-and-spoke-peering/main.tf @@ -69,10 +69,12 @@ module "nat-hub" { } module "vpc-hub-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = var.project_id - network = module.vpc-hub.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc-hub.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } ################################################################################ @@ -93,10 +95,12 @@ module "vpc-spoke-1" { } module "vpc-spoke-1-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project.project_id - network = module.vpc-spoke-1.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc-spoke-1.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "nat-spoke-1" { @@ -138,10 +142,12 @@ module "vpc-spoke-2" { } module "vpc-spoke-2-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project.project_id - network = module.vpc-spoke-2.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc-spoke-2.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "nat-spoke-2" { diff --git a/blueprints/networking/hub-and-spoke-vpn/net-dev.tf b/blueprints/networking/hub-and-spoke-vpn/net-dev.tf index 7d1c083580..736c742f62 100644 --- a/blueprints/networking/hub-and-spoke-vpn/net-dev.tf +++ b/blueprints/networking/hub-and-spoke-vpn/net-dev.tf @@ -39,10 +39,12 @@ module "dev-vpc" { } module "dev-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = var.project_id - network = module.dev-vpc.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.dev-vpc.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "dev-dns-peering" { diff --git a/blueprints/networking/hub-and-spoke-vpn/net-landing.tf b/blueprints/networking/hub-and-spoke-vpn/net-landing.tf index c70e2295ea..b385bfb1da 100644 --- a/blueprints/networking/hub-and-spoke-vpn/net-landing.tf +++ b/blueprints/networking/hub-and-spoke-vpn/net-landing.tf @@ -39,10 +39,12 @@ module "landing-vpc" { } module "landing-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = var.project_id - network = module.landing-vpc.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.landing-vpc.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "landing-dns-zone" { diff --git a/blueprints/networking/hub-and-spoke-vpn/net-prod.tf b/blueprints/networking/hub-and-spoke-vpn/net-prod.tf index aeb62019f3..ad58b58584 100644 --- a/blueprints/networking/hub-and-spoke-vpn/net-prod.tf +++ b/blueprints/networking/hub-and-spoke-vpn/net-prod.tf @@ -39,10 +39,12 @@ module "prod-vpc" { } module "prod-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = var.project_id - network = module.prod-vpc.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.prod-vpc.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "prod-dns-peering" { diff --git a/blueprints/networking/ilb-next-hop/gateways.tf b/blueprints/networking/ilb-next-hop/gateways.tf index e5a7fa253e..df06648413 100644 --- a/blueprints/networking/ilb-next-hop/gateways.tf +++ b/blueprints/networking/ilb-next-hop/gateways.tf @@ -62,22 +62,22 @@ module "ilb-left" { project_id = module.project.project_id region = var.region name = "${local.prefix}ilb-left" - network = module.vpc-left.self_link - subnetwork = values(module.vpc-left.subnet_self_links)[0] - address = local.addresses.ilb-left - ports = null - backend_config = { - session_affinity = var.ilb_session_affinity - timeout_sec = null - connection_draining_timeout_sec = null + vpc_config = { + network = module.vpc-left.self_link + subnetwork = values(module.vpc-left.subnet_self_links)[0] + } + address = local.addresses.ilb-left + backend_service_config = { + session_affinity = var.ilb_session_affinity } backends = [for z, mod in module.gw : { - failover = false - group = mod.group.self_link - balancing_mode = "CONNECTION" + group = mod.group.self_link }] health_check_config = { - type = "tcp", check = { port = 22 }, config = {}, logging = true + enable_logging = true + tcp = { + port = 22 + } } } @@ -86,21 +86,21 @@ module "ilb-right" { project_id = module.project.project_id region = var.region name = "${local.prefix}ilb-right" - network = module.vpc-right.self_link - subnetwork = values(module.vpc-right.subnet_self_links)[0] - address = local.addresses.ilb-right - ports = null - backend_config = { - session_affinity = var.ilb_session_affinity - timeout_sec = null - connection_draining_timeout_sec = null + vpc_config = { + network = module.vpc-right.self_link + subnetwork = values(module.vpc-right.subnet_self_links)[0] + } + address = local.addresses.ilb-right + backend_service_config = { + session_affinity = var.ilb_session_affinity } backends = [for z, mod in module.gw : { - failover = false - group = mod.group.self_link - balancing_mode = "CONNECTION" + group = mod.group.self_link }] health_check_config = { - type = "tcp", check = { port = 22 }, config = {}, logging = true + enable_logging = true + tcp = { + port = 22 + } } } diff --git a/blueprints/networking/ilb-next-hop/vpc-left.tf b/blueprints/networking/ilb-next-hop/vpc-left.tf index 426614c9fe..f5df5234cb 100644 --- a/blueprints/networking/ilb-next-hop/vpc-left.tf +++ b/blueprints/networking/ilb-next-hop/vpc-left.tf @@ -35,11 +35,13 @@ module "vpc-left" { } module "firewall-left" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project.project_id - network = module.vpc-left.name - admin_ranges = values(var.ip_ranges) - ssh_source_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"] + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc-left.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + ssh_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"] + } } module "nat-left" { diff --git a/blueprints/networking/ilb-next-hop/vpc-right.tf b/blueprints/networking/ilb-next-hop/vpc-right.tf index 95350eb09c..edd6941d6d 100644 --- a/blueprints/networking/ilb-next-hop/vpc-right.tf +++ b/blueprints/networking/ilb-next-hop/vpc-right.tf @@ -46,11 +46,13 @@ module "vpc-right" { } module "firewall-right" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project.project_id - network = module.vpc-right.name - admin_ranges = values(var.ip_ranges) - ssh_source_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"] + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc-right.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + ssh_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"] + } } module "nat-right" { diff --git a/blueprints/networking/nginx-reverse-proxy-cluster/main.tf b/blueprints/networking/nginx-reverse-proxy-cluster/main.tf index 5c58401c8d..c361aca05c 100644 --- a/blueprints/networking/nginx-reverse-proxy-cluster/main.tf +++ b/blueprints/networking/nginx-reverse-proxy-cluster/main.tf @@ -161,28 +161,22 @@ module "firewall" { source = "../../../modules/net-vpc-firewall" project_id = module.project.project_id network = module.vpc.name - custom_rules = { + ingress_rules = { format("%sallow-http-to-proxy-cluster", var.prefix) = { - description = "Allow Nginx HTTP(S) ingress traffic" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [var.cidrs[var.subnetwork], "35.191.0.0/16", "130.211.0.0/22"] + description = "Allow Nginx HTTP(S) ingress traffic" + source_ranges = [ + var.cidrs[var.subnetwork], "35.191.0.0/16", "130.211.0.0/22" + ] targets = [module.service-account-proxy.email] use_service_accounts = true rules = [{ protocol = "tcp", ports = [80, 443] }] - extra_attributes = {} } format("%sallow-iap-ssh", var.prefix) = { description = "Allow Nginx SSH traffic from IAP" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["35.235.240.0/20"] + source_ranges = ["35.235.240.0/20"] targets = [module.service-account-proxy.email] use_service_accounts = true rules = [{ protocol = "tcp", ports = [22] }] - extra_attributes = {} } } } diff --git a/blueprints/networking/onprem-google-access-dns/main.tf b/blueprints/networking/onprem-google-access-dns/main.tf index 4a78f2cabc..141860608c 100644 --- a/blueprints/networking/onprem-google-access-dns/main.tf +++ b/blueprints/networking/onprem-google-access-dns/main.tf @@ -69,11 +69,13 @@ module "vpc" { } module "vpc-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = var.project_id - network = module.vpc.name - admin_ranges = values(var.ip_ranges) - ssh_source_ranges = var.ssh_source_ranges + source = "../../../modules/net-vpc-firewall" + project_id = var.project_id + network = module.vpc.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + ssh_ranges = var.ssh_source_ranges + } } module "vpn1" { diff --git a/blueprints/networking/shared-vpc-gke/main.tf b/blueprints/networking/shared-vpc-gke/main.tf index 59d07d2dd5..2e770377f1 100644 --- a/blueprints/networking/shared-vpc-gke/main.tf +++ b/blueprints/networking/shared-vpc-gke/main.tf @@ -130,10 +130,12 @@ module "vpc-shared" { } module "vpc-shared-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.project-host.project_id - network = module.vpc-shared.name - admin_ranges = values(var.ip_ranges) + source = "../../../modules/net-vpc-firewall" + project_id = module.project-host.project_id + network = module.vpc-shared.name + default_rules_config = { + admin_ranges = values(var.ip_ranges) + } } module "nat" { diff --git a/fast/assets/schemas/firewall_rules.schema.yaml b/fast/assets/schemas/firewall_rules.schema.yaml index 1fd96caf17..6f8a8054dd 100644 --- a/fast/assets/schemas/firewall_rules.schema.yaml +++ b/fast/assets/schemas/firewall_rules.schema.yaml @@ -12,18 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -map(include('firewall_rule')) +egress: map(include('firewall_rule'), required=False) +ingress: map(include('firewall_rule'), required=False) --- firewall_rule: - description: str() - direction: enum("INGRESS", "EGRESS") - action: enum("allow", "deny") - sources: list(str()) - ranges: list(str()) - targets: list(str()) - use_service_accounts: bool() - rules: list(include('rule')) + deny: bool(required=False) + description: str(required=False) + destination_ranges: list(str(), required=False) + disabled: bool(required=False) + # enable_logging: + # include_metadata: bool(required=False) + priority: int(required=False) + source_ranges: list(str(), required=False) + sources: list(str(), required=False) + targets: list(str(), required=False) + use_service_accounts: bool(required=False) + rules: list(include('rule'), required=False) --- rule: - protocol: enum("tcp", "udp", "all") + protocol: str() ports: list(num()) diff --git a/fast/assets/templates/workflow-github.yaml b/fast/assets/templates/workflow-github.yaml index 81b5f3d2f9..1efb9c6670 100644 --- a/fast/assets/templates/workflow-github.yaml +++ b/fast/assets/templates/workflow-github.yaml @@ -30,7 +30,7 @@ env: SSH_AUTH_SOCK: /tmp/ssh_agent.sock TF_PROVIDERS_FILE: ${tf_providers_file} TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} - TF_VERSION: 1.1.7 + TF_VERSION: 1.3.2 jobs: fast-pr: diff --git a/fast/assets/templates/workflow-sourcerepo.yaml b/fast/assets/templates/workflow-sourcerepo.yaml index 7f6f08ff04..446c9c9605 100644 --- a/fast/assets/templates/workflow-sourcerepo.yaml +++ b/fast/assets/templates/workflow-sourcerepo.yaml @@ -95,4 +95,4 @@ substitutions: _FAST_OUTPUTS_BUCKET: ${outputs_bucket} _TF_PROVIDERS_FILE: ${tf_providers_file} _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} - _TF_VERSION: 1.1.7 + _TF_VERSION: 1.3.2 diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md index 2db9952c51..52c322a33a 100644 --- a/fast/extras/00-cicd-github/README.md +++ b/fast/extras/00-cicd-github/README.md @@ -8,11 +8,16 @@ This stage is designed for quick repository creation in a GitHub organization, a Initial file population of repositories is controlled via the `populate_from` attribute, and needs a bit of care: -- never run this stage gain with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. -- be mindful when enabling initial population of the modules repository, as the number of resulting files to manage is very close to the GitHub hourly limit for their API +- never run this stage with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. +- initial population of the modules repository is discouraged, as the number of resulting files Terraform needs to manage is very close to the GitHub hourly limit for their API, it's much easier to populate modules via regular git commands The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incremental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). +Once initial population is done, you need to manually push to the repository + +- the `.tfvars` file with custom variable values for your stages +- the workflow configuration file generated by FAST stages + ## GitHub provider credentials A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: @@ -77,7 +82,8 @@ When initial population is configured for a repository, this stage also adds a s | name | description | resources | |---|---|---| | [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | -| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_file · tls_private_key | +| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | +| [outputs.tf](./outputs.tf) | Module outputs. | | | [providers.tf](./providers.tf) | Provider configuration. | | | [variables.tf](./variables.tf) | Module variables. | | @@ -85,8 +91,15 @@ When initial population is configured for a repository, this stage also adds a s | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization](variables.tf#L28) | GitHub organization. | string | ✓ | | +| [organization](variables.tf#L34) | GitHub organization. | string | ✓ | | | [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | -| [repositories](variables.tf#L33) | Repositories to create. | map(object({…})) | | {} | +| [modules_ref](variables.tf#L28) | Optional git ref used in module sources. | string | | null | +| [repositories](variables.tf#L39) | Repositories to create. | map(object({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [clone](outputs.tf#L17) | Clone repository commands. | | diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf index 140230d06d..4ba49f7349 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/00-cicd-github/main.tf @@ -30,6 +30,7 @@ locals { } ] if v.populate_from != null ]) + modules_ref = var.modules_ref == null ? "" : "?ref=${var.modules_ref}" modules_repository = ( length(local._modules_repository) > 0 ? local._modules_repository.0 @@ -39,13 +40,24 @@ locals { for k, v in var.repositories : k => v.create_options == null ? k : github_repository.default[k].name } - repository_files = { - for k in local._repository_files : - "${k.repository}/${k.name}" => k - if !endswith(k.name, ".tf") || ( - !startswith(k.name, "0") && k.name != "globals.tf" - ) - } + repository_files = merge( + { + for k in local._repository_files : + "${k.repository}/${k.name}" => k + if !endswith(k.name, ".tf") || ( + !startswith(k.name, "0") && k.name != "globals.tf" + ) + }, + { + for k, v in var.repositories : + "${k}/templates/providers.tf.tpl" => { + repository = k + file = "../../assets/templates/providers.tf.tpl" + name = "templates/providers.tf.tpl" + } + if v.populate_from != null + } + ) } resource "github_repository" "default" { @@ -88,15 +100,20 @@ resource "tls_private_key" "default" { algorithm = "ED25519" } +resource "github_repository_deploy_key" "exdefaultample_repository_deploy_key" { + count = local.modules_repository == null ? 0 : 1 + title = "Modules repository access" + repository = local.modules_repository + key = tls_private_key.default.0.public_key_openssh + read_only = true +} + resource "github_actions_secret" "default" { for_each = local.modules_repository == null ? {} : { for k, v in local.repositories : - k => v if( - k != local.modules_repository && - var.repositories[k].populate_from != null - ) + k => v if k != local.modules_repository } - repository = local.repositories[local.modules_repository] + repository = each.key secret_name = "CICD_MODULES_KEY" plaintext_value = tls_private_key.default.0.private_key_openssh } @@ -112,8 +129,8 @@ resource "github_repository_file" "default" { endswith(each.value.name, ".tf") && local.modules_repository != null ? replace( file(each.value.file), - "/source\\s*=\\s*\"../../../", - "source = \"git@github.com:${var.organization}/${local.modules_repository}.git/" + "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/", + "source = \"git@github.com:${var.organization}/${local.modules_repository}.git//$1${local.modules_ref}\"" # " ) : file(each.value.file) ) diff --git a/modules/organization-policy/outputs.tf b/fast/extras/00-cicd-github/outputs.tf similarity index 77% rename from modules/organization-policy/outputs.tf rename to fast/extras/00-cicd-github/outputs.tf index 6134d87607..cb580e1fe2 100644 --- a/modules/organization-policy/outputs.tf +++ b/fast/extras/00-cicd-github/outputs.tf @@ -14,7 +14,10 @@ * limitations under the License. */ -output "policies" { - description = "Organization policies." - value = google_org_policy_policy.primary +output "clone" { + description = "Clone repository commands." + value = { + for k, v in var.repositories : + k => "git clone git@github.com:${var.organization}/${k}.git" + } } diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf index 29b79225e8..0d9cb7fd6d 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/00-cicd-github/variables.tf @@ -25,6 +25,12 @@ variable "commmit_config" { nullable = false } +variable "modules_ref" { + description = "Optional git ref used in module sources." + type = string + default = null +} + variable "organization" { description = "GitHub organization." type = string diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/00-bootstrap/README.md index a0bafe49f4..683025a77c 100644 --- a/fast/stages/00-bootstrap/README.md +++ b/fast/stages/00-bootstrap/README.md @@ -262,6 +262,7 @@ terraform init terraform apply \ -var bootstrap_user=$(gcloud config list --format 'value(core.account)') ``` +> If you see an error related to project name already exists, please make sure the project name is unique or the project was not deleted recently Once the initial `apply` completes successfully, configure a remote backend using the new GCS bucket, and impersonation on the automation service account for this stage. To do this you can use the generated `providers.tf` file if you have configured output files as described above, or extract its contents from Terraform's output, then migrate state with `terraform init`: diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/00-bootstrap/automation.tf index 13eb68f199..1475c811c9 100644 --- a/fast/stages/00-bootstrap/automation.tf +++ b/fast/stages/00-bootstrap/automation.tf @@ -111,11 +111,11 @@ module "automation-tf-bootstrap-gcs" { } module "automation-tf-bootstrap-sa" { - source = "../../../modules/iam-service-account" - project_id = module.automation-project.project_id - name = "bootstrap-0" - description = "Terraform organization bootstrap service account." - prefix = local.prefix + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "bootstrap-0" + display_name = "Terraform organization bootstrap service account." + prefix = local.prefix # allow SA used by CI/CD workflow to impersonate this SA iam = { "roles/iam.serviceAccountTokenCreator" = compact([ @@ -144,11 +144,11 @@ module "automation-tf-cicd-gcs" { } module "automation-tf-cicd-provisioning-sa" { - source = "../../../modules/iam-service-account" - project_id = module.automation-project.project_id - name = "cicd-0" - description = "Terraform stage 1 CICD service account." - prefix = local.prefix + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "cicd-0" + display_name = "Terraform stage 1 CICD service account." + prefix = local.prefix # allow SA used by CI/CD workflow to impersonate this SA iam = { "roles/iam.serviceAccountTokenCreator" = compact([ @@ -177,11 +177,11 @@ module "automation-tf-resman-gcs" { } module "automation-tf-resman-sa" { - source = "../../../modules/iam-service-account" - project_id = module.automation-project.project_id - name = "resman-0" - description = "Terraform stage 1 resman service account." - prefix = local.prefix + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "resman-0" + display_name = "Terraform stage 1 resman service account." + prefix = local.prefix # allow SA used by CI/CD workflow to impersonate this SA iam = { "roles/iam.serviceAccountTokenCreator" = compact([ diff --git a/fast/stages/00-bootstrap/cicd.tf b/fast/stages/00-bootstrap/cicd.tf index fba8f24816..7cdae41c98 100644 --- a/fast/stages/00-bootstrap/cicd.tf +++ b/fast/stages/00-bootstrap/cicd.tf @@ -87,12 +87,12 @@ module "automation-tf-cicd-repo" { # SAs used by CI/CD workflows to impersonate automation SAs module "automation-tf-cicd-sa" { - source = "../../../modules/iam-service-account" - for_each = local.cicd_repositories - project_id = module.automation-project.project_id - name = "${each.key}-1" - description = "Terraform CI/CD ${each.key} service account." - prefix = local.prefix + source = "../../../modules/iam-service-account" + for_each = local.cicd_repositories + project_id = module.automation-project.project_id + name = "${each.key}-1" + display_name = "Terraform CI/CD ${each.key} service account." + prefix = local.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/00-bootstrap/outputs-files.tf b/fast/stages/00-bootstrap/outputs-files.tf index 3016c8e219..ded88cd56d 100644 --- a/fast/stages/00-bootstrap/outputs-files.tf +++ b/fast/stages/00-bootstrap/outputs-files.tf @@ -19,27 +19,27 @@ resource "local_file" "providers" { for_each = var.outputs_location == null ? {} : local.providers file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/providers/${each.key}-providers.tf" - content = each.value + filename = "${try(pathexpand(var.outputs_location), "")}/providers/${each.key}-providers.tf" + content = try(each.value, null) } resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/00-bootstrap.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/00-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "local_file" "tfvars_globals" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/globals.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/globals.auto.tfvars.json" content = jsonencode(local.tfvars_globals) } resource "local_file" "workflows" { - for_each = local.cicd_workflows + for_each = var.outputs_location == null ? {} : local.cicd_workflows file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/workflows/${each.key}-workflow.yaml" - content = each.value + filename = "${try(pathexpand(var.outputs_location), "")}/workflows/${each.key}-workflow.yaml" + content = try(each.value, null) } diff --git a/fast/stages/01-resman/README.md b/fast/stages/01-resman/README.md index dbeb8afb74..52e73c55e8 100644 --- a/fast/stages/01-resman/README.md +++ b/fast/stages/01-resman/README.md @@ -182,17 +182,18 @@ Due to its simplicity, this stage lends itself easily to customizations: adding |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | | [billing_account](variables.tf#L38) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [organization](variables.tf#L191) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L215) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | +| [organization](variables.tf#L197) | Organization details. | object({…}) | ✓ | | 00-bootstrap | +| [prefix](variables.tf#L221) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | | [cicd_repositories](variables.tf#L47) | 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#L129) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [fast_features](variables.tf#L138) | Selective control for top-level FAST features. | object({…}) | | {…} | 00-bootstrap | -| [groups](variables.tf#L158) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [locations](variables.tf#L173) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 00-bootstrap | -| [organization_policy_configs](variables.tf#L201) | Organization policies customization. | object({…}) | | null | | -| [outputs_location](variables.tf#L209) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | -| [tag_names](variables.tf#L226) | Customized names for resource management tags. | object({…}) | | {…} | | -| [team_folders](variables.tf#L243) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [data_dir](variables.tf#L138) | Relative path for the folder storing configuration data. | string | | "data" | | +| [fast_features](variables.tf#L144) | Selective control for top-level FAST features. | object({…}) | | {…} | 00-bootstrap | +| [groups](variables.tf#L164) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | +| [locations](variables.tf#L179) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 00-bootstrap | +| [organization_policy_configs](variables.tf#L207) | Organization policies customization. | object({…}) | | null | | +| [outputs_location](variables.tf#L215) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | +| [tag_names](variables.tf#L232) | Customized names for resource management tags. | object({…}) | | {…} | | +| [team_folders](variables.tf#L249) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | ## Outputs diff --git a/fast/stages/01-resman/branch-data-platform.tf b/fast/stages/01-resman/branch-data-platform.tf index e6bbd8fc6f..66cc9fbb08 100644 --- a/fast/stages/01-resman/branch-data-platform.tf +++ b/fast/stages/01-resman/branch-data-platform.tf @@ -77,12 +77,12 @@ module "branch-dp-prod-folder" { # automation service accounts and buckets module "branch-dp-dev-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.data_platform ? 1 : 0 - project_id = var.automation.project_id - name = "dev-resman-dp-0" - description = "Terraform data platform development service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-dp-0" + display_name = "Terraform data platform development service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-dp-dev-sa-cicd.0.iam_email, null) @@ -94,12 +94,12 @@ module "branch-dp-dev-sa" { } module "branch-dp-prod-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.data_platform ? 1 : 0 - project_id = var.automation.project_id - name = "prod-resman-dp-0" - description = "Terraform data platform production service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-dp-0" + display_name = "Terraform data platform production service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-dp-prod-sa-cicd.0.iam_email, null) diff --git a/fast/stages/01-resman/branch-gke.tf b/fast/stages/01-resman/branch-gke.tf index dd1d68bd39..84ca41ed59 100644 --- a/fast/stages/01-resman/branch-gke.tf +++ b/fast/stages/01-resman/branch-gke.tf @@ -69,12 +69,12 @@ module "branch-gke-prod-folder" { } module "branch-gke-dev-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.gke ? 1 : 0 - project_id = var.automation.project_id - name = "dev-resman-gke-0" - description = "Terraform gke multitenant dev service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-gke-0" + display_name = "Terraform gke multitenant dev service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( ["group:${local.groups.gcp-devops}"], @@ -89,12 +89,12 @@ module "branch-gke-dev-sa" { } module "branch-gke-prod-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.gke ? 1 : 0 - project_id = var.automation.project_id - name = "prod-resman-gke-0" - description = "Terraform gke multitenant prod service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-gke-0" + display_name = "Terraform gke multitenant prod service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( ["group:${local.groups.gcp-devops}"], diff --git a/fast/stages/01-resman/branch-networking.tf b/fast/stages/01-resman/branch-networking.tf index e21fd5090c..530cf6b09f 100644 --- a/fast/stages/01-resman/branch-networking.tf +++ b/fast/stages/01-resman/branch-networking.tf @@ -86,11 +86,11 @@ module "branch-network-dev-folder" { # automation service account and bucket module "branch-network-sa" { - source = "../../../modules/iam-service-account" - project_id = var.automation.project_id - name = "prod-resman-net-0" - description = "Terraform resman networking service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "prod-resman-net-0" + display_name = "Terraform resman networking service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-network-sa-cicd.0.iam_email, null) diff --git a/fast/stages/01-resman/branch-project-factory.tf b/fast/stages/01-resman/branch-project-factory.tf index 356fb7b616..41651a28c3 100644 --- a/fast/stages/01-resman/branch-project-factory.tf +++ b/fast/stages/01-resman/branch-project-factory.tf @@ -22,8 +22,8 @@ module "branch-pf-dev-sa" { project_id = var.automation.project_id name = "dev-resman-pf-0" # naming: environment in description - description = "Terraform project factory development service account." - prefix = var.prefix + display_name = "Terraform project factory development service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-pf-dev-sa-cicd.0.iam_email, null) @@ -40,8 +40,8 @@ module "branch-pf-prod-sa" { project_id = var.automation.project_id name = "prod-resman-pf-0" # naming: environment in description - description = "Terraform project factory production service account." - prefix = var.prefix + display_name = "Terraform project factory production service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-pf-prod-sa-cicd.0.iam_email, null) diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/01-resman/branch-sandbox.tf index 84995c15bd..8b54e749a9 100644 --- a/fast/stages/01-resman/branch-sandbox.tf +++ b/fast/stages/01-resman/branch-sandbox.tf @@ -68,10 +68,10 @@ moved { } module "branch-sandbox-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.sandbox ? 1 : 0 - project_id = var.automation.project_id - name = "dev-resman-sbox-0" - description = "Terraform resman sandbox service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.sandbox ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-sbox-0" + display_name = "Terraform resman sandbox service account." + prefix = var.prefix } diff --git a/fast/stages/01-resman/branch-security.tf b/fast/stages/01-resman/branch-security.tf index c30269f857..c7b4fc9708 100644 --- a/fast/stages/01-resman/branch-security.tf +++ b/fast/stages/01-resman/branch-security.tf @@ -49,11 +49,11 @@ module "branch-security-folder" { # automation service account and bucket module "branch-security-sa" { - source = "../../../modules/iam-service-account" - project_id = var.automation.project_id - name = "prod-resman-sec-0" - description = "Terraform resman security service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "prod-resman-sec-0" + display_name = "Terraform resman security service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.branch-security-sa-cicd.0.iam_email, null) diff --git a/fast/stages/01-resman/branch-teams.tf b/fast/stages/01-resman/branch-teams.tf index e054bb3c5f..8b0e89b3aa 100644 --- a/fast/stages/01-resman/branch-teams.tf +++ b/fast/stages/01-resman/branch-teams.tf @@ -40,12 +40,12 @@ module "branch-teams-folder" { } module "branch-teams-sa" { - source = "../../../modules/iam-service-account" - count = var.fast_features.teams ? 1 : 0 - project_id = var.automation.project_id - name = "prod-resman-teams-0" - description = "Terraform resman teams service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + count = var.fast_features.teams ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-teams-0" + display_name = "Terraform resman teams service account." + prefix = var.prefix iam_storage_roles = { (var.automation.outputs_bucket) = ["roles/storage.admin"] } @@ -83,12 +83,12 @@ module "branch-teams-team-folder" { } module "branch-teams-team-sa" { - source = "../../../modules/iam-service-account" - for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} - project_id = var.automation.project_id - name = "prod-teams-${each.key}-0" - description = "Terraform team ${each.key} service account." - prefix = var.prefix + source = "../../../modules/iam-service-account" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + project_id = var.automation.project_id + name = "prod-teams-${each.key}-0" + display_name = "Terraform team ${each.key} service account." + prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = ( each.value.impersonation_groups == null diff --git a/fast/stages/01-resman/cicd-data-platform.tf b/fast/stages/01-resman/cicd-data-platform.tf index 6cd1211496..5b07883c44 100644 --- a/fast/stages/01-resman/cicd-data-platform.tf +++ b/fast/stages/01-resman/cicd-data-platform.tf @@ -95,10 +95,10 @@ module "branch-dp-dev-sa-cicd" { ? { 0 = local.cicd_repositories.data_platform_dev } : {} ) - project_id = var.automation.project_id - name = "dev-resman-dp-1" - description = "Terraform CI/CD data platform development service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "dev-resman-dp-1" + display_name = "Terraform CI/CD data platform development service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos @@ -138,10 +138,10 @@ module "branch-dp-prod-sa-cicd" { ? { 0 = local.cicd_repositories.data_platform_prod } : {} ) - project_id = var.automation.project_id - name = "prod-resman-dp-1" - description = "Terraform CI/CD data platform production service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "prod-resman-dp-1" + display_name = "Terraform CI/CD data platform production service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/01-resman/cicd-gke.tf b/fast/stages/01-resman/cicd-gke.tf index 65a04a630c..fa4f8767ca 100644 --- a/fast/stages/01-resman/cicd-gke.tf +++ b/fast/stages/01-resman/cicd-gke.tf @@ -95,10 +95,10 @@ module "branch-gke-dev-sa-cicd" { ? { 0 = local.cicd_repositories.gke_dev } : {} ) - project_id = var.automation.project_id - name = "dev-resman-gke-1" - description = "Terraform CI/CD GKE development service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "dev-resman-gke-1" + display_name = "Terraform CI/CD GKE development service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos @@ -138,10 +138,10 @@ module "branch-gke-prod-sa-cicd" { ? { 0 = local.cicd_repositories.gke_prod } : {} ) - project_id = var.automation.project_id - name = "prod-resman-gke-1" - description = "Terraform CI/CD GKE production service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "prod-resman-gke-1" + display_name = "Terraform CI/CD GKE production service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/01-resman/cicd-networking.tf b/fast/stages/01-resman/cicd-networking.tf index 9517704755..894348ff3b 100644 --- a/fast/stages/01-resman/cicd-networking.tf +++ b/fast/stages/01-resman/cicd-networking.tf @@ -57,10 +57,10 @@ module "branch-network-sa-cicd" { ? { 0 = local.cicd_repositories.networking } : {} ) - project_id = var.automation.project_id - name = "prod-resman-net-1" - description = "Terraform CI/CD stage 2 networking service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "prod-resman-net-1" + display_name = "Terraform CI/CD stage 2 networking service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/01-resman/cicd-project-factory.tf b/fast/stages/01-resman/cicd-project-factory.tf index 2c9f0972b3..8f357ce6c0 100644 --- a/fast/stages/01-resman/cicd-project-factory.tf +++ b/fast/stages/01-resman/cicd-project-factory.tf @@ -106,10 +106,10 @@ module "branch-pf-dev-sa-cicd" { ? { 0 = local.cicd_repositories.project_factory_dev } : {} ) - project_id = var.automation.project_id - name = "dev-pf-resman-pf-1" - description = "Terraform CI/CD project factory development service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "dev-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory development service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos @@ -154,10 +154,10 @@ module "branch-pf-prod-sa-cicd" { ? { 0 = local.cicd_repositories.project_factory_prod } : {} ) - project_id = var.automation.project_id - name = "prod-pf-resman-pf-1" - description = "Terraform CI/CD project factory production service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "prod-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory production service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/01-resman/cicd-security.tf b/fast/stages/01-resman/cicd-security.tf index 86fd84fdb1..dd27a47331 100644 --- a/fast/stages/01-resman/cicd-security.tf +++ b/fast/stages/01-resman/cicd-security.tf @@ -57,10 +57,10 @@ module "branch-security-sa-cicd" { ? { 0 = local.cicd_repositories.security } : {} ) - project_id = var.automation.project_id - name = "prod-resman-sec-1" - description = "Terraform CI/CD stage 2 security service account." - prefix = var.prefix + project_id = var.automation.project_id + name = "prod-resman-sec-1" + display_name = "Terraform CI/CD stage 2 security service account." + prefix = var.prefix iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos diff --git a/fast/stages/01-resman/data/org-policies/compute.yaml b/fast/stages/01-resman/data/org-policies/compute.yaml new file mode 100644 index 0000000000..0d27ac426d --- /dev/null +++ b/fast/stages/01-resman/data/org-policies/compute.yaml @@ -0,0 +1,73 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +compute.disableGuestAttributesAccess: + enforce: true + +compute.requireOsLogin: + enforce: true + +compute.restrictLoadBalancerCreationForTypes: + allow: + values: + - in:INTERNAL + +compute.skipDefaultNetworkCreation: + enforce: true + +compute.vmExternalIpAccess: + deny: + all: true + + +# compute.disableInternetNetworkEndpointGroup: +# enforce: true + +# compute.disableNestedVirtualization: +# enforce: true + +# compute.disableSerialPortAccess: +# enforce: true + +# compute.restrictCloudNATUsage: +# deny: +# all: true + +# compute.restrictDedicatedInterconnectUsage: +# deny: +# all: true + +# compute.restrictPartnerInterconnectUsage: +# deny: +# all: true + +# compute.restrictProtocolForwardingCreationForTypes: +# deny: +# all: true + +# compute.restrictSharedVpcHostProjects: +# deny: +# all: true + +# compute.restrictSharedVpcSubnetworks: +# deny: +# all: true + +# compute.restrictVpcPeering: +# deny: +# all: true + +# compute.restrictVpnPeerIPs: +# deny: +# all: true + +# compute.restrictXpnProjectLienRemoval: +# enforce: true + +# compute.setNewProjectDefaultToZonalDNSOnly: +# enforce: true + +# compute.vmCanIpForward: +# deny: +# all: true diff --git a/fast/stages/01-resman/data/org-policies/iam.yaml b/fast/stages/01-resman/data/org-policies/iam.yaml new file mode 100644 index 0000000000..4d83f827fe --- /dev/null +++ b/fast/stages/01-resman/data/org-policies/iam.yaml @@ -0,0 +1,12 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +iam.automaticIamGrantsForDefaultServiceAccounts: + enforce: true + +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: true diff --git a/fast/stages/01-resman/data/org-policies/serverless.yaml b/fast/stages/01-resman/data/org-policies/serverless.yaml new file mode 100644 index 0000000000..de62e6c702 --- /dev/null +++ b/fast/stages/01-resman/data/org-policies/serverless.yaml @@ -0,0 +1,26 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +run.allowedIngress: + allow: + values: + - is:internal + +# run.allowedVPCEgress: +# allow: +# values: +# - is:private-ranges-only + +# cloudfunctions.allowedIngressSettings: +# allow: +# values: +# - is:ALLOW_INTERNAL_ONLY + +# cloudfunctions.allowedVpcConnectorEgressSettings: +# allow: +# values: +# - is:PRIVATE_RANGES_ONLY + +# cloudfunctions.requireVPCConnector: +# enforce: true diff --git a/fast/stages/01-resman/data/org-policies/sql.yaml b/fast/stages/01-resman/data/org-policies/sql.yaml new file mode 100644 index 0000000000..88b84d9d50 --- /dev/null +++ b/fast/stages/01-resman/data/org-policies/sql.yaml @@ -0,0 +1,9 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +sql.restrictAuthorizedNetworks: + enforce: true + +sql.restrictPublicIp: + enforce: true diff --git a/fast/stages/01-resman/data/org-policies/storage.yaml b/fast/stages/01-resman/data/org-policies/storage.yaml new file mode 100644 index 0000000000..6c0a673f3a --- /dev/null +++ b/fast/stages/01-resman/data/org-policies/storage.yaml @@ -0,0 +1,6 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +storage.uniformBucketLevelAccess: + enforce: true diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/01-resman/organization.tf index 40a789eecd..7ecf795232 100644 --- a/fast/stages/01-resman/organization.tf +++ b/fast/stages/01-resman/organization.tf @@ -66,44 +66,12 @@ module "organization" { ) } : {} ) - # sample subset of useful organization policies, edit to suit requirements + # sample subset of useful organization policies, edit to suit requirements org_policies = { - "compute.disableGuestAttributesAccess" = { enforce = true } - "compute.requireOsLogin" = { enforce = true } - "compute.restrictLoadBalancerCreationForTypes" = { allow = { values = ["in:INTERNAL"] } } - "compute.skipDefaultNetworkCreation" = { enforce = true } - "compute.vmExternalIpAccess" = { deny = { all = true } } - "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } } - "iam.automaticIamGrantsForDefaultServiceAccounts" = { enforce = true } - "iam.disableServiceAccountKeyCreation" = { enforce = true } - "iam.disableServiceAccountKeyUpload" = { enforce = true } - "run.allowedIngress" = { allow = { values = ["is:internal"] } } - "sql.restrictAuthorizedNetworks" = { enforce = true } - "sql.restrictPublicIp" = { enforce = true } - "storage.uniformBucketLevelAccess" = { enforce = true } - # "cloudfunctions.allowedIngressSettings" = { - # allow = { values = ["is:ALLOW_INTERNAL_ONLY"] } - # } - # "cloudfunctions.allowedVpcConnectorEgressSettings" = { - # allow = { values = ["is:PRIVATE_RANGES_ONLY"] } - # } - # "cloudfunctions.requireVPCConnector" = { enforce = true } - # "compute.disableInternetNetworkEndpointGroup" = { enforce = true } - # "compute.disableNestedVirtualization" = { enforce = true } - # "compute.disableSerialPortAccess" = { enforce = true } - # "compute.restrictCloudNATUsage" = { deny = { all = true }} - # "compute.restrictDedicatedInterconnectUsage" = { deny = { all = true }} - # "compute.restrictPartnerInterconnectUsage" = { deny = { all = true }} - # "compute.restrictProtocolForwardingCreationForTypes" = { deny = { all = true }} - # "compute.restrictSharedVpcHostProjects" = { deny = { all = true }} - # "compute.restrictSharedVpcSubnetworks" = { deny = { all = true }} - # "compute.restrictVpcPeering" = { deny = { all = true }} - # "compute.restrictVpnPeerIPs" = { deny = { all = true }} - # "compute.restrictXpnProjectLienRemoval" = { enforce = true } - # "compute.setNewProjectDefaultToZonalDNSOnly" = { enforce = true } - # "compute.vmCanIpForward" = { deny = { all = true }} - # "gcp.resourceLocations" = { + "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } } + + #"gcp.resourceLocations" = { # allow = { values = local.allowed_regions } # } # "iam.workloadIdentityPoolProviders" = { @@ -114,8 +82,9 @@ module "organization" { # ] # } # } - # "run.allowedVPCEgress" = { allow = { values = ["is:private-ranges-only"] } } } + org_policies_data_path = "${var.data_dir}/org-policies" + tags = { (var.tag_names.context) = { description = "Resource management context." diff --git a/fast/stages/01-resman/variables.tf b/fast/stages/01-resman/variables.tf index 8da869670d..6de9a7fad3 100644 --- a/fast/stages/01-resman/variables.tf +++ b/fast/stages/01-resman/variables.tf @@ -135,6 +135,12 @@ variable "custom_roles" { default = null } +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data" +} + variable "fast_features" { # tfdoc:variable:source 00-bootstrap description = "Selective control for top-level FAST features." diff --git a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml index 3e2d9cc9d6..cab42edc94 100644 --- a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml +++ b/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml @@ -1,27 +1,21 @@ # skip boilerplate check -ingress-allow-composer-nodes: - description: "Allow traffic to Composer nodes." - direction: INGRESS - action: allow - sources: - - composer-worker - targets: - - composer-worker - use_service_accounts: false - rules: - - protocol: tcp - ports: [80, 443, 3306, 3307] - -ingress-allow-dataflow-load: - description: "Allow traffic to Dataflow nodes." - direction: INGRESS - action: allow - sources: - - dataflow - targets: - - dataflow - use_service_accounts: false - rules: - - protocol: tcp - ports: [12345, 12346] +ingress: + ingress-allow-composer-nodes: + description: "Allow traffic to Composer nodes." + sources: + - composer-worker + targets: + - composer-worker + rules: + - protocol: tcp + ports: [80, 443, 3306, 3307] + ingress-allow-dataflow-load: + description: "Allow traffic to Dataflow nodes." + sources: + - dataflow + targets: + - dataflow + rules: + - protocol: tcp + ports: [12345, 12346] diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml index 672af07f71..1405170fb5 100644 --- a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml +++ b/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml @@ -1,29 +1,19 @@ # skip boilerplate check -allow-hc-nva-ssh-trusted: - description: "Allow traffic from Google healthchecks to NVA appliances" - direction: INGRESS - action: allow - sources: [] - ranges: - - $healthchecks - targets: [] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 22 - -allow-onprem-probes-trusted-example: - description: "Allow traffic from onprem probes" - direction: INGRESS - action: allow - sources: [] - ranges: - - $onprem_probes - targets: [] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 12345 +ingress: + allow-hc-nva-ssh-trusted: + description: "Allow traffic from Google healthchecks to NVA appliances" + source_ranges: + - healthchecks + rules: + - protocol: tcp + ports: + - 22 + allow-onprem-probes-trusted-example: + description: "Allow traffic from onprem probes" + source_ranges: + - onprem_probes + rules: + - protocol: tcp + ports: + - 12345 diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml index 15db503ba6..aa51c0fe80 100644 --- a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml +++ b/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml @@ -1,15 +1,11 @@ # skip boilerplate check -allow-hc-nva-ssh-untrusted: - description: "Allow traffic from Google healthchecks to NVA appliances" - direction: INGRESS - action: allow - sources: [] - ranges: - - $healthchecks - targets: [] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 22 +ingress: + allow-hc-nva-ssh-untrusted: + description: "Allow traffic from Google healthchecks to NVA appliances" + source_ranges: + - healthchecks + rules: + - protocol: tcp + ports: + - 22 diff --git a/fast/stages/02-networking-nva/landing.tf b/fast/stages/02-networking-nva/landing.tf index be051b766f..5a990030f0 100644 --- a/fast/stages/02-networking-nva/landing.tf +++ b/fast/stages/02-networking-nva/landing.tf @@ -57,15 +57,16 @@ module "landing-untrusted-vpc" { } module "landing-untrusted-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.landing-project.project_id - network = module.landing-untrusted-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/landing-untrusted" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-untrusted-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/landing-untrusted" + } } # NAT @@ -123,13 +124,14 @@ module "landing-trusted-vpc" { } module "landing-trusted-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.landing-project.project_id - network = module.landing-trusted-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/landing-trusted" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-trusted-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/landing-trusted" + } } diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/02-networking-nva/nva.tf index d0afbd7253..f4f7b9e5ee 100644 --- a/fast/stages/02-networking-nva/nva.tf +++ b/fast/stages/02-networking-nva/nva.tf @@ -120,16 +120,20 @@ module "ilb-nva-untrusted" { name = "nva-untrusted-${each.value.0}" service_label = var.prefix global_access = true - network = module.landing-untrusted-vpc.self_link - subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.key}/landing-untrusted-default-${each.value.0}"] - backends = [for key, _ in local.nva_locality : - { - failover = false - group = module.nva-mig[key].group_manager.instance_group - balancing_mode = "CONNECTION" - } if local.nva_locality[key].region == each.key] + vpc_config = { + network = module.landing-untrusted-vpc.self_link + subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.key}/landing-untrusted-default-${each.value.0}"] + } + backends = [ + for key, _ in local.nva_locality : { + group = module.nva-mig[key].group_manager.instance_group + } if local.nva_locality[key].region == each.key + ] health_check_config = { - type = "tcp", check = { port = 22 }, config = {}, logging = false + enable_logging = true + tcp = { + port = 22 + } } } @@ -142,16 +146,20 @@ module "ilb-nva-trusted" { name = "nva-trusted-${each.value.0}" service_label = var.prefix global_access = true - network = module.landing-trusted-vpc.self_link - subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.key}/landing-trusted-default-${each.value.0}"] - backends = [for key, _ in local.nva_locality : - { - failover = false - group = module.nva-mig[key].group_manager.instance_group - balancing_mode = "CONNECTION" - } if local.nva_locality[key].region == each.key] + vpc_config = { + network = module.landing-trusted-vpc.self_link + subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.key}/landing-trusted-default-${each.value.0}"] + } + backends = [ + for key, _ in local.nva_locality : { + group = module.nva-mig[key].group_manager.instance_group + } if local.nva_locality[key].region == each.key + ] health_check_config = { - type = "tcp", check = { port = 22 }, config = {}, logging = false + enable_logging = true + tcp = { + port = 22 + } } } diff --git a/fast/stages/02-networking-nva/spoke-dev.tf b/fast/stages/02-networking-nva/spoke-dev.tf index 3c6843e2a8..fb88384c49 100644 --- a/fast/stages/02-networking-nva/spoke-dev.tf +++ b/fast/stages/02-networking-nva/spoke-dev.tf @@ -97,15 +97,16 @@ module "dev-spoke-vpc" { } module "dev-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/dev" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/dev" + } } module "peering-dev" { diff --git a/fast/stages/02-networking-nva/spoke-prod.tf b/fast/stages/02-networking-nva/spoke-prod.tf index 5d35ca8fee..484550acac 100644 --- a/fast/stages/02-networking-nva/spoke-prod.tf +++ b/fast/stages/02-networking-nva/spoke-prod.tf @@ -97,15 +97,16 @@ module "prod-spoke-vpc" { } module "prod-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/prod" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/prod" + } } module "peering-prod" { diff --git a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml index 3e2d9cc9d6..cab42edc94 100644 --- a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml +++ b/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml @@ -1,27 +1,21 @@ # skip boilerplate check -ingress-allow-composer-nodes: - description: "Allow traffic to Composer nodes." - direction: INGRESS - action: allow - sources: - - composer-worker - targets: - - composer-worker - use_service_accounts: false - rules: - - protocol: tcp - ports: [80, 443, 3306, 3307] - -ingress-allow-dataflow-load: - description: "Allow traffic to Dataflow nodes." - direction: INGRESS - action: allow - sources: - - dataflow - targets: - - dataflow - use_service_accounts: false - rules: - - protocol: tcp - ports: [12345, 12346] +ingress: + ingress-allow-composer-nodes: + description: "Allow traffic to Composer nodes." + sources: + - composer-worker + targets: + - composer-worker + rules: + - protocol: tcp + ports: [80, 443, 3306, 3307] + ingress-allow-dataflow-load: + description: "Allow traffic to Dataflow nodes." + sources: + - dataflow + targets: + - dataflow + rules: + - protocol: tcp + ports: [12345, 12346] diff --git a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml b/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml index e72b7c9c7d..3c1425a7c0 100644 --- a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml +++ b/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml @@ -1,15 +1,11 @@ # skip boilerplate check -allow-onprem-probes-example: - description: "Allow traffic from onprem probes" - direction: INGRESS - action: allow - sources: [] - ranges: - - $onprem_probes - targets: [] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 12345 +ingress: + allow-onprem-probes-example: + description: "Allow traffic from onprem probes" + source_ranges: + - onprem_probes + rules: + - protocol: tcp + ports: + - 12345 diff --git a/fast/stages/02-networking-peering/landing.tf b/fast/stages/02-networking-peering/landing.tf index ccd0d6254c..83a0d509af 100644 --- a/fast/stages/02-networking-peering/landing.tf +++ b/fast/stages/02-networking-peering/landing.tf @@ -67,15 +67,16 @@ module "landing-vpc" { } module "landing-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.landing-project.project_id - network = module.landing-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/landing" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/landing" + } } module "landing-nat-ew1" { diff --git a/fast/stages/02-networking-peering/spoke-dev.tf b/fast/stages/02-networking-peering/spoke-dev.tf index 9fe6c480f2..e67cfb70db 100644 --- a/fast/stages/02-networking-peering/spoke-dev.tf +++ b/fast/stages/02-networking-peering/spoke-dev.tf @@ -67,15 +67,16 @@ module "dev-spoke-vpc" { } module "dev-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/dev" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/dev" + } } module "dev-spoke-cloudnat" { diff --git a/fast/stages/02-networking-peering/spoke-prod.tf b/fast/stages/02-networking-peering/spoke-prod.tf index 8ff69a98cb..cf49152fa1 100644 --- a/fast/stages/02-networking-peering/spoke-prod.tf +++ b/fast/stages/02-networking-peering/spoke-prod.tf @@ -67,15 +67,16 @@ module "prod-spoke-vpc" { } module "prod-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/prod" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/prod" + } } module "prod-spoke-cloudnat" { diff --git a/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml index d4df8cdc31..67386c4467 100644 --- a/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml +++ b/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml @@ -1,27 +1,17 @@ # skip boilerplate check -ingress-allow-composer-nodes: - description: "Allow traffic to Composer nodes." - direction: INGRESS - action: allow - sources: [] - ranges: ["0.0.0.0/0"] - targets: - - composer-worker - use_service_accounts: false - rules: - - protocol: tcp - ports: [80, 443, 3306, 3307] - -ingress-allow-dataflow-load: - description: "Allow traffic to Dataflow nodes." - direction: INGRESS - action: allow - sources: [] - ranges: ["0.0.0.0/0"] - targets: - - dataflow - use_service_accounts: false - rules: - - protocol: tcp - ports: [12345, 12346] +ingress: + ingress-allow-composer-nodes: + description: "Allow traffic to Composer nodes." + targets: + - composer-worker + rules: + - protocol: tcp + ports: [80, 443, 3306, 3307] + ingress-allow-dataflow-load: + description: "Allow traffic to Dataflow nodes." + targets: + - dataflow + rules: + - protocol: tcp + ports: [12345, 12346] diff --git a/fast/stages/02-networking-separate-envs/spoke-dev.tf b/fast/stages/02-networking-separate-envs/spoke-dev.tf index 08eac23053..ca7d8d4686 100644 --- a/fast/stages/02-networking-separate-envs/spoke-dev.tf +++ b/fast/stages/02-networking-separate-envs/spoke-dev.tf @@ -66,15 +66,16 @@ module "dev-spoke-vpc" { } module "dev-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/dev" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/dev" + } } module "dev-spoke-cloudnat" { diff --git a/fast/stages/02-networking-separate-envs/spoke-prod.tf b/fast/stages/02-networking-separate-envs/spoke-prod.tf index 4261f1ce0f..eba530a6c4 100644 --- a/fast/stages/02-networking-separate-envs/spoke-prod.tf +++ b/fast/stages/02-networking-separate-envs/spoke-prod.tf @@ -66,15 +66,16 @@ module "prod-spoke-vpc" { } module "prod-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/prod" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/prod" + } } module "prod-spoke-cloudnat" { diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml index 3e2d9cc9d6..cab42edc94 100644 --- a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml +++ b/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml @@ -1,27 +1,21 @@ # skip boilerplate check -ingress-allow-composer-nodes: - description: "Allow traffic to Composer nodes." - direction: INGRESS - action: allow - sources: - - composer-worker - targets: - - composer-worker - use_service_accounts: false - rules: - - protocol: tcp - ports: [80, 443, 3306, 3307] - -ingress-allow-dataflow-load: - description: "Allow traffic to Dataflow nodes." - direction: INGRESS - action: allow - sources: - - dataflow - targets: - - dataflow - use_service_accounts: false - rules: - - protocol: tcp - ports: [12345, 12346] +ingress: + ingress-allow-composer-nodes: + description: "Allow traffic to Composer nodes." + sources: + - composer-worker + targets: + - composer-worker + rules: + - protocol: tcp + ports: [80, 443, 3306, 3307] + ingress-allow-dataflow-load: + description: "Allow traffic to Dataflow nodes." + sources: + - dataflow + targets: + - dataflow + rules: + - protocol: tcp + ports: [12345, 12346] diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml b/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml index e72b7c9c7d..3c1425a7c0 100644 --- a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml +++ b/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml @@ -1,15 +1,11 @@ # skip boilerplate check -allow-onprem-probes-example: - description: "Allow traffic from onprem probes" - direction: INGRESS - action: allow - sources: [] - ranges: - - $onprem_probes - targets: [] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 12345 +ingress: + allow-onprem-probes-example: + description: "Allow traffic from onprem probes" + source_ranges: + - onprem_probes + rules: + - protocol: tcp + ports: + - 12345 diff --git a/fast/stages/02-networking-vpn/landing.tf b/fast/stages/02-networking-vpn/landing.tf index ccd0d6254c..83a0d509af 100644 --- a/fast/stages/02-networking-vpn/landing.tf +++ b/fast/stages/02-networking-vpn/landing.tf @@ -67,15 +67,16 @@ module "landing-vpc" { } module "landing-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.landing-project.project_id - network = module.landing-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/landing" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/landing" + } } module "landing-nat-ew1" { diff --git a/fast/stages/02-networking-vpn/spoke-dev.tf b/fast/stages/02-networking-vpn/spoke-dev.tf index 9fe6c480f2..e67cfb70db 100644 --- a/fast/stages/02-networking-vpn/spoke-dev.tf +++ b/fast/stages/02-networking-vpn/spoke-dev.tf @@ -67,15 +67,16 @@ module "dev-spoke-vpc" { } module "dev-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/dev" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/dev" + } } module "dev-spoke-cloudnat" { diff --git a/fast/stages/02-networking-vpn/spoke-prod.tf b/fast/stages/02-networking-vpn/spoke-prod.tf index 8ff69a98cb..cf49152fa1 100644 --- a/fast/stages/02-networking-vpn/spoke-prod.tf +++ b/fast/stages/02-networking-vpn/spoke-prod.tf @@ -67,15 +67,16 @@ module "prod-spoke-vpc" { } module "prod-spoke-firewall" { - source = "../../../modules/net-vpc-firewall" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.name - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - data_folder = "${var.data_dir}/firewall-rules/prod" - cidr_template_file = "${var.data_dir}/cidrs.yaml" + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.data_dir}/cidrs.yaml" + rules_folder = "${var.data_dir}/firewall-rules/prod" + } } module "prod-spoke-cloudnat" { diff --git a/modules/README.md b/modules/README.md index 129a8b8f98..9995c4ebec 100644 --- a/modules/README.md +++ b/modules/README.md @@ -36,7 +36,6 @@ These modules are used in the examples included in this repository. If you are u - [service accounts](./iam-service-account) - [logging bucket](./logging-bucket) - [organization](./organization) -- [organization-policy](./organization-policy) - [project](./project) - [projects-data-source](./projects-data-source) diff --git a/modules/cloud-function/main.tf b/modules/cloud-function/main.tf index 1ad64f46ee..0a42490480 100644 --- a/modules/cloud-function/main.tf +++ b/modules/cloud-function/main.tf @@ -141,7 +141,7 @@ resource "google_cloudfunctions2_function" "function" { environment_variables = var.environment_variables source { storage_source { - bucket = google_storage_bucket.bucket[0].name + bucket = local.bucket object = google_storage_bucket_object.bundle.name } } diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 4e5b5a45d7..895f051799 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -417,25 +417,25 @@ module "nginx-mig" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [instance_template](variables.tf#L150) | Instance template for the default version. | string | ✓ | | -| [location](variables.tf#L155) | Compute zone or region. | string | ✓ | | -| [name](variables.tf#L160) | Managed group name. | string | ✓ | | -| [project_id](variables.tf#L171) | Project id. | string | ✓ | | +| [instance_template](variables.tf#L174) | Instance template for the default version. | string | ✓ | | +| [location](variables.tf#L179) | Compute zone or region. | string | ✓ | | +| [name](variables.tf#L184) | Managed group name. | string | ✓ | | +| [project_id](variables.tf#L195) | Project id. | string | ✓ | | | [all_instances_config](variables.tf#L17) | Metadata and labels set to all instances in the group. | object({…}) | | null | | [auto_healing_policies](variables.tf#L26) | Auto-healing policies for this group. | object({…}) | | null | | [autoscaler_config](variables.tf#L35) | Optional autoscaler configuration. | object({…}) | | null | | [default_version_name](variables.tf#L83) | Name used for the default version. | string | | "default" | | [description](variables.tf#L89) | Optional description used for all resources managed by this module. | string | | "Terraform managed." | | [distribution_policy](variables.tf#L95) | DIstribution policy for regional MIG. | object({…}) | | null | -| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | -| [named_ports](variables.tf#L165) | Named ports. | map(number) | | null | -| [stateful_config](variables.tf#L183) | Stateful configuration for individual instances. | map(object({…})) | | {} | -| [stateful_disks](variables.tf#L176) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | -| [target_pools](variables.tf#L202) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | -| [target_size](variables.tf#L208) | Group target size, leave null when using an autoscaler. | number | | null | -| [update_policy](variables.tf#L214) | Update policy. Minimal action and type are required. | object({…}) | | null | -| [versions](variables.tf#L235) | Additional application versions, target_size is optional. | map(object({…})) | | {} | -| [wait_for_instances](variables.tf#L248) | Wait for all instances to be created/updated before returning. | object({…}) | | null | +| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | +| [named_ports](variables.tf#L189) | Named ports. | map(number) | | null | +| [stateful_config](variables.tf#L207) | Stateful configuration for individual instances. | map(object({…})) | | {} | +| [stateful_disks](variables.tf#L200) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | +| [target_pools](variables.tf#L226) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | +| [target_size](variables.tf#L232) | Group target size, leave null when using an autoscaler. | number | | null | +| [update_policy](variables.tf#L238) | Update policy. Minimal action and type are required. | object({…}) | | null | +| [versions](variables.tf#L259) | Additional application versions, target_size is optional. | map(object({…})) | | {} | +| [wait_for_instances](variables.tf#L272) | Wait for all instances to be created/updated before returning. | object({…}) | | null | ## Outputs diff --git a/modules/compute-mig/health-check.tf b/modules/compute-mig/health-check.tf index 9a5d8d1f6b..4a4ed40def 100644 --- a/modules/compute-mig/health-check.tf +++ b/modules/compute-mig/health-check.tf @@ -17,25 +17,16 @@ # tfdoc:file:description Health check resource. locals { - hc = var.health_check_config - hc_grpc = try(local.hc.grpc, null) != null - hc_http = ( - try(local.hc.http, null) != null && - lower(try(local.hc.http.use_protocol, "")) == "http" - ) - hc_http2 = ( - try(local.hc.http, null) != null && - lower(try(local.hc.http.use_protocol, "")) == "http2" - ) - hc_https = ( - try(local.hc.http, null) != null && - lower(try(local.hc.http.use_protocol, "")) == "https" - ) - hc_ssl = try(local.hc.tcp.use_ssl, null) == true - hc_tcp = try(local.hc.tcp, null) != null && !local.hc_ssl + hc = var.health_check_config + hc_grpc = try(local.hc.grpc, null) != null + hc_http = try(local.hc.http, null) != null + hc_http2 = try(local.hc.http2, null) != null + hc_https = try(local.hc.https, null) != null + hc_ssl = try(local.hc.ssl, null) != null + hc_tcp = try(local.hc.tcp, null) != null } -resource "google_compute_health_check" "autohealing" { +resource "google_compute_health_check" "default" { provider = google-beta count = local.hc != null ? 1 : 0 project = var.project_id diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf index 927389952e..35f255a682 100644 --- a/modules/compute-mig/main.tf +++ b/modules/compute-mig/main.tf @@ -17,7 +17,7 @@ locals { health_check = ( try(var.auto_healing_policies.health_check, null) == null - ? try(google_compute_health_check.autohealing.0.self_link, null) + ? try(google_compute_health_check.default.0.self_link, null) : try(var.auto_healing_policies.health_check, null) ) instance_group_manager = ( diff --git a/modules/compute-mig/outputs.tf b/modules/compute-mig/outputs.tf index a7be7d2ebb..41b20c1fa2 100644 --- a/modules/compute-mig/outputs.tf +++ b/modules/compute-mig/outputs.tf @@ -37,6 +37,6 @@ output "health_check" { value = ( var.health_check_config == null ? null - : google_compute_health_check.autohealing.0 + : google_compute_health_check.default.0 ) } diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index 299dacc889..056bd198ee 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -124,7 +124,24 @@ variable "health_check_config" { proxy_header = optional(string) request_path = optional(string) response = optional(string) - use_protocol = optional(string, "http") # http http2 https + })) + http2 = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + })) + https = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) })) tcp = optional(object({ port = optional(number) @@ -133,7 +150,14 @@ variable "health_check_config" { proxy_header = optional(string) request = optional(string) response = optional(string) - use_ssl = optional(bool, false) + })) + ssl = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request = optional(string) + response = optional(string) })) }) default = null diff --git a/modules/folder/README.md b/modules/folder/README.md index d543004a25..2190eaacc9 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -75,6 +75,10 @@ module "folder" { # tftest modules=1 resources=8 ``` +### Organization policy factory + +See the [organization policy factory in the project module](../project#organization-policy-factory). + ### Firewall policy factory In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). @@ -311,8 +315,9 @@ module "folder" { | [logging_sinks](variables.tf#L105) | Logging sinks to create for this folder. | map(object({…})) | | {} | | [name](variables.tf#L126) | Folder name. | string | | null | | [org_policies](variables.tf#L132) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [parent](variables.tf#L172) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L182) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [org_policies_data_path](variables.tf#L172) | Path containing org policies in YAML format. | string | | null | +| [parent](variables.tf#L178) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L188) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index da47806390..3766005ae5 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -17,9 +17,61 @@ # tfdoc:file:description Folder-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { + name = "${local.folder.name}/policies/${k}" + parent = local.folder.name + is_boolean_policy = v.allow == null && v.deny == null has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || @@ -40,8 +92,8 @@ locals { resource "google_org_policy_policy" "default" { for_each = local.org_policies - name = "${local.folder.name}/policies/${each.key}" - parent = local.folder.name + name = each.value.name + parent = each.value.parent spec { inherit_from_parent = each.value.inherit_from_parent diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index a00e147f65..359531b7d0 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -169,6 +169,12 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "Path containing org policies in YAML format." + type = string + default = null +} + variable "parent" { description = "Parent in folders/folder_id or organizations/org_id format." type = string diff --git a/modules/gke-hub/README.md b/modules/gke-hub/README.md index 1a3c547c60..17069180a9 100644 --- a/modules/gke-hub/README.md +++ b/modules/gke-hub/README.md @@ -178,50 +178,28 @@ module "firewall" { source = "./fabric/modules/net-vpc-firewall" project_id = module.project.project_id network = module.vpc.name - custom_rules = { + ingress_rules = { allow-mesh = { - description = "Allow mesh" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["10.1.0.0/16", "10.3.0.0/16"] - targets = ["cluster-1-node", "cluster-2-node"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = null }, - { protocol = "udp", ports = null }, - { protocol = "icmp", ports = null }, - { protocol = "esp", ports = null }, - { protocol = "ah", ports = null }, - { protocol = "sctp", ports = null }] - extra_attributes = { - priority = 900 - } - }, + description = "Allow mesh" + priority = 900 + source_ranges = ["10.1.0.0/16", "10.3.0.0/16"] + targets = ["cluster-1-node", "cluster-2-node"] + }, "allow-cluster-1-istio" = { - description = "Allow istio sidecar injection, istioctl version and istioctl ps" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [ "192.168.1.0/28" ] - targets = ["cluster-1-node"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [8080, 15014, 15017] }] - extra_attributes = { - priority = 1000 - } + description = "Allow istio sidecar injection, istioctl version and istioctl ps" + source_ranges = ["192.168.1.0/28"] + targets = ["cluster-1-node"] + rules = [ + { protocol = "tcp", ports = [8080, 15014, 15017] } + ] }, "allow-cluster-2-istio" = { - description = "Allow istio sidecar injection, istioctl version and istioctl ps" - direction = "INGRESS" - action = "allow" - sources = [] - ranges = [ "192.168.2.0/28" ] - targets = ["cluster-2-node"] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [8080, 15014, 15017] }] - extra_attributes = { - priority = 1000 - } + description = "Allow istio sidecar injection, istioctl version and istioctl ps" + source_ranges = ["192.168.2.0/28"] + targets = ["cluster-2-node"] + rules = [ + { protocol = "tcp", ports = [8080, 15014, 15017] } + ] } } } diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md index eb5ab95be3..688506855f 100644 --- a/modules/iam-service-account/README.md +++ b/modules/iam-service-account/README.md @@ -43,21 +43,22 @@ module "myproject-default-service-accounts" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L84) | Name of the service account to create. | string | ✓ | | -| [project_id](variables.tf#L95) | Project id where service account will be created. | string | ✓ | | +| [name](variables.tf#L91) | Name of the service account to create. | string | ✓ | | +| [project_id](variables.tf#L102) | Project id where service account will be created. | string | ✓ | | | [description](variables.tf#L17) | Optional description. | string | | null | | [display_name](variables.tf#L23) | Display name of the service account to create. | string | | "Terraform-managed." | | [generate_key](variables.tf#L29) | Generate a key for service account. | bool | | false | | [iam](variables.tf#L35) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_billing_roles](variables.tf#L42) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string)) | | {} | -| [iam_folder_roles](variables.tf#L49) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string)) | | {} | -| [iam_organization_roles](variables.tf#L56) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string)) | | {} | -| [iam_project_roles](variables.tf#L63) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | -| [iam_sa_roles](variables.tf#L70) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | -| [iam_storage_roles](variables.tf#L77) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | -| [prefix](variables.tf#L89) | Prefix applied to service account names. | string | | null | -| [public_keys_directory](variables.tf#L100) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string | | "" | -| [service_account_create](variables.tf#L106) | Create service account. When set to false, uses a data source to reference an existing service account. | bool | | true | +| [iam_additive](variables.tf#L42) | IAM additive bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_billing_roles](variables.tf#L49) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string)) | | {} | +| [iam_folder_roles](variables.tf#L56) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string)) | | {} | +| [iam_organization_roles](variables.tf#L63) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string)) | | {} | +| [iam_project_roles](variables.tf#L70) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | +| [iam_sa_roles](variables.tf#L77) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | +| [iam_storage_roles](variables.tf#L84) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | +| [prefix](variables.tf#L96) | Prefix applied to service account names. | string | | null | +| [public_keys_directory](variables.tf#L107) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string | | "" | +| [service_account_create](variables.tf#L113) | Create service account. When set to false, uses a data source to reference an existing service account. | bool | | true | ## Outputs @@ -66,9 +67,9 @@ module "myproject-default-service-accounts" { | [email](outputs.tf#L17) | Service account email. | | | [iam_email](outputs.tf#L25) | IAM-format service account email. | | | [id](outputs.tf#L33) | Service account id. | | -| [key](outputs.tf#L41) | Service account key. | ✓ | -| [name](outputs.tf#L47) | Service account name. | | -| [service_account](outputs.tf#L52) | Service account resource. | | -| [service_account_credentials](outputs.tf#L57) | Service account json credential templates for uploaded public keys data. | | +| [key](outputs.tf#L42) | Service account key. | ✓ | +| [name](outputs.tf#L48) | Service account name. | | +| [service_account](outputs.tf#L57) | Service account resource. | | +| [service_account_credentials](outputs.tf#L62) | Service account json credential templates for uploaded public keys data. | | diff --git a/modules/iam-service-account/iam.tf b/modules/iam-service-account/iam.tf index 1aa260a76a..02c879d9d7 100644 --- a/modules/iam-service-account/iam.tf +++ b/modules/iam-service-account/iam.tf @@ -17,6 +17,15 @@ # tfdoc:file:description IAM bindings. locals { + _iam_additive_pairs = flatten([ + for role, members in var.iam_additive : [ + for member in members : { role = role, member = member } + ] + ]) + iam_additive = { + for pair in local._iam_additive_pairs : + "${pair.role}-${pair.member}" => pair + } iam_billing_pairs = flatten([ for entity, roles in var.iam_billing_roles : [ for role in roles : [ @@ -61,6 +70,13 @@ locals { ]) } +resource "google_service_account_iam_member" "roles" { + for_each = local.iam_additive + service_account_id = local.service_account.name + role = each.value.role + member = each.value.member +} + resource "google_service_account_iam_binding" "roles" { for_each = var.iam service_account_id = local.service_account.name diff --git a/modules/iam-service-account/main.tf b/modules/iam-service-account/main.tf index 37f8205b46..d9f3b9c4f6 100644 --- a/modules/iam-service-account/main.tf +++ b/modules/iam-service-account/main.tf @@ -21,7 +21,7 @@ locals { ? google_service_account_key.key["1"] : map("", null) , {}) - prefix = var.prefix != null ? "${var.prefix}-" : "" + prefix = var.prefix == null || var.prefix == "" ? "" : "${var.prefix}-" resource_email_static = "${local.prefix}${var.name}@${var.project_id}.iam.gserviceaccount.com" resource_iam_email = ( local.service_account != null @@ -29,6 +29,7 @@ locals { : local.resource_iam_email_static ) resource_iam_email_static = "serviceAccount:${local.resource_email_static}" + service_account_id_static = "projects/${var.project_id}/serviceAccounts/${local.resource_email_static}" service_account = ( var.service_account_create ? try(google_service_account.service_account.0, null) diff --git a/modules/iam-service-account/outputs.tf b/modules/iam-service-account/outputs.tf index 42196534c3..e6c28dfdab 100644 --- a/modules/iam-service-account/outputs.tf +++ b/modules/iam-service-account/outputs.tf @@ -32,9 +32,10 @@ output "iam_email" { output "id" { description = "Service account id." - value = local.service_account.id + value = local.service_account_id_static depends_on = [ - local.service_account + data.google_service_account.service_account, + google_service_account.service_account ] } @@ -46,7 +47,11 @@ output "key" { output "name" { description = "Service account name." - value = local.service_account.name + value = local.service_account_id_static + depends_on = [ + data.google_service_account.service_account, + google_service_account.service_account + ] } output "service_account" { diff --git a/modules/iam-service-account/variables.tf b/modules/iam-service-account/variables.tf index ee15613433..363814e186 100644 --- a/modules/iam-service-account/variables.tf +++ b/modules/iam-service-account/variables.tf @@ -39,6 +39,13 @@ variable "iam" { nullable = false } +variable "iam_additive" { + description = "IAM additive bindings on the service account in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + variable "iam_billing_roles" { description = "Billing account roles granted to this service account, by billing account id. Non-authoritative." type = map(list(string)) diff --git a/modules/net-ilb/README.md b/modules/net-ilb/README.md index 5637f0ef9c..9916f736fe 100644 --- a/modules/net-ilb/README.md +++ b/modules/net-ilb/README.md @@ -23,11 +23,13 @@ module "ilb" { region = "europe-west1" name = "ilb-test" service_label = "ilb-test" - network = var.vpc.self_link - subnetwork = var.subnet.self_link + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } group_configs = { my-group = { - zone = "europe-west1-b", named_ports = null + zone = "europe-west1-b" instances = [ "instance-1-self-link", "instance-2-self-link" @@ -35,12 +37,12 @@ module "ilb" { } } backends = [{ - failover = false group = module.ilb.groups.my-group.self_link - balancing_mode = "CONNECTION" }] health_check_config = { - type = "http", check = { port = 80 }, config = {}, logging = true + http = { + port = 80 + } } } # tftest modules=1 resources=4 @@ -91,18 +93,21 @@ module "ilb" { region = "europe-west1" name = "ilb-test" service_label = "ilb-test" - network = var.vpc.self_link - subnetwork = var.subnet.self_link + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } ports = [80] backends = [ for z, mod in module.instance-group : { - failover = false group = mod.group.self_link - balancing_mode = "CONNECTION" + balancing_mode = "UTILIZATION" } ] health_check_config = { - type = "http", check = { port = 80 }, config = {}, logging = true + http = { + port = 80 + } } } # tftest modules=3 resources=7 @@ -113,37 +118,36 @@ module "ilb" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [backends](variables.tf#L33) | Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'. | list(object({…})) | ✓ | | -| [name](variables.tf#L98) | Name used for all resources. | string | ✓ | | -| [network](variables.tf#L103) | Network used for resources. | string | ✓ | | -| [project_id](variables.tf#L114) | Project id where resources will be created. | string | ✓ | | -| [region](variables.tf#L125) | GCP region. | string | ✓ | | -| [subnetwork](variables.tf#L136) | Subnetwork used for the forwarding rule. | string | ✓ | | +| [name](variables.tf#L184) | Name used for all resources. | string | ✓ | | +| [project_id](variables.tf#L195) | Project id where resources will be created. | string | ✓ | | +| [region](variables.tf#L206) | GCP region. | string | ✓ | | +| [vpc_config](variables.tf#L217) | VPC-level configuration. | object({…}) | ✓ | | | [address](variables.tf#L17) | Optional IP address used for the forwarding rule. | string | | null | -| [backend_config](variables.tf#L23) | Optional backend configuration. | object({…}) | | null | -| [failover_config](variables.tf#L42) | Optional failover configuration. | object({…}) | | null | -| [global_access](variables.tf#L52) | Global access, defaults to false if not set. | bool | | null | -| [group_configs](variables.tf#L58) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…})) | | {} | -| [health_check](variables.tf#L68) | Name of existing health check to use, disables auto-created health check. | string | | null | -| [health_check_config](variables.tf#L74) | Configuration of the auto-created helth check. | object({…}) | | {…} | -| [labels](variables.tf#L92) | Labels set on resources. | map(string) | | {} | -| [ports](variables.tf#L108) | Comma-separated ports, leave null to use all ports. | list(string) | | null | -| [protocol](variables.tf#L119) | IP protocol used, defaults to TCP. | string | | "TCP" | -| [service_label](variables.tf#L130) | Optional prefix of the fully qualified forwarding rule name. | string | | null | +| [backend_service_config](variables.tf#L23) | Backend service level configuration. | object({…}) | | {} | +| [backends](variables.tf#L56) | Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'. | list(object({…})) | | [] | +| [description](variables.tf#L75) | Optional description used for resources. | string | | "Terraform managed." | +| [global_access](variables.tf#L81) | Global access, defaults to false if not set. | bool | | null | +| [group_configs](variables.tf#L87) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…})) | | {} | +| [health_check](variables.tf#L98) | Name of existing health check to use, disables auto-created health check. | string | | null | +| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | {…} | +| [labels](variables.tf#L178) | Labels set on resources. | map(string) | | {} | +| [ports](variables.tf#L189) | Comma-separated ports, leave null to use all ports. | list(string) | | null | +| [protocol](variables.tf#L200) | IP protocol used, defaults to TCP. | string | | "TCP" | +| [service_label](variables.tf#L211) | Optional prefix of the fully qualified forwarding rule name. | string | | null | ## Outputs | name | description | sensitive | |---|---|:---:| -| [backend](outputs.tf#L17) | Backend resource. | | -| [backend_id](outputs.tf#L22) | Backend id. | | -| [backend_self_link](outputs.tf#L27) | Backend self link. | | +| [backend_service](outputs.tf#L17) | Backend resource. | | +| [backend_service_id](outputs.tf#L22) | Backend id. | | +| [backend_service_self_link](outputs.tf#L27) | Backend self link. | | | [forwarding_rule](outputs.tf#L32) | Forwarding rule resource. | | | [forwarding_rule_address](outputs.tf#L37) | Forwarding rule address. | | | [forwarding_rule_id](outputs.tf#L42) | Forwarding rule id. | | | [forwarding_rule_self_link](outputs.tf#L47) | Forwarding rule self link. | | -| [group_self_links](outputs.tf#L52) | Optional unmanaged instance group self links. | | -| [groups](outputs.tf#L59) | Optional unmanaged instance group resources. | | +| [group_self_links](outputs.tf#L57) | Optional unmanaged instance group self links. | | +| [groups](outputs.tf#L52) | Optional unmanaged instance group resources. | | | [health_check](outputs.tf#L64) | Auto-created health-check resource. | | | [health_check_self_id](outputs.tf#L69) | Auto-created health-check self id. | | | [health_check_self_link](outputs.tf#L74) | Auto-created health-check self link. | | diff --git a/modules/net-ilb/groups.tf b/modules/net-ilb/groups.tf new file mode 100644 index 0000000000..fe8bf13d58 --- /dev/null +++ b/modules/net-ilb/groups.tf @@ -0,0 +1,33 @@ +/** + * 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. + */ + +# tfdoc:file:description Optional instance group resources. + +resource "google_compute_instance_group" "unmanaged" { + for_each = var.group_configs + project = var.project_id + zone = each.value.zone + name = each.key + description = "Terraform-managed." + instances = each.value.instances + dynamic "named_port" { + for_each = each.value.named_ports + content { + name = named_port.key + port = named_port.value + } + } +} diff --git a/modules/net-ilb/health-check.tf b/modules/net-ilb/health-check.tf new file mode 100644 index 0000000000..4a4ed40def --- /dev/null +++ b/modules/net-ilb/health-check.tf @@ -0,0 +1,119 @@ +/** + * 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. + */ + +# tfdoc:file:description Health check resource. + +locals { + hc = var.health_check_config + hc_grpc = try(local.hc.grpc, null) != null + hc_http = try(local.hc.http, null) != null + hc_http2 = try(local.hc.http2, null) != null + hc_https = try(local.hc.https, null) != null + hc_ssl = try(local.hc.ssl, null) != null + hc_tcp = try(local.hc.tcp, null) != null +} + +resource "google_compute_health_check" "default" { + provider = google-beta + count = local.hc != null ? 1 : 0 + project = var.project_id + name = var.name + description = local.hc.description + check_interval_sec = local.hc.check_interval_sec + healthy_threshold = local.hc.healthy_threshold + timeout_sec = local.hc.timeout_sec + unhealthy_threshold = local.hc.unhealthy_threshold + + dynamic "grpc_health_check" { + for_each = local.hc_grpc ? [""] : [] + content { + port = local.hc.grpc.port + port_name = local.hc.grpc.port_name + port_specification = local.hc.grpc.port_specification + grpc_service_name = local.hc.grpc.service_name + } + } + + dynamic "http_health_check" { + for_each = local.hc_http ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "http2_health_check" { + for_each = local.hc_http2 ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "https_health_check" { + for_each = local.hc_https ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "ssl_health_check" { + for_each = local.hc_ssl ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "tcp_health_check" { + for_each = local.hc_tcp ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "log_config" { + for_each = try(local.hc.enable_logging, null) == true ? [""] : [] + content { + enable = true + } + } +} diff --git a/modules/net-ilb/main.tf b/modules/net-ilb/main.tf index aa4addcc0c..be4c578690 100644 --- a/modules/net-ilb/main.tf +++ b/modules/net-ilb/main.tf @@ -16,252 +16,100 @@ locals { + bs_conntrack = var.backend_service_config.connection_tracking + bs_failover = var.backend_service_config.failover_config health_check = ( var.health_check != null ? var.health_check - : try(local.health_check_resource.self_link, null) + : google_compute_health_check.default.0.self_link ) - health_check_resource = try( - google_compute_health_check.http.0, - google_compute_health_check.https.0, - google_compute_health_check.tcp.0, - google_compute_health_check.ssl.0, - google_compute_health_check.http2.0, - {} - ) - health_check_type = try(var.health_check_config.type, null) } resource "google_compute_forwarding_rule" "default" { - provider = google-beta - project = var.project_id - name = var.name - description = "Terraform managed." + provider = google-beta + project = var.project_id + region = var.region + name = var.name + description = var.description + ip_address = var.address + ip_protocol = var.protocol # TCP | UDP + backend_service = ( + google_compute_region_backend_service.default.self_link + ) load_balancing_scheme = "INTERNAL" - region = var.region - network = var.network - subnetwork = var.subnetwork - ip_address = var.address - ip_protocol = var.protocol # TCP | UDP - ports = var.ports # "nnnnn" or "nnnnn,nnnnn,nnnnn" max 5 - service_label = var.service_label - all_ports = var.ports == null ? true : null + network = var.vpc_config.network + ports = var.ports # "nnnnn" or "nnnnn,nnnnn,nnnnn" max 5 + subnetwork = var.vpc_config.subnetwork allow_global_access = var.global_access - backend_service = google_compute_region_backend_service.default.self_link + labels = var.labels + all_ports = var.ports == null ? true : null + service_label = var.service_label # is_mirroring_collector = false - labels = var.labels } resource "google_compute_region_backend_service" "default" { - provider = google-beta - project = var.project_id - name = var.name - description = "Terraform managed." - load_balancing_scheme = "INTERNAL" - region = var.region - network = var.network - health_checks = [local.health_check] - protocol = var.protocol - - session_affinity = try(var.backend_config.session_affinity, null) - timeout_sec = try(var.backend_config.timeout_sec, null) - connection_draining_timeout_sec = try(var.backend_config.connection_draining_timeout_sec, null) + provider = google-beta + project = var.project_id + region = var.region + name = var.name + description = var.description + load_balancing_scheme = "INTERNAL" + protocol = var.protocol + network = var.vpc_config.network + health_checks = [local.health_check] + connection_draining_timeout_sec = var.backend_service_config.connection_draining_timeout_sec + session_affinity = var.backend_service_config.session_affinity + timeout_sec = var.backend_service_config.timeout_sec dynamic "backend" { for_each = { for b in var.backends : b.group => b } - iterator = backend content { balancing_mode = backend.value.balancing_mode - description = "Terraform managed." + description = backend.value.description failover = backend.value.failover group = backend.key } } - dynamic "failover_policy" { - for_each = var.failover_config == null ? [] : [var.failover_config] - iterator = config - content { - disable_connection_drain_on_failover = config.value.disable_connection_drain - drop_traffic_if_unhealthy = config.value.drop_traffic_if_unhealthy - failover_ratio = config.value.ratio - } - } - -} - -resource "google_compute_instance_group" "unmanaged" { - for_each = var.group_configs - project = var.project_id - zone = each.value.zone - name = each.key - description = "Terraform-managed." - instances = each.value.instances - dynamic "named_port" { - for_each = each.value.named_ports != null ? each.value.named_ports : {} - iterator = config - content { - name = config.key - port = config.value - } - } -} - -resource "google_compute_health_check" "http" { - provider = google-beta - count = ( - var.health_check == null && local.health_check_type == "http" ? 1 : 0 - ) - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] + dynamic "connection_tracking_policy" { + for_each = local.bs_conntrack == null ? [] : [""] content { - enable = true + connection_persistence_on_unhealthy_backends = ( + local.bs_conntrack.persist_conn_on_unhealthy != null + ? local.bs_conntrack.persist_conn_on_unhealthy + : null + ) + idle_timeout_sec = local.bs_conntrack.idle_timeout_sec + tracking_mode = ( + local.bs_conntrack.track_per_session != null + ? local.bs_conntrack.track_per_session + : null + ) } } -} - -resource "google_compute_health_check" "https" { - provider = google-beta - count = ( - var.health_check == null && local.health_check_type == "https" ? 1 : 0 - ) - project = var.project_id - name = var.name - description = "Terraform managed." - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - https_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] + dynamic "failover_policy" { + for_each = local.bs_failover == null ? [] : [""] content { - enable = true + disable_connection_drain_on_failover = local.bs_failover.disable_conn_drain + drop_traffic_if_unhealthy = local.bs_failover.drop_traffic_if_unhealthy + failover_ratio = local.bs_failover.ratio } } -} - -resource "google_compute_health_check" "tcp" { - provider = google-beta - count = ( - var.health_check == null && local.health_check_type == "tcp" ? 1 : 0 - ) - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - tcp_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] + for_each = var.backend_service_config.log_sample_rate == null ? [] : [""] content { - enable = true + enable = true + sample_rate = var.backend_service_config.log_sample_rate } } -} - -resource "google_compute_health_check" "ssl" { - provider = google-beta - count = ( - var.health_check == null && local.health_check_type == "ssl" ? 1 : 0 - ) - project = var.project_id - name = var.name - description = "Terraform managed." - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - ssl_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] + dynamic "subsetting" { + for_each = var.backend_service_config.enable_subsetting == true ? [""] : [] content { - enable = true + policy = "CONSISTENT_HASH_SUBSETTING" } } -} - -resource "google_compute_health_check" "http2" { - provider = google-beta - count = ( - var.health_check == null && local.health_check_type == "http2" ? 1 : 0 - ) - project = var.project_id - name = var.name - description = "Terraform managed." - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http2_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } } - diff --git a/modules/net-ilb/outputs.tf b/modules/net-ilb/outputs.tf index 55b454e181..3f8eb9e4c7 100644 --- a/modules/net-ilb/outputs.tf +++ b/modules/net-ilb/outputs.tf @@ -14,17 +14,17 @@ * limitations under the License. */ -output "backend" { +output "backend_service" { description = "Backend resource." value = google_compute_region_backend_service.default } -output "backend_id" { +output "backend_service_id" { description = "Backend id." value = google_compute_region_backend_service.default.id } -output "backend_self_link" { +output "backend_service_self_link" { description = "Backend self link." value = google_compute_region_backend_service.default.self_link } @@ -49,6 +49,11 @@ output "forwarding_rule_self_link" { value = google_compute_forwarding_rule.default.self_link } +output "groups" { + description = "Optional unmanaged instance group resources." + value = google_compute_instance_group.unmanaged +} + output "group_self_links" { description = "Optional unmanaged instance group self links." value = { @@ -56,22 +61,17 @@ output "group_self_links" { } } -output "groups" { - description = "Optional unmanaged instance group resources." - value = google_compute_instance_group.unmanaged -} - output "health_check" { description = "Auto-created health-check resource." - value = local.health_check_resource + value = try(google_compute_health_check.default.0, null) } output "health_check_self_id" { description = "Auto-created health-check self id." - value = try(local.health_check_resource.id, null) + value = try(google_compute_health_check.default.0.id, null) } output "health_check_self_link" { description = "Auto-created health-check self link." - value = try(local.health_check_resource.self_link, null) + value = try(google_compute_health_check.default.0.self_link, null) } diff --git a/modules/net-ilb/variables.tf b/modules/net-ilb/variables.tf index 638aee5219..d2ffc5a672 100644 --- a/modules/net-ilb/variables.tf +++ b/modules/net-ilb/variables.tf @@ -20,33 +20,62 @@ variable "address" { default = null } -variable "backend_config" { - description = "Optional backend configuration." +variable "backend_service_config" { + description = "Backend service level configuration." type = object({ - session_affinity = string - timeout_sec = number - connection_draining_timeout_sec = number + connection_draining_timeout_sec = optional(number) + connection_tracking = optional(object({ + idle_timeout_sec = optional(number) + persist_conn_on_unhealthy = optional(string) + track_per_session = optional(bool) + })) + enable_subsetting = optional(bool) + failover_config = optional(object({ + disable_conn_drain = optional(bool) + drop_traffic_if_unhealthy = optional(bool) + ratio = optional(number) + })) + log_sample_rate = optional(number) + session_affinity = optional(string) + timeout_sec = optional(number) }) - default = null + default = {} + nullable = false + validation { + condition = contains( + [ + "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION", + "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO" + ], + coalesce(var.backend_service_config.session_affinity, "NONE") + ) + error_message = "Invalid session affinity value." + } } variable "backends" { description = "Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'." type = list(object({ - failover = bool group = string - balancing_mode = string + balancing_mode = optional(string, "CONNECTION") + description = optional(string, "Terraform managed.") + failover = optional(bool, false) })) + default = [] + nullable = false + validation { + condition = alltrue([ + for b in var.backends : contains( + ["CONNECTION", "UTILIZATION"], coalesce(b.balancing_mode, "CONNECTION") + )]) + error_message = "When specified balancing mode needs to be 'CONNECTION' or 'UTILIZATION'." + } } -variable "failover_config" { - description = "Optional failover configuration." - type = object({ - disable_connection_drain = bool - drop_traffic_if_unhealthy = bool - ratio = number - }) - default = null +variable "description" { + description = "Optional description used for resources." + type = string + default = "Terraform managed." } variable "global_access" { @@ -58,11 +87,12 @@ variable "global_access" { variable "group_configs" { description = "Optional unmanaged groups to create. Can be referenced in backends via outputs." type = map(object({ - instances = list(string) - named_ports = map(number) zone = string + instances = optional(list(string), []) + named_ports = optional(map(number), {}) })) - default = {} + default = {} + nullable = false } variable "health_check" { @@ -72,20 +102,76 @@ variable "health_check" { } variable "health_check_config" { - description = "Configuration of the auto-created helth check." + description = "Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage." type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool + check_interval_sec = optional(number) + description = optional(string, "Terraform managed.") + enable_logging = optional(bool, false) + healthy_threshold = optional(number) + timeout_sec = optional(number) + unhealthy_threshold = optional(number) + grpc = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + service_name = optional(string) + })) + http = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + })) + http2 = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + })) + https = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + })) + tcp = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request = optional(string) + response = optional(string) + })) + ssl = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request = optional(string) + response = optional(string) + })) }) default = { - type = "http" - check = { + tcp = { port_specification = "USE_SERVING_PORT" } - config = {} - logging = false + } + validation { + condition = ( + (try(var.health_check_config.grpc, null) == null ? 0 : 1) + + (try(var.health_check_config.http, null) == null ? 0 : 1) + + (try(var.health_check_config.tcp, null) == null ? 0 : 1) <= 1 + ) + error_message = "Only one health check type can be configured at a time." } } @@ -100,11 +186,6 @@ variable "name" { type = string } -variable "network" { - description = "Network used for resources." - type = string -} - variable "ports" { description = "Comma-separated ports, leave null to use all ports." type = list(string) @@ -133,7 +214,11 @@ variable "service_label" { default = null } -variable "subnetwork" { - description = "Subnetwork used for the forwarding rule." - type = string +variable "vpc_config" { + description = "VPC-level configuration." + type = object({ + network = string + subnetwork = string + }) + nullable = false } diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md index 188dbc0b1a..6b36edd93c 100644 --- a/modules/net-vpc-firewall/README.md +++ b/modules/net-vpc-firewall/README.md @@ -2,11 +2,10 @@ This module allows creation and management of different types of firewall rules for a single VPC network: -- blanket ingress rules based on IP ranges that allow all traffic via the `admin_ranges` variable -- simplified tag-based ingress rules for the HTTP, HTTPS and SSH protocols via the `xxx_source_ranges` variables; HTTP and HTTPS tags match those set by the console via the "Allow HTTP(S) traffic" instance flags -- custom rules via the `custom_rules` variables +- custom rules via the `egress_rules` and `ingress_rules` variables +- optional predefined rules that simplify prototyping via the `default_rules_config` variable -The simplified tag-based rules are enabled by default, set to the ranges of the GCP health checkers for HTTP/HTTPS, and the IAP forwarders for SSH. To disable them set the corresponding variables to empty lists. +The predefined rules are enabled by default and set to the ranges of the GCP health checkers for HTTP/HTTPS, and the IAP forwarders for SSH. See the relevant section below on how to configure or disable them. ## Examples @@ -16,10 +15,12 @@ This is often useful for prototyping or testing infrastructure, allowing open in ```hcl module "firewall" { - source = "./fabric/modules/net-vpc-firewall" - project_id = "my-project" - network = "my-network" - admin_ranges = ["10.0.0.0/8"] + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + default_rules_config = { + admin_ranges = ["10.0.0.0/8"] + } } # tftest modules=1 resources=4 ``` @@ -28,70 +29,119 @@ module "firewall" { This is an example of how to define custom rules, with a sample rule allowing open ingress for the NTP protocol to instances with the `ntp-svc` tag. +Some implicit defaults are used in the rules variable types and can be controlled by explicitly setting specific attributes: + +- action is controlled via the `deny` attribute which defaults to `true` for egress and `false` for ingress +- priority defaults to `1000` +- destination ranges (for egress) and source ranges (for ingress) default to `["0.0.0.0/0"]` if not explicitly set +- rules default to all protocols if not set + ```hcl module "firewall" { - source = "./fabric/modules/net-vpc-firewall" - project_id = "my-project" - network = "my-network" - admin_ranges = ["10.0.0.0/8"] - custom_rules = { - ntp-svc = { - description = "NTP service." - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["0.0.0.0/0"] - targets = ["ntp-svc"] - use_service_accounts = false - rules = [{ protocol = "udp", ports = [123] }] - extra_attributes = {} + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + default_rules_config = { + admin_ranges = ["10.0.0.0/8"] + } + egress_rules = { + # implicit `deny` action + allow-egress-rfc1918 = { + description = "Allow egress to RFC 1918 ranges." + destination_ranges = [ + "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" + ] + # implicit { protocol = "all" } rule + } + deny-egress-all = { + description = "Block egress." + # implicit ["0.0.0.0/0"] destination ranges + # implicit { protocol = "all" } rule + } + } + ingress_rules = { + # implicit `allow` action + allow-ingress-ntp = { + description = "Allow NTP service based on tag." + source_ranges = ["0.0.0.0/0"] + targets = ["ntp-svc"] + rules = [{ protocol = "udp", ports = [123] }] } } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=7 ``` -### No predefined rules +### Controlling or turning off default rules + +Predefined rules can be controlled or turned off via the `default_rules_config` variable. -If you don't want any predefined rules set `admin_ranges`, `http_source_ranges`, `https_source_ranges` and `ssh_source_ranges` to an empty list. +#### Overriding default tags and ranges + +Each protocol rule has a default set of tags and ranges: + +- the health check range and the `http-server`/`https-server` tag for HTTP/HTTPS, matching tags set via GCP console flags on GCE instances +- the IAP forwarders range and `ssh` tag for SSH + +Default tags and ranges can be overridden for each protocol, like shown here for SSH: ```hcl module "firewall" { - source = "./fabric/modules/net-vpc-firewall" - project_id = "my-project" - network = "my-network" - admin_ranges = [] - http_source_ranges = [] - https_source_ranges = [] - ssh_source_ranges = [] - custom_rules = { - allow-https = { - description = "Allow HTTPS from internal networks." - direction = "INGRESS" - action = "allow" - sources = [] - ranges = ["rfc1918"] - targets = [] - use_service_accounts = false - rules = [{ protocol = "tcp", ports = [443] }] - extra_attributes = {} - } + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + default_rules_config = { + ssh_ranges = ["10.0.0.0/8"] + ssh_rags = ["ssh-default"] } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=3 ``` +#### Disabling predefined rules + +Default rules can be disabled individually by specifying an empty set of ranges: + +```hcl +module "firewall" { + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + default_rules_config = { + ssh_ranges = [] + } +} +# tftest modules=1 resources=2 +``` + +Or the entire set of rules can be disabled via the `disabled` attribute: + +```hcl +module "firewall" { + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + default_rules_config = { + disabled = true + } +} +# tftest modules=0 resources=0 +``` ### Rules Factory + The module includes a rules factory (see [Resource Factories](../../blueprints/factories/)) for the massive creation of rules leveraging YaML configuration files. Each configuration file can optionally contain more than one rule which a structure that reflects the `custom_rules` variable. ```hcl module "firewall" { - source = "./fabric/modules/net-vpc-firewall" - project_id = "my-project" - network = "my-network" - data_folder = "config/firewall" - cidr_template_file = "config/cidr_template.yaml" + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" + factories_config = { + rules_folder = "config/firewall" + cidr_tpl_file = "config/cidr_template.yaml" + } + } # tftest skip ``` @@ -100,13 +150,9 @@ module "firewall" { # ./config/firewall/load_balancers.yaml allow-healthchecks: description: Allow ingress from healthchecks. - direction: INGRESS - action: allow - sources: [] ranges: - - $healthchecks + - healthchecks targets: ["lb-backends"] - use_service_accounts: false rules: - protocol: tcp ports: @@ -129,26 +175,19 @@ healthchecks: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [network](variables.tf#L80) | Name of the network this set of firewall rules applies to. | string | ✓ | | -| [project_id](variables.tf#L85) | Project id of the project that holds the network. | string | ✓ | | -| [admin_ranges](variables.tf#L17) | IP CIDR ranges that have complete access to all subnets. | list(string) | | [] | -| [cidr_template_file](variables.tf#L23) | Path for optional file containing name->cidr_list map to be used by the rules factory. | string | | null | -| [custom_rules](variables.tf#L29) | List of custom rule definitions (refer to variables file for syntax). | map(object({…})) | | {} | -| [data_folder](variables.tf#L48) | Path for optional folder containing firewall rules defined as YaML objects used by the rules factory. | string | | null | -| [http_source_ranges](variables.tf#L54) | List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges. | list(string) | | ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] | -| [https_source_ranges](variables.tf#L60) | List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges. | list(string) | | ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] | -| [named_ranges](variables.tf#L66) | Names that can be used of valid values for the `ranges` field of `custom_rules`. | map(list(string)) | | {…} | -| [ssh_source_ranges](variables.tf#L90) | List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range. | list(string) | | ["35.235.240.0/20"] | +| [network](variables.tf#L109) | Name of the network this set of firewall rules applies to. | string | ✓ | | +| [project_id](variables.tf#L114) | Project id of the project that holds the network. | string | ✓ | | +| [default_rules_config](variables.tf#L17) | Optionally created convenience rules. Set the variable or individual members to null to disable. | object({…}) | | {} | +| [egress_rules](variables.tf#L37) | List of egress rule definitions, default to deny action. | map(object({…})) | | {} | +| [factories_config](variables.tf#L83) | Paths to data files and folders that enable factory functionality. | object({…}) | | null | +| [ingress_rules](variables.tf#L60) | List of ingress rule definitions, default to allow action. | map(object({…})) | | {} | +| [named_ranges](variables.tf#L92) | Define mapping of names to ranges that can be used in custom rules. | map(list(string)) | | {…} | ## Outputs | name | description | sensitive | |---|---|:---:| -| [admin_ranges](outputs.tf#L17) | Admin ranges data. | | -| [custom_egress_allow_rules](outputs.tf#L26) | Custom egress rules with allow blocks. | | -| [custom_egress_deny_rules](outputs.tf#L34) | Custom egress rules with allow blocks. | | -| [custom_ingress_allow_rules](outputs.tf#L42) | Custom ingress rules with allow blocks. | | -| [custom_ingress_deny_rules](outputs.tf#L50) | Custom ingress rules with deny blocks. | | -| [rules](outputs.tf#L58) | All google_compute_firewall resources created. | | +| [default_rules](outputs.tf#L17) | Default rule resources. | | +| [rules](outputs.tf#L27) | Custom rule resources. | | diff --git a/modules/net-vpc-firewall/default-rules.tf b/modules/net-vpc-firewall/default-rules.tf new file mode 100644 index 0000000000..bbca6dd57a --- /dev/null +++ b/modules/net-vpc-firewall/default-rules.tf @@ -0,0 +1,77 @@ +/** + * 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. + */ + +# tfdoc:file:description Optional default rule resources. + +locals { + default_rules = { + for k, v in var.default_rules_config : + k => var.default_rules_config.disabled == true || v == null ? [] : v + if k != "disabled" + } +} + +resource "google_compute_firewall" "allow-admins" { + count = length(local.default_rules.admin_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-admins" + description = "Access from the admin subnet to all subnets." + network = var.network + project = var.project_id + source_ranges = local.default_rules.admin_ranges + allow { protocol = "all" } +} + +resource "google_compute_firewall" "allow-tag-http" { + count = length(local.default_rules.http_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-http" + description = "Allow http to machines with matching tags." + network = var.network + project = var.project_id + source_ranges = local.default_rules.http_ranges + target_tags = local.default_rules.http_tags + allow { + protocol = "tcp" + ports = ["80"] + } +} + +resource "google_compute_firewall" "allow-tag-https" { + count = length(local.default_rules.https_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-https" + description = "Allow http to machines with matching tags." + network = var.network + project = var.project_id + source_ranges = local.default_rules.https_ranges + target_tags = local.default_rules.https_tags + allow { + protocol = "tcp" + ports = ["443"] + } +} + +resource "google_compute_firewall" "allow-tag-ssh" { + count = length(local.default_rules.ssh_ranges) > 0 ? 1 : 0 + name = "${var.network}-ingress-tag-ssh" + description = "Allow SSH to machines with matching tags." + network = var.network + project = var.project_id + source_ranges = local.default_rules.ssh_ranges + target_tags = local.default_rules.ssh_tags + allow { + protocol = "tcp" + ports = ["22"] + } +} diff --git a/modules/net-vpc-firewall/main.tf b/modules/net-vpc-firewall/main.tf index c08b2d1651..708b8844b6 100644 --- a/modules/net-vpc-firewall/main.tf +++ b/modules/net-vpc-firewall/main.tf @@ -15,149 +15,125 @@ */ locals { - _custom_rules = { - for id, rule in var.custom_rules : - id => merge(rule, { - # make rules a map so we use it in a for_each - rules = { for index, ports in rule.rules : index => ports } - # lookup any named ranges references - ranges = flatten([ - for range in rule.ranges : - try(var.named_ranges[range], range) - ]) - }) - } - - cidrs = try({ - for name, cidrs in yamldecode(file(var.cidr_template_file)) : - name => cidrs - }, {}) - - _factory_rules_raw = flatten([ - for file in try(fileset(var.data_folder, "**/*.yaml"), []) : [ - for key, ruleset in yamldecode(file("${var.data_folder}/${file}")) : - merge(ruleset, { - name = "${key}" - rules = { for index, ports in ruleset.rules : index => ports } - ranges = try(ruleset.ranges, null) == null ? null : flatten( - [for cidr in ruleset.ranges : - can(regex("^\\$", cidr)) - ? local.cidrs[trimprefix(cidr, "$")] - : [cidr] - ]) - extra_attributes = try(ruleset.extra_attributes, {}) - }) + # define list of rule files + _factory_rule_files = [ + for f in try(fileset(var.factories_config.rules_folder, "**/*.yaml"), []) : + "${var.factories_config.rules_folder}/${f}" + ] + # decode rule files and account for optional attributes + _factory_rule_list = flatten([ + for f in local._factory_rule_files : [ + for direction, ruleset in yamldecode(file(f)) : [ + for name, rule in ruleset : { + name = name + deny = try(rule.deny, false) + rules = try(rule.rules, [{ protocol = "all" }]) + description = try(rule.description, null) + destination_ranges = try(rule.destination_ranges, null) + direction = upper(direction) + disabled = try(rule.disabled, null) + enable_logging = try(rule.enable_logging, null) + priority = try(rule.priority, 1000) + source_ranges = try(rule.source_ranges, null) + sources = try(rule.sources, null) + targets = try(rule.targets, null) + use_service_accounts = try(rule.use_service_accounts, false) + } + ] ] ]) - _factory_rules = { - for d in local._factory_rules_raw : d["name"] => d + for r in local._factory_rule_list : r.name => r + if contains(["EGRESS", "INGRESS"], r.direction) } - - custom_rules = merge(local._custom_rules, local._factory_rules) -} - - -############################################################################### -# rules based on IP ranges -############################################################################### - -resource "google_compute_firewall" "allow-admins" { - count = length(var.admin_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-admins" - description = "Access from the admin subnet to all subnets" - network = var.network - project = var.project_id - source_ranges = var.admin_ranges - allow { protocol = "all" } -} - -############################################################################### -# rules based on tags -############################################################################### - -resource "google_compute_firewall" "allow-tag-ssh" { - count = length(var.ssh_source_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-ssh" - description = "Allow SSH to machines with the 'ssh' tag" - network = var.network - project = var.project_id - source_ranges = var.ssh_source_ranges - target_tags = ["ssh"] - allow { - protocol = "tcp" - ports = ["22"] + _named_ranges = merge( + try(yamldecode(file(var.factories_config.cidr_tpl_file)), {}), + var.named_ranges + ) + _rules = merge( + local._factory_rules, local._rules_egress, local._rules_ingress + ) + _rules_egress = { + for name, rule in merge(var.egress_rules) : + name => merge(rule, { direction = "EGRESS" }) } -} - -resource "google_compute_firewall" "allow-tag-http" { - count = length(var.http_source_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-http" - description = "Allow HTTP to machines with the 'http-server' tag" - network = var.network - project = var.project_id - source_ranges = var.http_source_ranges - target_tags = ["http-server"] - allow { - protocol = "tcp" - ports = ["80"] + _rules_ingress = { + for name, rule in merge(var.ingress_rules) : + name => merge(rule, { direction = "INGRESS" }) } -} - -resource "google_compute_firewall" "allow-tag-https" { - count = length(var.https_source_ranges) > 0 ? 1 : 0 - name = "${var.network}-ingress-tag-https" - description = "Allow HTTPS to machines with the 'https' tag" - network = var.network - project = var.project_id - source_ranges = var.https_source_ranges - target_tags = ["https-server"] - allow { - protocol = "tcp" - ports = ["443"] + # convert rules data to resource format and replace range template variables + rules = { + for name, rule in local._rules : + name => merge(rule, { + action = rule.deny == true ? "DENY" : "ALLOW" + destination_ranges = flatten([ + for range in coalesce(try(rule.destination_ranges, null), []) : + try(local._named_ranges[range], range) + ]) + rules = { for k, v in rule.rules : k => v } + source_ranges = flatten([ + for range in coalesce(try(rule.source_ranges, null), []) : + try(local._named_ranges[range], range) + ]) + }) } } -################################################################################ -# dynamic rules # -################################################################################ - resource "google_compute_firewall" "custom-rules" { - # provider = "google-beta" - for_each = local.custom_rules + for_each = local.rules + project = var.project_id + network = var.network name = each.key description = each.value.description direction = each.value.direction - network = var.network - project = var.project_id source_ranges = ( each.value.direction == "INGRESS" - ? coalesce(each.value.ranges, []) == [] ? ["0.0.0.0/0"] : each.value.ranges - : null + ? ( + coalesce(each.value.source_ranges, []) == [] + ? ["0.0.0.0/0"] + : each.value.source_ranges + ) : null ) destination_ranges = ( each.value.direction == "EGRESS" - ? coalesce(each.value.ranges, []) == [] ? ["0.0.0.0/0"] : each.value.ranges + ? ( + coalesce(each.value.destination_ranges, []) == [] + ? ["0.0.0.0/0"] + : each.value.destination_ranges + ) : null + ) + source_tags = ( + each.value.use_service_accounts || each.value.direction == "EGRESS" + ? null + : each.value.sources + ) + source_service_accounts = ( + each.value.use_service_accounts && each.value.direction == "INGRESS" + ? each.value.sources : null ) - source_tags = each.value.use_service_accounts || each.value.direction == "EGRESS" ? null : each.value.sources - source_service_accounts = each.value.use_service_accounts && each.value.direction == "INGRESS" ? each.value.sources : null - target_tags = each.value.use_service_accounts ? null : each.value.targets - target_service_accounts = each.value.use_service_accounts ? each.value.targets : null - disabled = lookup(each.value.extra_attributes, "disabled", false) - priority = lookup(each.value.extra_attributes, "priority", 1000) + target_tags = ( + each.value.use_service_accounts ? null : each.value.targets + ) + target_service_accounts = ( + each.value.use_service_accounts ? each.value.targets : null + ) + disabled = each.value.disabled == true + priority = each.value.priority dynamic "log_config" { - for_each = lookup(each.value.extra_attributes, "logging", null) != null ? [each.value.extra_attributes.logging] : [] - iterator = logging_config + for_each = each.value.enable_logging == null ? [] : [""] content { - metadata = logging_config.value + metadata = ( + try(each.value.enable_logging.include_metadata, null) == true + ? "INCLUDE_ALL_METADATA" + : "EXCLUDE_ALL_METADATA" + ) } } dynamic "deny" { - for_each = each.value.action == "deny" ? each.value.rules : {} - + for_each = each.value.action == "DENY" ? each.value.rules : {} iterator = rule content { protocol = rule.value.protocol @@ -166,8 +142,7 @@ resource "google_compute_firewall" "custom-rules" { } dynamic "allow" { - for_each = each.value.action == "allow" ? each.value.rules : {} - + for_each = each.value.action == "ALLOW" ? each.value.rules : {} iterator = rule content { protocol = rule.value.protocol diff --git a/modules/net-vpc-firewall/outputs.tf b/modules/net-vpc-firewall/outputs.tf index f784583c53..9206ab5464 100644 --- a/modules/net-vpc-firewall/outputs.tf +++ b/modules/net-vpc-firewall/outputs.tf @@ -14,54 +14,17 @@ * limitations under the License. */ -output "admin_ranges" { - description = "Admin ranges data." - +output "default_rules" { + description = "Default rule resources." value = { - enabled = length(var.admin_ranges) > 0 - ranges = join(",", var.admin_ranges) + admin = try(google_compute_firewall.allow-admins, null) + http = try(google_compute_firewall.allow-tag-http, null) + https = try(google_compute_firewall.allow-tag-https, null) + ssh = try(google_compute_firewall.allow-tag-ssh, null) } } -output "custom_egress_allow_rules" { - description = "Custom egress rules with allow blocks." - value = [ - for rule in google_compute_firewall.custom-rules : - rule.name if rule.direction == "EGRESS" && try(length(rule.allow), 0) > 0 - ] -} - -output "custom_egress_deny_rules" { - description = "Custom egress rules with allow blocks." - value = [ - for rule in google_compute_firewall.custom-rules : - rule.name if rule.direction == "EGRESS" && try(length(rule.deny), 0) > 0 - ] -} - -output "custom_ingress_allow_rules" { - description = "Custom ingress rules with allow blocks." - value = [ - for rule in google_compute_firewall.custom-rules : - rule.name if rule.direction == "INGRESS" && try(length(rule.allow), 0) > 0 - ] -} - -output "custom_ingress_deny_rules" { - description = "Custom ingress rules with deny blocks." - value = [ - for rule in google_compute_firewall.custom-rules : - rule.name if rule.direction == "INGRESS" && try(length(rule.deny), 0) > 0 - ] -} - output "rules" { - description = "All google_compute_firewall resources created." - value = merge( - google_compute_firewall.custom-rules, - try({ (google_compute_firewall.allow-admins.0.name) = google_compute_firewall.allow-admins.0 }, {}), - try({ (google_compute_firewall.allow-tag-ssh.0.name) = google_compute_firewall.allow-tag-ssh.0 }, {}), - try({ (google_compute_firewall.allow-tag-http.0.name) = google_compute_firewall.allow-tag-http.0 }, {}), - try({ (google_compute_firewall.allow-tag-https.0.name) = google_compute_firewall.allow-tag-https.0 }, {}) - ) + description = "Custom rule resources." + value = google_compute_firewall.custom-rules } diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf index b0a2a9d792..dd8033342c 100644 --- a/modules/net-vpc-firewall/variables.tf +++ b/modules/net-vpc-firewall/variables.tf @@ -14,67 +14,96 @@ * limitations under the License. */ -variable "admin_ranges" { - description = "IP CIDR ranges that have complete access to all subnets." - type = list(string) - default = [] +variable "default_rules_config" { + description = "Optionally created convenience rules. Set the variable or individual members to null to disable." + type = object({ + admin_ranges = optional(list(string)) + disabled = optional(bool, false) + http_ranges = optional(list(string), [ + "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] + ) + http_tags = optional(list(string), ["http-server"]) + https_ranges = optional(list(string), [ + "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] + ) + https_tags = optional(list(string), ["https-server"]) + ssh_ranges = optional(list(string), ["35.235.240.0/20"]) + ssh_tags = optional(list(string), ["ssh"]) + }) + default = {} + nullable = false } -variable "cidr_template_file" { - description = "Path for optional file containing name->cidr_list map to be used by the rules factory." - type = string - default = null -} - -variable "custom_rules" { - description = "List of custom rule definitions (refer to variables file for syntax)." +variable "egress_rules" { + description = "List of egress rule definitions, default to deny action." type = map(object({ - description = string - direction = string - action = string # (allow|deny) - ranges = list(string) - sources = list(string) - targets = list(string) - use_service_accounts = bool - rules = list(object({ - protocol = string - ports = list(string) + deny = optional(bool, true) + description = optional(string) + destination_ranges = optional(list(string)) + disabled = optional(bool, false) + enable_logging = optional(object({ + include_metadata = optional(bool) })) - extra_attributes = map(string) + priority = optional(number, 1000) + sources = optional(list(string)) + targets = optional(list(string)) + use_service_accounts = optional(bool, false) + rules = optional(list(object({ + protocol = string + ports = optional(list(string)) + })), [{ protocol = "all" }]) })) - default = {} -} - -variable "data_folder" { - description = "Path for optional folder containing firewall rules defined as YaML objects used by the rules factory." - type = string - default = null + default = {} + nullable = false } -variable "http_source_ranges" { - description = "List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges." - type = list(string) - default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] +variable "ingress_rules" { + description = "List of ingress rule definitions, default to allow action." + type = map(object({ + deny = optional(bool, false) + description = optional(string) + disabled = optional(bool, false) + enable_logging = optional(object({ + include_metadata = optional(bool) + })) + priority = optional(number, 1000) + source_ranges = optional(list(string)) + sources = optional(list(string)) + targets = optional(list(string)) + use_service_accounts = optional(bool, false) + rules = optional(list(object({ + protocol = string + ports = optional(list(string)) + })), [{ protocol = "all" }]) + })) + default = {} + nullable = false } -variable "https_source_ranges" { - description = "List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges." - type = list(string) - default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] +variable "factories_config" { + description = "Paths to data files and folders that enable factory functionality." + type = object({ + cidr_tpl_file = optional(string) + rules_folder = string + }) + default = null } variable "named_ranges" { - description = "Names that can be used of valid values for the `ranges` field of `custom_rules`." + description = "Define mapping of names to ranges that can be used in custom rules." type = map(list(string)) default = { - any = ["0.0.0.0/0"] - dns-forwarders = ["35.199.192.0/19"] - health-checkers = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] + any = ["0.0.0.0/0"] + dns-forwarders = ["35.199.192.0/19"] + health-checkers = [ + "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22" + ] iap-forwarders = ["35.235.240.0/20"] private-googleapis = ["199.36.153.8/30"] restricted-googleapis = ["199.36.153.4/30"] rfc1918 = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] } + nullable = false } variable "network" { @@ -86,10 +115,3 @@ variable "project_id" { description = "Project id of the project that holds the network." type = string } - -variable "ssh_source_ranges" { - description = "List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range." - type = list(string) - default = ["35.235.240.0/20"] -} - diff --git a/modules/organization-policy/README.md b/modules/organization-policy/README.md deleted file mode 100644 index 3b91417045..0000000000 --- a/modules/organization-policy/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Google Cloud Organization Policy - -This module allows creation and management of [GCP Organization Policies](https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints) by defining them in a well formatted `yaml` files or with HCL. - -Yaml based factory can simplify centralized management of Org Policies for a DevSecOps team by providing a simple way to define/structure policies and exclusions. - -> **_NOTE:_** This module uses experimental feature `module_variable_optional_attrs` which will be included into [terraform release 1.3](https://github.com/hashicorp/terraform/releases/tag/v1.3.0-alpha20220706). - -## Example - -### Terraform code - -```hcl -# using configuration provided in a set of yaml files -module "org-policy-factory" { - source = "./fabric/modules/organization-policy" - - config_directory = "./policies" -} - -# using configuration provided in the module variable -module "org-policy" { - source = "./fabric/modules/organization-policy" - - policies = { - "folders/1234567890" = { - # enforce boolean policy with no conditions - "iam.disableServiceAccountKeyUpload" = { - rules = [ - { - enforce = true - } - ] - }, - # Deny All for compute.vmCanIpForward policy - "compute.vmCanIpForward" = { - inherit_from_parent = false - rules = [ - deny = [] # stands for deny_all - ] - } - }, - "organizations/1234567890" = { - # allow only internal ingress when match condition env=prod - "run.allowedIngress" = { - rules = [ - { - allow = ["internal"] - condition = { - description= "allow ingress" - expression = "resource.matchTag('123456789/environment', 'prod')" - title = "allow-for-prod-org" - } - } - ] - } - } - } -} -# tftest skip -``` - -## Org Policy definition format and structure - -### Structure of `policies` variable - -```hcl -policies = { - "parent_id" = { # parent id in format projects/project-id, folders/1234567890 or organizations/1234567890. - "policy_name" = { # policy constraint id, for example compute.vmExternalIpAccess. - inherit_from_parent = true|false # (Optional) Only for list constraints. Determines the inheritance behavior for this policy. - reset = true|false # (Optional) Ignores policies set above this resource and restores the constraint_default enforcement behavior. - rules = [ # Up to 10 PolicyRules are allowed. - { - allow = ["value1", "value2"] # (Optional) Only for list constraints. Stands for `allow_all` if set to empty list `[]` or to `values.allowed_values` if set to a list of values - denyl = ["value3", "value4"] # (Optional) Only for list constraints. Stands for `deny_all` if set to empty list `[]` or to `values.denied_values` if set to a list of values - enforce = true|false # (Optional) Only for boolean constraints. If true, then the Policy is enforced. - condition = { # (Optional) A condition which determines whether this rule is used in the evaluation of the policy. - description = "Condition description" # (Optional) - expression = "Condition expression" # (Optional) For example "resource.matchTag('123456789/environment', 'prod')". - location = "policy-error.log" # (Optional) String indicating the location of the expression for error reporting. - title = "condition-title" # (Optional) - } - } - ] - } - } -} -# tftest skip -``` - -### Structure of configuration provided in a yaml file/s - -Configuration should be placed in a set of yaml files in the config directory. Policy entry structure as follows: - -```yaml -parent_id: # parent id in format projects/project-id, folders/1234567890 or organizations/1234567890. - policy_name1: # policy constraint id, for example compute.vmExternalIpAccess. - inherit_from_parent: true|false # (Optional) Only for list constraints. Determines the inheritance behavior for this policy. - reset: true|false # (Optional) Ignores policies set above this resource and restores the constraint_default enforcement behavior. - rules: - - allow: ["value1", "value2"] # (Optional) Only for list constraints. Stands for `allow_all` if set to empty list `[]` or to `values.allowed_values` if set to a list of values - deny: ["value3", "value4"] # (Optional) Only for list constraints. Stands for `deny_all` if set to empty list `[]` or to `values.denied_values` if set to a list of values - enforce: true|false # (Optional) Only for boolean constraints. If true, then the Policy is enforced. - condition: # (Optional) A condition which determines whether this rule is used in the evaluation of the policy. - description: Condition description # (Optional) - expression: Condition expression # (Optional) For example resource.matchTag("123456789/environment", "prod") - location: policy-error.log # (Optional) String indicating the location of the expression for error reporting. - title: condition-title # (Optional) -``` - -Module allows policies to be distributed into multiple yaml files for a better management and navigation. - -```bash -├── org-policies -│ ├── baseline.yaml -│   ├── image-import-projects.yaml -│   └── exclusions.yaml -``` - -Organization policies example yaml configuration - -```bash -cat ./policies/baseline.yaml -organizations/1234567890: - constraints/compute.vmExternalIpAccess: - rules: - - deny: [] # Stands for deny_all = true -folders/1234567890: - compute.vmCanIpForward: - inherit_from_parent: false - reset: false - rules: - - allow: [] # Stands for allow_all = true -projects/my-project-id: - run.allowedIngress: - inherit_from_parent: true - rules: - - allow: ['internal'] # Stands for values.allowed_values - condition: - description: allow internal ingress - expression: resource.matchTag("123456789/environment", "prod") - location: test.log - title: allow-for-prod - iam.allowServiceAccountCredentialLifetimeExtension: - rules: - - deny: [] # Stands for deny_all = true - compute.disableGlobalLoadBalancing: - reset: true -``` - - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [config_directory](variables.tf#L17) | Paths to a folder where organization policy configs are stored in yaml format. Files suffix must be `.yaml`. | string | | null | -| [policies](variables.tf#L23) | Organization policies keyed by parent in format `projects/project-id`, `folders/1234567890` or `organizations/1234567890`. | map(map(object({…}))) | | {} | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| [policies](outputs.tf#L17) | Organization policies. | | - - diff --git a/modules/organization-policy/main.tf b/modules/organization-policy/main.tf deleted file mode 100644 index 960a8462eb..0000000000 --- a/modules/organization-policy/main.tf +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 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 { - policy_files = var.config_directory == null ? [] : concat( - [ - for config_file in fileset("${path.root}/${var.config_directory}", "**/*.yaml") : - "${path.root}/${var.config_directory}/${config_file}" - ] - ) - - policies_raw = merge( - merge( - [ - for config_file in local.policy_files : - try(yamldecode(file(config_file)), {}) - ]... - ), var.policies) - - policies_list = flatten([ - for parent, policies in local.policies_raw : [ - for policy_name, policy in policies : { - parent = parent, - policy_name = policy_name, - inherit_from_parent = try(policy["inherit_from_parent"], null), - reset = try(policy["reset"], null), - rules = [ - for rule in try(policy["rules"], []) : { - allow_all = try(length(rule["allow"]), -1) == 0 ? "TRUE" : null - deny_all = try(length(rule["deny"]), -1) == 0 ? "TRUE" : null - enforce = try(rule["enforce"], null) == true ? "TRUE" : try( - rule["enforce"], null) == false ? "FALSE" : null, - condition = try(rule["condition"], null) != null ? { - description = try(rule["condition"]["description"], null), - expression = try(rule["condition"]["expression"], null), - location = try(rule["condition"]["location"], null), - title = try(rule["condition"]["title"], null) - } : null, - values = try(length(rule["allow"]), 0) > 0 || try(length(rule["deny"]), 0) > 0 ? { - allowed_values = try(length(rule["allow"]), 0) > 0 ? rule["allow"] : null - denied_values = try(length(rule["deny"]), 0) > 0 ? rule["deny"] : null - } : null - } - ] - } - ] - ]) - - policies_map = { - for item in local.policies_list : - format("%s-%s", item["parent"], item["policy_name"]) => item - } -} - -resource "google_org_policy_policy" "primary" { - for_each = local.policies_map - name = format("%s/policies/%s", each.value.parent, each.value.policy_name) - parent = each.value.parent - - spec { - inherit_from_parent = each.value.inherit_from_parent - reset = each.value.reset - dynamic "rules" { - for_each = each.value.rules - content { - allow_all = rules.value.allow_all - deny_all = rules.value.deny_all - enforce = rules.value.enforce - dynamic "condition" { - for_each = rules.value.condition != null ? [""] : [] - content { - description = rules.value.condition.description - expression = rules.value.condition.expression - location = rules.value.condition.location - title = rules.value.condition.title - } - } - dynamic "values" { - for_each = rules.value.values != null ? [""] : [] - content { - allowed_values = rules.value.values.allowed_values - denied_values = rules.value.values.denied_values - } - } - } - } - } -} diff --git a/modules/organization-policy/variables.tf b/modules/organization-policy/variables.tf deleted file mode 100644 index ff842dd98f..0000000000 --- a/modules/organization-policy/variables.tf +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 "config_directory" { - description = "Paths to a folder where organization policy configs are stored in yaml format. Files suffix must be `.yaml`." - type = string - default = null -} - -variable "policies" { - description = "Organization policies keyed by parent in format `projects/project-id`, `folders/1234567890` or `organizations/1234567890`." - type = map(map(object({ - inherit_from_parent = optional(bool) # List policy only. - reset = optional(bool) - rules = optional( - list(object({ - allow = optional(list(string)) # List policy only. Stands for `allow_all` if set to empty list `[]` or to `values.allowed_values` if set to a list of values - deny = optional(list(string)) # List policy only. Stands for `deny_all` if set to empty list `[]` or to `values.denied_values` if set to a list of values - enforce = optional(bool) # Boolean policy only. - condition = optional( - object({ - description = optional(string) - expression = optional(string) - location = optional(string) - title = optional(string) - }) - ) - })) - ) - }))) - default = {} -} diff --git a/modules/organization/README.md b/modules/organization/README.md index 3c57b74349..95c4e2e837 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -6,6 +6,7 @@ This module allows managing several organization properties: - custom IAM roles - audit logging configuration for services - organization policies +- organization policy custom constraints To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -22,7 +23,21 @@ module "org" { "roles/resourcemanager.projectCreator" = ["group:cloud-admins@example.org"] } + org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + } + } + org_policies = { + "custom.gkeEnableAutoUpgrade" = { + enforce = true + } "compute.disableGuestAttributesAccess" = { enforce = true } @@ -61,7 +76,7 @@ module "org" { } } } -# tftest modules=1 resources=10 +# tftest modules=1 resources=12 ``` ## IAM @@ -74,11 +89,100 @@ There are several mutually exclusive ways of managing IAM in this module If you set audit policies via the `iam_audit_config_authoritative` variable, be sure to also configure IAM bindings via `iam_bindings_authoritative`, as audit policies use the underlying `google_organization_iam_policy` resource, which is also authoritative for any role. -Some care must also be takend with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph. +Some care must also be taken with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph. + +### Organization policy factory + +See the [organization policy factory in the project module](../project#organization-policy-factory). + +### Org policy custom constraints + +Refer to the [Creating and managing custom constraints](https://cloud.google.com/resource-manager/docs/organization-policy/creating-managing-custom-constraints) documentation for details on usage. +To manage organization policy custom constraints, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + + org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + } + } + + # not necessarily to enforce on the org level, policy may be applied on folder/project levels + org_policies = { + "custom.gkeEnableAutoUpgrade" = { + enforce = true + } + } +} +# tftest modules=1 resources=2 +``` + +### Org policy custom constraints factory + +Org policy custom constraints can be loaded from a directory containing YAML files where each file defines one or more custom constraints. The structure of the YAML files is exactly the same as the `org_policy_custom_constraints` variable. + +The example below deploys a few org policy custom constraints split between two YAML files. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + + org_policy_custom_constraints_data_path = "/my/path" + +} +# tftest skip +``` + +```yaml +# /my/path/gke.yaml +custom.gkeEnableLogging: + resource_types: + - container.googleapis.com/Cluster + method_types: + - CREATE + - UPDATE + condition: resource.loggingService == "none" + action_type: DENY + display_name: Do not disable Cloud Logging +custom.gkeEnableAutoUpgrade: + resource_types: + - container.googleapis.com/NodePool + method_types: + - CREATE + condition: resource.management.autoUpgrade == true + action_type: ALLOW + display_name: Enable node auto-upgrade + description: All node pools must have node auto-upgrade enabled. +``` + +```yaml +# /my/path/dataproc.yaml + +custom.dataprocNoMoreThan10Workers + resource_types: + - dataproc.googleapis.com/Cluster + method_types: + - CREATE + - UPDATE + condition: resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10 + action_type: DENY + display_name: Total number of worker instances cannot be larger than 10 + description: Cluster cannot have more than 10 workers, including primary and secondary workers. +``` ## Hierarchical firewall policies -Hirerarchical firewall policies can be managed in two ways: +Hierarchical firewall policies can be managed in two ways: - via the `firewall_policies` variable, to directly define policies and rules in Terraform - via the `firewall_policy_factory` variable, to leverage external YaML files via a simple "factory" embedded in the module ([see here](../../blueprints/factories) for more context on factories) @@ -310,6 +414,7 @@ module "org" { | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_audit_config · google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member · google_organization_iam_policy | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_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_essential_contacts_contact | +| [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 | @@ -320,7 +425,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L191) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L217) | 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)) | | {} | | [firewall_policies](variables.tf#L31) | Hierarchical firewall policy rules created in the organization. | map(map(object({…}))) | | {} | @@ -336,8 +441,11 @@ module "org" { | [logging_exclusions](variables.tf#L122) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L129) | Logging sinks to create for this organization. | map(object({…})) | | {} | | [org_policies](variables.tf#L151) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L200) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L206) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | +| [org_policies_data_path](variables.tf#L191) | Path containing org policies in YAML format. | string | | null | +| [org_policy_custom_constraints](variables.tf#L197) | Organization policiy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policy_custom_constraints_data_path](variables.tf#L211) | Path containing org policy custom constraints in YAML format. | string | | null | +| [tag_bindings](variables.tf#L227) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L233) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | ## Outputs diff --git a/modules/organization/org-policy-custom-constraints.tf b/modules/organization/org-policy-custom-constraints.tf new file mode 100644 index 0000000000..bfc2de1bce --- /dev/null +++ b/modules/organization/org-policy-custom-constraints.tf @@ -0,0 +1,62 @@ +/** + * 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 { + _custom_constraints_factory_data_raw = ( + var.org_policy_custom_constraints_data_path == null + ? tomap({}) + : tomap(merge([ + for f in fileset(var.org_policy_custom_constraints_data_path, "*.yaml") : + yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}")) + ]...)) + ) + + _custom_constraints_factory_data = { + for k, v in local._custom_constraints_factory_data_raw : + k => { + display_name = try(v.display_name, null) + description = try(v.description, null) + action_type = v.action_type + condition = v.condition + method_types = v.method_types + resource_types = v.resource_types + } + } + + _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints) + + custom_constraints = { + for k, v in local._custom_constraints : + k => merge(v, { + name = k + parent = var.organization_id + }) + } +} + +resource "google_org_policy_custom_constraint" "constraint" { + provider = google-beta + + for_each = local.custom_constraints + name = each.value.name + parent = each.value.parent + display_name = each.value.display_name + description = each.value.description + action_type = each.value.action_type + condition = each.value.condition + method_types = each.value.method_types + resource_types = each.value.resource_types +} diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index defa11b0bd..425e8f520b 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -17,9 +17,61 @@ # tfdoc:file:description Organization-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { + name = "${var.organization_id}/policies/${k}" + parent = var.organization_id + is_boolean_policy = v.allow == null && v.deny == null has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || @@ -40,8 +92,8 @@ locals { resource "google_org_policy_policy" "default" { for_each = local.org_policies - name = "${var.organization_id}/policies/${each.key}" - parent = var.organization_id + name = each.value.name + parent = each.value.parent spec { inherit_from_parent = each.value.inherit_from_parent @@ -98,6 +150,6 @@ resource "google_org_policy_policy" "default" { google_organization_iam_custom_role.roles, google_organization_iam_member.additive, google_organization_iam_policy.authoritative, + google_org_policy_custom_constraint.constraint, ] - } diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 7499d6d676..5b98a9e1e1 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -188,6 +188,32 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "Path containing org policies in YAML format." + type = string + default = null +} + +variable "org_policy_custom_constraints" { + description = "Organization policiy custom constraints keyed by constraint name." + type = map(object({ + display_name = optional(string) + description = optional(string) + action_type = string + condition = string + method_types = list(string) + resource_types = list(string) + })) + default = {} + nullable = false +} + +variable "org_policy_custom_constraints_data_path" { + description = "Path containing org policy custom constraints in YAML format." + type = string + default = null +} + variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string @@ -197,6 +223,7 @@ variable "organization_id" { } } + variable "tag_bindings" { description = "Tag bindings for this organization, in key => tag value id format." type = map(string) diff --git a/modules/project/README.md b/modules/project/README.md index dbb66fcd1f..37af720c97 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -211,6 +211,71 @@ module "project" { # tftest modules=1 resources=10 ``` +### Organization policy factory + +Organization policies can be loaded from a directory containing YAML files where each file defines one or more constraints. The structure of the YAML files is exactly the same as the `org_policies` variable. + +Note that contraints defined via `org_policies` take precedence over those in `org_policies_data_path`. In other words, if you specify the same contraint in a YAML file *and* in the `org_policies` variable, the latter will take priority. + +The example below deploys a few organization policies split between two YAML files. + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = "organizations/1234567890" + name = "Folder name" + org_policies_data_path = "/my/path" +} +# tftest skip +``` + +```yaml +# /my/path/boolean.yaml +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: false + rules: + - condition: + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + title: condition + description: test condition + location: xxx + enforce: true +``` + +```yaml +# /my/path/list.yaml +compute.vmExternalIpAccess: + deny: + all: true + +iam.allowedPolicyMemberDomains: + allow: + values: + - C0xxxxxxx + - C0yyyyyyy + +compute.restrictLoadBalancerCreationForTypes: + deny: + values: ["in:EXTERNAL"] + rules: + - condition: + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + title: condition + description: test condition + allow: + values: ["in:EXTERNAL"] + - condition: + expression: resource.matchTagId("tagKeys/12345", "tagValues/12345") + title: condition2 + description: test condition2 + allow: + all: true +``` + + ## Logging Sinks ```hcl @@ -407,21 +472,22 @@ output "compute_robot" { | [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L124) | List of projects that will act as metric scopes for this project. | list(string) | | [] | | [org_policies](variables.tf#L136) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [oslogin](variables.tf#L176) | Enable OS Login. | bool | | false | -| [oslogin_admins](variables.tf#L182) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | -| [oslogin_users](variables.tf#L190) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | -| [parent](variables.tf#L197) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L207) | Prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L213) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L219) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L231) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L238) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L245) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L251) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L257) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L266) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | -| [skip_delete](variables.tf#L276) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L282) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [org_policies_data_path](variables.tf#L176) | Path containing org policies in YAML format. | string | | null | +| [oslogin](variables.tf#L182) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L188) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L196) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L203) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L213) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L219) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L225) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L237) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L244) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L251) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L257) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L263) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L272) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L282) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L288) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index ae4a85012e..d64dd95608 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -17,9 +17,61 @@ # tfdoc:file:description Project-level organization policies. locals { + _factory_data_raw = ( + var.org_policies_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policies_data_path, "*.yaml") : + yamldecode(file("${var.org_policies_data_path}/${f}")) + ]...) + ) + + # simulate applying defaults to data coming from yaml files + _factory_data = { + for k, v in local._factory_data_raw : + k => { + inherit_from_parent = try(v.inherit_from_parent, null) + reset = try(v.reset, null) + allow = can(v.allow) ? { + all = try(v.allow.all, null) + values = try(v.allow.values, null) + } : null + deny = can(v.deny) ? { + all = try(v.deny.all, null) + values = try(v.deny.values, null) + } : null + enforce = try(v.enforce, true) + + rules = [ + for r in try(v.rules, []) : { + allow = can(r.allow) ? { + all = try(r.allow.all, null) + values = try(r.allow.values, null) + } : null + deny = can(r.deny) ? { + all = try(r.deny.all, null) + values = try(r.deny.values, null) + } : null + enforce = try(r.enforce, true) + condition = { + description = try(r.condition.description, null) + expression = try(r.condition.expression, null) + location = try(r.condition.location, null) + title = try(r.condition.title, null) + } + } + ] + } + } + + _org_policies = merge(local._factory_data, var.org_policies) + org_policies = { - for k, v in var.org_policies : + for k, v in local._org_policies : k => merge(v, { + name = "projects/${local.project.project_id}/policies/${k}" + parent = "projects/${local.project.project_id}" + is_boolean_policy = v.allow == null && v.deny == null has_values = ( length(coalesce(try(v.allow.values, []), [])) > 0 || @@ -40,8 +92,8 @@ locals { resource "google_org_policy_policy" "default" { for_each = local.org_policies - name = "projects/${local.project.project_id}/policies/${each.key}" - parent = "projects/${local.project.project_id}" + name = each.value.name + parent = each.value.parent spec { inherit_from_parent = each.value.inherit_from_parent diff --git a/modules/project/variables.tf b/modules/project/variables.tf index a58affc92f..7cd36c82be 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -173,6 +173,12 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "Path containing org policies in YAML format." + type = string + default = null +} + variable "oslogin" { description = "Enable OS Login." type = bool diff --git a/stages.png b/stages.png deleted file mode 100644 index 83f3c7e8e3..0000000000 Binary files a/stages.png and /dev/null differ diff --git a/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py b/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py index 7d8a47b158..6e2c141da5 100644 --- a/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py +++ b/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py @@ -14,12 +14,11 @@ import os - FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') def test_resources(e2e_plan_runner): - "Test that plan works and the numbers of resources is as expected." - modules, resources = e2e_plan_runner(FIXTURES_DIR) - assert len(modules) == 4 - assert len(resources) == 18 + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner(FIXTURES_DIR) + assert len(modules) == 4 + assert len(resources) == 20 diff --git a/tests/conftest.py b/tests/conftest.py index a5ded0707f..ab4e00e6b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,8 +28,8 @@ def _plan_runner(): "Returns a function to run Terraform plan on a fixture." - def run_plan(fixture_path=None, targets=None, refresh=True, tmpdir=True, - **tf_vars): + def run_plan(fixture_path=None, extra_files=None, tf_var_file=None, + targets=None, refresh=True, tmpdir=True, **tf_vars): "Runs Terraform plan and returns parsed output." if fixture_path is None: # find out the fixture directory from the caller's directory @@ -46,9 +46,9 @@ def run_plan(fixture_path=None, targets=None, refresh=True, tmpdir=True, shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True) tf = tftest.TerraformTest(tmp_path if tmpdir else fixture_path, BASEDIR, os.environ.get('TERRAFORM', 'terraform')) - tf.setup(upgrade=True) - plan = tf.plan(output=True, refresh=refresh, tf_vars=tf_vars, - targets=targets) + tf.setup(extra_files=extra_files, upgrade=True) + plan = tf.plan(output=True, refresh=refresh, tf_var_file=tf_var_file, + tf_vars=tf_vars, targets=targets) return plan return run_plan @@ -58,9 +58,11 @@ def run_plan(fixture_path=None, targets=None, refresh=True, tmpdir=True, def plan_runner(_plan_runner): "Returns a function to run Terraform plan on a module fixture." - def run_plan(fixture_path=None, targets=None, **tf_vars): + def run_plan(fixture_path=None, extra_files=None, tf_var_file=None, + targets=None, **tf_vars): "Runs Terraform plan and returns plan and module resources." - plan = _plan_runner(fixture_path, targets=targets, **tf_vars) + plan = _plan_runner(fixture_path, extra_files=extra_files, + tf_var_file=tf_var_file, targets=targets, **tf_vars) # skip the fixture root_module = plan.root_module['child_modules'][0] return plan, root_module['resources'] @@ -98,8 +100,8 @@ def recursive_e2e_plan_runner(_plan_runner): def walk_plan(node, modules, resources): # TODO(jccb): this would be better with node.get() but # TerraformPlanOutput objects don't have it - new_modules = node['child_modules'] if 'child_modules' in node else [] - resources += node['resources'] if 'resources' in node else [] + new_modules = node.get('child_modules', []) + resources += node.get('resources', []) modules += new_modules for module in new_modules: walk_plan(module, modules, resources) diff --git a/tests/modules/organization_policy/__init__.py b/tests/modules/cloud_function_v2/__init__.py similarity index 100% rename from tests/modules/organization_policy/__init__.py rename to tests/modules/cloud_function_v2/__init__.py diff --git a/modules/organization-policy/versions.tf b/tests/modules/cloud_function_v2/fixture/bundle/main.py similarity index 61% rename from modules/organization-policy/versions.tf rename to tests/modules/cloud_function_v2/fixture/bundle/main.py index 286536a65e..6d6d1266c3 100644 --- a/modules/organization-policy/versions.tf +++ b/tests/modules/cloud_function_v2/fixture/bundle/main.py @@ -4,26 +4,10 @@ # 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 +# 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. - -terraform { - required_version = ">= 1.3.1" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 4.40.0" # tftest - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest - } - } -} - - diff --git a/tests/modules/cloud_function_v2/fixture/main.tf b/tests/modules/cloud_function_v2/fixture/main.tf new file mode 100644 index 0000000000..db99d523b2 --- /dev/null +++ b/tests/modules/cloud_function_v2/fixture/main.tf @@ -0,0 +1,31 @@ +/** + * 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. + */ + +module "test" { + source = "../../../../modules/cloud-function" + project_id = "my-project" + name = "test" + bucket_name = var.bucket_name + v2 = true + bundle_config = { + source_dir = "bundle" + output_path = "bundle.zip" + excludes = null + } + iam = { + "roles/cloudfunctions.invoker" = ["allUsers"] + } +} diff --git a/tests/modules/organization_policy/fixture/main.tf b/tests/modules/cloud_function_v2/fixture/variables.tf similarity index 79% rename from tests/modules/organization_policy/fixture/main.tf rename to tests/modules/cloud_function_v2/fixture/variables.tf index 09a09267ce..600840858c 100644 --- a/tests/modules/organization_policy/fixture/main.tf +++ b/tests/modules/cloud_function_v2/fixture/variables.tf @@ -14,9 +14,7 @@ * limitations under the License. */ -module "org-policy" { - source = "../../../../modules/organization-policy" - - config_directory = var.config_directory - policies = var.policies +variable "bucket_name" { + type = string + default = "test" } diff --git a/tests/modules/cloud_function_v2/test_plan.py b/tests/modules/cloud_function_v2/test_plan.py new file mode 100644 index 0000000000..de5a06a9d5 --- /dev/null +++ b/tests/modules/cloud_function_v2/test_plan.py @@ -0,0 +1,34 @@ +# 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. + +import pytest + + +@pytest.fixture +def resources(plan_runner): + _, resources = plan_runner() + return resources + + +def test_resource_count(resources): + "Test number of resources created." + assert len(resources) == 3 + + +def test_iam(resources): + "Test IAM binding resources." + bindings = [r['values'] for r in resources if r['type'] + == 'google_cloudfunctions_function_iam_binding'] + assert len(bindings) == 1 + assert bindings[0]['role'] == 'roles/cloudfunctions.invoker' diff --git a/tests/modules/conftest.py b/tests/modules/conftest.py index 3d559e6f0e..a34cef6562 100644 --- a/tests/modules/conftest.py +++ b/tests/modules/conftest.py @@ -12,9 +12,45 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect +import pathlib + +import hcl2 import pytest +import yaml def pytest_collection_modifyitems(config, items): for item in items: item.add_marker(pytest.mark.xdist_group(name=item.path.parent.name)) + + +@pytest.fixture(scope='session') +def tfvars_to_yaml(): + + def converter(source, dest, from_var, to_var=None): + p_fixture = pathlib.Path(inspect.stack()[1].filename).parent / 'fixture' + p_source = p_fixture / source + if not p_source.exists(): + raise ValueError(f"tfvars '{source}' not found") + try: + with p_source.open() as f: + data = hcl2.load(f) + except Exception as e: + raise ValueError(f'error decoding tfvars: {e.args[0]}') + if from_var not in data: + raise ValueError(f"variable '{from_var}' not in tfvars") + if to_var is None: + data_yaml = data[from_var] + else: + data_yaml = {to_var: data[from_var]} + p_dest = pathlib.Path(dest) if not isinstance(dest, pathlib.Path) else dest + try: + with p_dest.open('w') as f: + data_yaml = yaml.dump(data_yaml, f) + except yaml.YAMLError as e: + raise ValueError(f'error encoding data to yaml: {e.args[0]}') + except (IOError, OSError) as e: + raise ValueError(f"error writing '{dest}': {e.args[0]}") + + return converter diff --git a/tests/modules/dns/fixture/variables.tf b/tests/modules/dns/fixture/variables.tf index 0fc6871a10..8e55a287a1 100644 --- a/tests/modules/dns/fixture/variables.tf +++ b/tests/modules/dns/fixture/variables.tf @@ -50,7 +50,7 @@ variable "recordsets" { wrr_routing = [ { weight = 0.6, records = ["127.0.0.7"] }, { weight = 0.2, records = ["127.0.0.8"] }, - { weight = 0.2, records = ["10.10.0.9"] } + { weight = 0.2, records = ["127.0.0.9"] } ] } } diff --git a/tests/modules/dns/test_plan.py b/tests/modules/dns/test_plan.py index a5f7407bd1..5cc1ba7093 100644 --- a/tests/modules/dns/test_plan.py +++ b/tests/modules/dns/test_plan.py @@ -17,8 +17,9 @@ def test_private(plan_runner): "Test private zone with three recordsets." _, resources = plan_runner() assert len(resources) == 7 - assert set(r['type'] for r in resources) == set( - ['google_dns_record_set', 'google_dns_managed_zone']) + assert set(r['type'] for r in resources) == { + 'google_dns_record_set', 'google_dns_managed_zone' + } for r in resources: if r['type'] != 'google_dns_managed_zone': continue @@ -33,48 +34,49 @@ def test_private_recordsets(plan_runner): r['values'] for r in resources if r['type'] == 'google_dns_record_set' ] - assert set(r['name'] for r in recordsets) == set([ + assert set(r['name'] for r in recordsets) == { 'localhost.test.example.', 'local-host.test.example.', '*.test.example.', "test.example.", "geo.test.example.", "wrr.test.example." - ]) + } for r in recordsets: if r['name'] not in ['wrr.test.example.', 'geo.test.example.']: assert r['routing_policy'] == [] assert r['rrdatas'] != [] + +def test_routing_policies(plan_runner): + "Test recordsets with routing policies." + _, resources = plan_runner() + recordsets = [ + r['values'] for r in resources if r['type'] == 'google_dns_record_set' + ] geo_zone = [ r['values'] for r in resources if r['address'] == 'module.test.google_dns_record_set.cloud-geo-records["A geo"]' ][0] assert geo_zone['name'] == 'geo.test.example.' assert geo_zone['routing_policy'][0]['wrr'] == [] - assert geo_zone['routing_policy'][0]['geo'] == [{ - 'location': 'europe-west1', - 'rrdatas': ['127.0.0.4'] - }, { - 'location': 'europe-west2', - 'rrdatas': ['127.0.0.5'] - }, { - 'location': 'europe-west3', - 'rrdatas': ['127.0.0.6'] - }] + geo_policy = geo_zone['routing_policy'][0]['geo'] + assert geo_policy[0]['location'] == 'europe-west1' + assert geo_policy[0]['rrdatas'] == ['127.0.0.4'] + assert geo_policy[1]['location'] == 'europe-west2' + assert geo_policy[1]['rrdatas'] == ['127.0.0.5'] + assert geo_policy[2]['location'] == 'europe-west3' + assert geo_policy[2]['rrdatas'] == ['127.0.0.6'] wrr_zone = [ r['values'] for r in resources if r['address'] == 'module.test.google_dns_record_set.cloud-wrr-records["A wrr"]' ][0] assert wrr_zone['name'] == 'wrr.test.example.' - assert wrr_zone['routing_policy'][0]['wrr'] == [{ - 'rrdatas': ['127.0.0.7'], - 'weight': 0.6 - }, { - 'rrdatas': ['127.0.0.8'], - 'weight': 0.2 - }, { - 'rrdatas': ['10.10.0.9'], - 'weight': 0.2 - }] + wrr_policy = wrr_zone['routing_policy'][0]['wrr'] + assert wrr_policy[0]['weight'] == 0.6 + assert wrr_policy[0]['rrdatas'] == ['127.0.0.7'] + assert wrr_policy[1]['weight'] == 0.2 + assert wrr_policy[1]['rrdatas'] == ['127.0.0.8'] + assert wrr_policy[2]['weight'] == 0.2 + assert wrr_policy[2]['rrdatas'] == ['127.0.0.9'] assert wrr_zone['routing_policy'][0]['geo'] == [] diff --git a/tests/modules/folder/fixture/main.tf b/tests/modules/folder/fixture/main.tf index 8290f82ecf..a347f61bb2 100644 --- a/tests/modules/folder/fixture/main.tf +++ b/tests/modules/folder/fixture/main.tf @@ -27,4 +27,5 @@ module "test" { logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path } diff --git a/tests/modules/folder/fixture/variables.tf b/tests/modules/folder/fixture/variables.tf index 7c03e05683..e2d7a293b3 100644 --- a/tests/modules/folder/fixture/variables.tf +++ b/tests/modules/folder/fixture/variables.tf @@ -58,3 +58,8 @@ variable "org_policies" { type = any default = {} } + +variable "org_policies_data_path" { + type = any + default = null +} diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index f84d50fb06..0a6b972908 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -12,31 +12,106 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hcl2 +import yaml -def test_policy_boolean(plan_runner): - "Test boolean org policy." - policies = '''{ - "iam.disableServiceAccountKeyCreation" = { - enforce = true - } - "iam.disableServiceAccountKeyUpload" = { - enforce = false - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - enforce = true +BOOLEAN_POLICIES = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" } - ] + enforce = true + } + ] + } +}''' + +LIST_POLICIES = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] } - }''' - _, resources = plan_runner(org_policies=policies) - assert len(resources) == 3 + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } +}''' + + +def test_policy_boolean(plan_runner): + "Test boolean org policy." + _, resources = plan_runner(org_policies=BOOLEAN_POLICIES) + validate_policy_boolean_resources(resources) + +def test_policy_list(plan_runner): + "Test list org policy." + _, resources = plan_runner(org_policies=LIST_POLICIES) + validate_policy_list_resources(resources) + + +def test_policy_boolean_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {BOOLEAN_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean_resources(resources) + + +def test_policy_list_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {LIST_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list_resources(resources) + + +def validate_policy_boolean_resources(resources): + assert len(resources) == 3 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] assert len(policies) == 2 @@ -76,7 +151,7 @@ def test_policy_boolean(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -86,46 +161,7 @@ def test_policy_boolean(plan_runner): } -def test_policy_list(plan_runner): - "Test list org policy." - policies = '''{ - "compute.vmExternalIpAccess" = { - deny = { all = true } - } - "iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } - } - "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - allow = { - values = ["EXTERNAL_1"] - } - }, - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" - title = "condition2" - description = "test condition2" - location = "xxx" - } - allow = { - all = true - } - } - ] - } - }''' - _, resources = plan_runner(org_policies=policies) +def validate_policy_list_resources(resources): assert len(resources) == 4 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] @@ -193,7 +229,7 @@ def test_policy_list(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -208,14 +244,10 @@ def test_policy_list(plan_runner): assert p3['rules'][2] == { 'allow_all': 'TRUE', 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' }], 'deny_all': None, 'enforce': None, diff --git a/tests/modules/net_ilb/fixture/main.tf b/tests/modules/net_ilb/fixture/main.tf index c592421f93..66346efcba 100644 --- a/tests/modules/net_ilb/fixture/main.tf +++ b/tests/modules/net_ilb/fixture/main.tf @@ -15,21 +15,21 @@ */ module "test" { - source = "../../../../modules/net-ilb" - project_id = "my-project" - region = "europe-west1" - network = "default" - subnetwork = "default" - name = "ilb-test" - labels = {} - address = var.address - backends = var.backends - backend_config = var.backend_config - failover_config = var.failover_config - global_access = var.global_access - health_check = var.health_check - health_check_config = var.health_check_config - ports = var.ports - protocol = var.protocol - service_label = var.service_label + source = "../../../../modules/net-ilb" + project_id = "my-project" + region = "europe-west1" + name = "ilb-test" + vpc_config = { + network = "default" + subnetwork = "default" + } + address = var.address + backend_service_config = var.backend_service_config + backends = var.backends + description = var.description + global_access = var.global_access + group_configs = var.group_configs + ports = var.ports + protocol = var.protocol + service_label = var.service_label } diff --git a/tests/modules/net_ilb/fixture/variables.tf b/tests/modules/net_ilb/fixture/variables.tf index b00b49a6c3..2c2c2fb4ea 100644 --- a/tests/modules/net_ilb/fixture/variables.tf +++ b/tests/modules/net_ilb/fixture/variables.tf @@ -19,30 +19,20 @@ variable "address" { default = null } -variable "backends" { - type = list(object({ - failover = bool - group = string - balancing_mode = string - })) +variable "backend_service_config" { + description = "Backend service level configuration." + type = any + default = {} } -variable "backend_config" { - type = object({ - session_affinity = string - timeout_sec = number - connection_draining_timeout_sec = number - }) - default = null +variable "backends" { + type = any + default = [] } -variable "failover_config" { - type = object({ - disable_connection_drain = bool - drop_traffic_if_unhealthy = bool - ratio = number - }) - default = null +variable "description" { + type = string + default = "Terraform managed." } variable "global_access" { @@ -50,26 +40,9 @@ variable "global_access" { default = null } -variable "health_check" { - type = string - default = null -} - -variable "health_check_config" { - type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool - }) - default = { - type = "http" - check = { - port_specification = "USE_SERVING_PORT" - } - config = {} - logging = false - } +variable "group_configs" { + type = any + default = {} } variable "ports" { diff --git a/tests/modules/net_ilb/test_plan.py b/tests/modules/net_ilb/test_plan.py index 722ada3dad..9956b331b0 100644 --- a/tests/modules/net_ilb/test_plan.py +++ b/tests/modules/net_ilb/test_plan.py @@ -29,7 +29,7 @@ def test_defaults(plan_runner): assert backend['backend'][0]['group'] == 'foo' health_check = resources['google_compute_health_check'] for k, v in health_check.items(): - if k == 'http_health_check': + if k == 'tcp_health_check': assert len(v) == 1 assert v[0]['port_specification'] == 'USE_SERVING_PORT' elif k.endswith('_health_check'): @@ -38,12 +38,14 @@ def test_defaults(plan_runner): def test_forwarding_rule(plan_runner): "Test forwarding rule variables." - _, resources = plan_runner(backends=_BACKENDS, - global_access='true', + _, resources = plan_runner(backends=_BACKENDS, global_access='true', ports="[80]") assert len(resources) == 3 - values = [r['values'] for r in resources if r['type'] - == 'google_compute_forwarding_rule'][0] + values = [ + r['values'] + for r in resources + if r['type'] == 'google_compute_forwarding_rule' + ][0] assert not values['all_ports'] assert values['ports'] == ['80'] assert values['allow_global_access'] diff --git a/tests/modules/net_vpc/fixture/test.subnets.tfvars b/tests/modules/net_vpc/fixture/test.subnets.tfvars new file mode 100644 index 0000000000..499e498f4e --- /dev/null +++ b/tests/modules/net_vpc/fixture/test.subnets.tfvars @@ -0,0 +1,44 @@ +subnet_iam = { + "europe-west1/a" = { + "roles/compute.networkUser" = [ + "user:a@example.com", "group:g-a@example.com" + ] + } + "europe-west1/c" = { + "roles/compute.networkUser" = [ + "user:c@example.com", "group:g-c@example.com" + ] + } +} +subnets = [ + { + name = "a" + region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + }, + { + name = "b" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24", + description = "Subnet b" + enable_private_access = false + }, + { + name = "c" + region = "europe-west1" + ip_cidr_range = "10.0.2.0/24" + secondary_ip_ranges = { + a = "192.168.0.0/24" + b = "192.168.1.0/24" + } + }, + { + name = "d" + region = "europe-west1" + ip_cidr_range = "10.0.3.0/24" + flow_logs_config = { + flow_sampling = 0.5 + aggregation_interval = "INTERVAL_10_MIN" + } + } +] diff --git a/tests/modules/net_vpc/test_plan_subnets.py b/tests/modules/net_vpc/test_plan_subnets.py index 266d46de20..affea44c2c 100644 --- a/tests/modules/net_vpc/test_plan_subnets.py +++ b/tests/modules/net_vpc/test_plan_subnets.py @@ -13,33 +13,6 @@ # limitations under the License. DATA_FOLDER = "data" -SUBNET_IAM = '''{ - "europe-west1/a" = { - "roles/compute.networkUser" = ["user:a@example.com", "group:g-a@example.com"] - } - "europe-west1/c" = { - "roles/compute.networkUser" = ["user:c@example.com", "group:g-c@example.com"] - } -}''' -SUBNETS = '''[ - { - name = "a", region = "europe-west1", ip_cidr_range = "10.0.0.0/24" - }, - { - name = "b", region = "europe-west1", ip_cidr_range = "10.0.1.0/24", - description="Subnet b", enable_private_access=false - }, - { - name = "c", region = "europe-west1", ip_cidr_range = "10.0.2.0/24", - secondary_ip_ranges={a="192.168.0.0/24", b="192.168.1.0/24"} - }, - { - name = "d", region = "europe-west1", ip_cidr_range = "10.0.3.0/24", - flow_logs_config = { - flow_sampling = 0.5, aggregation_interval = "INTERVAL_10_MIN" - } - }, -]''' def test_subnet_factory(plan_runner): @@ -56,7 +29,7 @@ def test_subnet_factory(plan_runner): def test_subnets(plan_runner): "Test subnets variable." - _, resources = plan_runner(subnet_iam=SUBNET_IAM, subnets=SUBNETS) + _, resources = plan_runner(tf_var_file='test.subnets.tfvars') assert len(resources) == 7 subnets = [ r['values'] for r in resources if r['type'] == 'google_compute_subnetwork' diff --git a/tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml b/tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml index 508c9efe12..9e773273ad 100644 --- a/tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml +++ b/tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml @@ -12,17 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -allow-healthchecks: - description: Allow ingress from healthchecks. - direction: INGRESS - action: allow - sources: [] - ranges: - - $healthchecks - targets: ["lb-backends"] - use_service_accounts: false - rules: - - protocol: tcp - ports: - - 80 - - 443 +ingress: + allow-healthchecks: + description: Allow ingress from healthchecks. + source_ranges: + - healthchecks + targets: ["lb-backends"] + rules: + - protocol: tcp + ports: + - 80 + - 443 diff --git a/tests/modules/net_vpc_firewall/fixture/main.tf b/tests/modules/net_vpc_firewall/fixture/main.tf index 26237cd699..e69aeff105 100644 --- a/tests/modules/net_vpc_firewall/fixture/main.tf +++ b/tests/modules/net_vpc_firewall/fixture/main.tf @@ -15,14 +15,11 @@ */ module "firewall" { - source = "../../../../modules/net-vpc-firewall" - project_id = var.project_id - network = var.network - admin_ranges = var.admin_ranges - http_source_ranges = var.http_source_ranges - https_source_ranges = var.https_source_ranges - ssh_source_ranges = var.ssh_source_ranges - custom_rules = var.custom_rules - data_folder = var.data_folder - cidr_template_file = var.cidr_template_file + source = "../../../../modules/net-vpc-firewall" + project_id = "test-project" + network = "test-vpc" + default_rules_config = var.default_rules_config + egress_rules = var.egress_rules + ingress_rules = var.ingress_rules + factories_config = var.factories_config } diff --git a/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars b/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars new file mode 100644 index 0000000000..36944bea42 --- /dev/null +++ b/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars @@ -0,0 +1,22 @@ +egress_rules = { + allow-egress-rfc1918 = { + description = "Allow egress to RFC 1918 ranges." + is_egress = true + destination_ranges = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + } + deny-egress-all = { + description = "Block egress." + is_deny = true + is_egress = true + } +} +ingress_rules = { + allow-ingress-ntp = { + description = "Allow NTP service based on tag." + targets = ["ntp-svc"] + rules = [{ protocol = "udp", ports = [123] }] + } +} +default_rules_config = { + disabled = true +} diff --git a/tests/modules/net_vpc_firewall/fixture/variables.tf b/tests/modules/net_vpc_firewall/fixture/variables.tf index 8627bf8130..fd71e93b85 100644 --- a/tests/modules/net_vpc_firewall/fixture/variables.tf +++ b/tests/modules/net_vpc_firewall/fixture/variables.tf @@ -14,84 +14,38 @@ * limitations under the License. */ -variable "admin_ranges" { - description = "IP CIDR ranges that have complete access to all subnets." - type = list(string) - default = [] -} - -variable "cidr_template_file" { - description = "Path for optional file containing name->cidr_list map to be used by the rules factory." - type = string - default = null -} +/** + * 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 "custom_rules" { - description = "List of custom rule definitions (refer to variables file for syntax)." - type = map(object({ - description = string - direction = string - action = string # (allow|deny) - ranges = list(string) - sources = list(string) - targets = list(string) - use_service_accounts = bool - rules = list(object({ - protocol = string - ports = list(string) - })) - extra_attributes = map(string) - })) +variable "default_rules_config" { + type = any default = {} } -variable "data_folder" { - description = "Path for optional folder containing firewall rules defined as YaML objects used by the rules factory." - type = string - default = null -} - -variable "http_source_ranges" { - description = "List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges." - type = list(string) - default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] -} - -variable "https_source_ranges" { - description = "List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges." - type = list(string) - default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] -} - -variable "named_ranges" { - description = "Names that can be used of valid values for the `ranges` field of `custom_rules`." - type = map(list(string)) - default = { - any = ["0.0.0.0/0"] - dns-forwarders = ["35.199.192.0/19"] - health-checkers = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"] - iap-forwarders = ["35.235.240.0/20"] - private-googleapis = ["199.36.153.8/30"] - restricted-googleapis = ["199.36.153.4/30"] - rfc1918 = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } -} - -variable "network" { - description = "Name of the network this set of firewall rules applies to." - type = string - default = "vpc" +variable "egress_rules" { + type = any + default = {} } -variable "project_id" { - description = "Project id of the project that holds the network." - type = string - default = "project" +variable "factories_config" { + type = any + default = null } -variable "ssh_source_ranges" { - description = "List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range." - type = list(string) - default = ["35.235.240.0/20"] +variable "ingress_rules" { + type = any + default = {} } - diff --git a/tests/modules/net_vpc_firewall/test_plan.py b/tests/modules/net_vpc_firewall/test_plan.py index 52de2a4f39..f164d02c3f 100644 --- a/tests/modules/net_vpc_firewall/test_plan.py +++ b/tests/modules/net_vpc_firewall/test_plan.py @@ -12,27 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -def test_vpc_firewall_simple(plan_runner): - "Test vpc with no extra options." +import pytest + + +def test_defaults(plan_runner): + "Test variable defaults." _, resources = plan_runner() assert len(resources) == 3 - assert set([r['type'] for r in resources]) == set( - ['google_compute_firewall']) - assert set([r['values']['name'] for r in resources]) == set( - ['vpc-ingress-tag-http', 'vpc-ingress-tag-https', 'vpc-ingress-tag-ssh']) - assert set([r['values']['project'] for r in resources]) == set(['project']) - assert set([r['values']['network'] for r in resources]) == set(['vpc']) + assert set([r['type'] for r in resources]) == set(['google_compute_firewall']) + assert set([r['values']['name'] for r in resources]) == set([ + 'test-vpc-ingress-tag-http', 'test-vpc-ingress-tag-https', + 'test-vpc-ingress-tag-ssh' + ]) + assert set([r['values']['project'] for r in resources + ]) == set(['test-project']) + assert set([r['values']['network'] for r in resources]) == set(['test-vpc']) + + +def test_rules(plan_runner): + "Test custom rules." + _, resources = plan_runner(tf_var_file='test.rules.tfvars') + assert len(resources) == 3 + rules = {r['index']: r['values'] for r in resources} + rule = rules['allow-ingress-ntp'] + assert rule['source_ranges'] == ['0.0.0.0/0'] + assert rule['allow'] == [{'ports': ['123'], 'protocol': 'udp'}] + rule = rules['deny-egress-all'] + assert rule['destination_ranges'] == ['0.0.0.0/0'] + assert rule['deny'] == [{'ports': [], 'protocol': 'all'}] -def test_vpc_firewall_factory(plan_runner): - "Test shared vpc variables." - _, resources = plan_runner( - data_folder="config/firewall", - cidr_template_file="config/cidr_template.yaml" - ) +def test_factory(plan_runner): + "Test factory." + factories_config = '''{ + cidr_tpl_file = "config/cidr_template.yaml" + rules_folder = "config/firewall" + }''' + _, resources = plan_runner(factories_config=factories_config) assert len(resources) == 4 - factory_rule = [r for r in resources if r["values"] - ["name"] == "allow-healthchecks"][0]["values"] + factory_rule = [ + r for r in resources if r["values"]["name"] == "allow-healthchecks" + ][0]["values"] assert set(factory_rule["source_ranges"]) == set( ["130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22", "35.191.0.0/16"]) assert set(factory_rule["target_tags"]) == set(["lb-backends"]) diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 13f8e335f4..a620542e85 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -15,20 +15,23 @@ */ module "test" { - source = "../../../../modules/organization" - organization_id = "organizations/1234567890" - custom_roles = var.custom_roles - firewall_policies = var.firewall_policies - firewall_policy_association = var.firewall_policy_association - firewall_policy_factory = var.firewall_policy_factory - group_iam = var.group_iam - iam = var.iam - iam_additive = var.iam_additive - iam_additive_members = var.iam_additive_members - iam_audit_config = var.iam_audit_config - logging_sinks = var.logging_sinks - logging_exclusions = var.logging_exclusions - org_policies = var.org_policies - tag_bindings = var.tag_bindings - tags = var.tags + source = "../../../../modules/organization" + organization_id = "organizations/1234567890" + custom_roles = var.custom_roles + firewall_policies = var.firewall_policies + firewall_policy_association = var.firewall_policy_association + firewall_policy_factory = var.firewall_policy_factory + group_iam = var.group_iam + iam = var.iam + iam_additive = var.iam_additive + iam_additive_members = var.iam_additive_members + iam_audit_config = var.iam_audit_config + logging_sinks = var.logging_sinks + logging_exclusions = var.logging_exclusions + org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path + org_policy_custom_constraints = var.org_policy_custom_constraints + org_policy_custom_constraints_data_path = var.org_policy_custom_constraints_data_path + tag_bindings = var.tag_bindings + tags = var.tags } diff --git a/tests/modules/organization/fixture/test.orgpolicies-boolean.tfvars b/tests/modules/organization/fixture/test.orgpolicies-boolean.tfvars new file mode 100644 index 0000000000..eceafe6d29 --- /dev/null +++ b/tests/modules/organization/fixture/test.orgpolicies-boolean.tfvars @@ -0,0 +1,19 @@ +org_policies = { + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } +} diff --git a/tests/modules/organization/fixture/test.orgpolicies-list.tfvars b/tests/modules/organization/fixture/test.orgpolicies-list.tfvars new file mode 100644 index 0000000000..7380717336 --- /dev/null +++ b/tests/modules/organization/fixture/test.orgpolicies-list.tfvars @@ -0,0 +1,37 @@ +org_policies = { + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } +} diff --git a/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars b/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars new file mode 100644 index 0000000000..a02f97c485 --- /dev/null +++ b/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars @@ -0,0 +1,18 @@ +org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + }, + "custom.dataprocNoMoreThan10Workers" = { + resource_types = ["dataproc.googleapis.com/Cluster"] + method_types = ["CREATE", "UPDATE"] + condition = "resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10" + action_type = "DENY" + display_name = "Total number of worker instances cannot be larger than 10" + description = "Cluster cannot have more than 10 workers, including primary and secondary workers." + } +} diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf index f56e51dcc7..c4efa8fb88 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -74,6 +74,21 @@ variable "org_policies" { default = {} } +variable "org_policies_data_path" { + type = any + default = null +} + +variable "org_policy_custom_constraints" { + type = any + default = {} +} + +variable "org_policy_custom_constraints_data_path" { + type = any + default = null +} + variable "tag_bindings" { type = any default = null diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 63ff2e7637..05550832bc 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -12,216 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pathlib -def test_policy_boolean(plan_runner): - "Test boolean org policy." - policies = '''{ - "iam.disableServiceAccountKeyCreation" = { - enforce = true - } - "iam.disableServiceAccountKeyUpload" = { - enforce = false - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - enforce = true - } - ] - } - }''' - _, resources = plan_runner(org_policies=policies) - assert len(resources) == 2 - - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 2 - assert all( - x['values']['parent'] == 'organizations/1234567890' for x in policies) - - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyCreation' - ][0] - - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - }] +from .validate_policies import validate_policy_boolean, validate_policy_list, validate_policy_custom_constraints - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyUpload' - ][0] - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert len(p2['rules']) == 2 - assert p2['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'FALSE', - 'values': [] - } - assert p2['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - } +def test_policy_boolean(plan_runner): + "Test boolean org policy." + tfvars = 'test.orgpolicies-boolean.tfvars' + _, resources = plan_runner(tf_var_file=tfvars) + validate_policy_boolean(resources) def test_policy_list(plan_runner): "Test list org policy." - policies = '''{ - "compute.vmExternalIpAccess" = { - deny = { all = true } - } - "iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } - } - "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - allow = { - values = ["EXTERNAL_1"] - } - }, - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" - title = "condition2" - description = "test condition2" - location = "xxx" - } - allow = { - all = true - } - } - ] - } - }''' - _, resources = plan_runner(org_policies=policies) - assert len(resources) == 3 + tfvars = 'test.orgpolicies-list.tfvars' + _, resources = plan_runner(tf_var_file=tfvars) + validate_policy_list(resources) + + +def test_policy_custom_constraints(plan_runner): + "Test org policy custom constraints." + tfvars = 'test.orgpolicy-custom-constraints.tfvars' + _, resources = plan_runner(tf_var_file=tfvars) + validate_policy_custom_constraints(resources) - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 3 - assert all( - x['values']['parent'] == 'organizations/1234567890' for x in policies) - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.vmExternalIpAccess' - ][0] - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': 'TRUE', - 'enforce': None, - 'values': [] - }] +def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path): + dest = tmp_path / 'policies.yaml' + tfvars_to_yaml('test.orgpolicies-boolean.tfvars', dest, 'org_policies') + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean(resources) - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.allowedPolicyMemberDomains' - ][0] - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert p2['rules'] == [{ - 'allow_all': - None, - 'condition': [], - 'deny_all': - None, - 'enforce': - None, - 'values': [{ - 'allowed_values': [ - 'C0xxxxxxx', - 'C0yyyyyyy', - ], - 'denied_values': None - }] - }] - p3 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' - ][0] - assert p3['inherit_from_parent'] is None - assert p3['reset'] is None - assert len(p3['rules']) == 3 - assert p3['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': None, - 'denied_values': ['in:EXTERNAL'] - }] - } +def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path): + dest = tmp_path / 'policies.yaml' + tfvars_to_yaml('test.orgpolicies-list.tfvars', dest, 'org_policies') + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list(resources) - assert p3['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': ['EXTERNAL_1'], - 'denied_values': None - }] - } - assert p3['rules'][2] == { - 'allow_all': 'TRUE', - 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' - }], - 'deny_all': None, - 'enforce': None, - 'values': [] - } +def test_factory_policy_custom_constraints(plan_runner, tfvars_to_yaml, tmp_path): + dest = tmp_path / 'constraints.yaml' + tfvars_to_yaml('test.orgpolicy-custom-constraints.tfvars', dest, 'org_policy_custom_constraints') + _, resources = plan_runner(org_policy_custom_constraints_data_path=f'"{tmp_path}"') + validate_policy_custom_constraints(resources) diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py new file mode 100644 index 0000000000..d2a5e097ea --- /dev/null +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -0,0 +1,92 @@ +# 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. + +import difflib +import pathlib + + +def test_policy_implementation(): + '''Verify org policy implementation is the same (except minor + differences) in the organization, folder and project modules.''' + + modules_path = pathlib.Path(__file__).parents[3] / 'modules' + lines = {} + for module in ['project', 'folder', 'organization']: + path = modules_path / module / 'organization-policies.tf' + lines[module] = path.open().readlines() + + diff1 = difflib.unified_diff(lines['project'], lines['folder']) + assert list(diff1) == [ + '--- \n', + '+++ \n', + '@@ -14,7 +14,7 @@\n', + ' * limitations under the License.\n', + ' */\n', + ' \n', + '-# tfdoc:file:description Project-level organization policies.\n', + '+# tfdoc:file:description Folder-level organization policies.\n', + ' \n', + ' locals {\n', + ' _factory_data_raw = (\n', + '@@ -69,8 +69,8 @@\n', + ' org_policies = {\n', + ' for k, v in local._org_policies :\n', + ' k => merge(v, {\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', + ' \n', + ' is_boolean_policy = v.allow == null && v.deny == null\n', + ' has_values = (\n', + ] + + diff2 = difflib.unified_diff(lines['folder'], lines['organization']) + assert list(diff2) == [ + '--- \n', + '+++ \n', + '@@ -14,7 +14,7 @@\n', + ' * limitations under the License.\n', + ' */\n', + ' \n', + '-# tfdoc:file:description Folder-level organization policies.\n', + '+# tfdoc:file:description Organization-level organization policies.\n', + ' \n', + ' locals {\n', + ' _factory_data_raw = (\n', + '@@ -69,8 +69,8 @@\n', + ' org_policies = {\n', + ' for k, v in local._org_policies :\n', + ' k => merge(v, {\n', + '- name = "${local.folder.name}/policies/${k}"\n', + '- parent = local.folder.name\n', + '+ name = "${var.organization_id}/policies/${k}"\n', + '+ parent = var.organization_id\n', ' \n', + ' is_boolean_policy = v.allow == null && v.deny == null\n', + ' has_values = (\n', + '@@ -143,4 +143,13 @@\n', + ' }\n', + ' }\n', + ' }\n', + '+\n', + '+ depends_on = [\n', + '+ google_organization_iam_audit_config.config,\n', + '+ google_organization_iam_binding.authoritative,\n', + '+ google_organization_iam_custom_role.roles,\n', + '+ google_organization_iam_member.additive,\n', + '+ google_organization_iam_policy.authoritative,\n', + '+ google_org_policy_custom_constraint.constraint,\n', + '+ ]\n', + ' }\n' + ] diff --git a/tests/modules/organization/validate_policies.py b/tests/modules/organization/validate_policies.py new file mode 100644 index 0000000000..51844b15ea --- /dev/null +++ b/tests/modules/organization/validate_policies.py @@ -0,0 +1,162 @@ +# 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. + + +def validate_policy_boolean(resources): + assert len(resources) == 2 + assert all( + r['values']['parent'] == 'organizations/1234567890' for r in resources) + policies = { + r['index']: r['values']['spec'][0] + for r in resources + if r['type'] == 'google_org_policy_policy' + } + assert len(policies) == 2 + p1 = policies['iam.disableServiceAccountKeyCreation'] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + p2 = policies['iam.disableServiceAccountKeyUpload'] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId(aa, bb)', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } + + +def validate_policy_list(resources): + assert len(resources) == 3 + assert all( + r['values']['parent'] == 'organizations/1234567890' for r in resources) + policies = { + r['index']: r['values']['spec'][0] + for r in resources + if r['type'] == 'google_org_policy_policy' + } + assert len(policies) == 3 + p1 = policies['compute.vmExternalIpAccess'] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + p2 = policies['iam.allowedPolicyMemberDomains'] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + p3 = policies['compute.restrictLoadBalancerCreationForTypes'] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId(aa, bb)', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + } + +def validate_policy_custom_constraints(resources): + assert len(resources) == 2 + assert all( + r['values']['parent'] == 'organizations/1234567890' for r in resources) + constraints = { + r['index']: r['values'] + for r in resources + if r['type'] == 'google_org_policy_custom_constraint' + } + assert len(constraints) == 2 + c1 = constraints['custom.gkeEnableAutoUpgrade'] + assert c1['resource_types'][0] == 'container.googleapis.com/NodePool' + assert c1['method_types'] == ['CREATE'] + assert c1['condition'] == 'resource.management.autoUpgrade == true' + assert c1['action_type'] == 'ALLOW' + + c2 = constraints['custom.dataprocNoMoreThan10Workers'] + assert c2['resource_types'][0] == 'dataproc.googleapis.com/Cluster' + assert c2['method_types'] == ['CREATE', 'UPDATE'] + assert c2['condition'] == 'resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10' + assert c2['action_type'] == 'DENY' diff --git a/tests/modules/organization_policy/fixture/policies/test.yaml b/tests/modules/organization_policy/fixture/policies/test.yaml deleted file mode 100644 index 4b81e524cf..0000000000 --- a/tests/modules/organization_policy/fixture/policies/test.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# 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. - - -organizations/1234567890: - constraints/compute.vmExternalIpAccess: - rules: - - deny_all: true -folders/1234567890: - compute.vmCanIpForward: - inherit_from_parent: false - reset: false - rules: - - allow: [] -projects/my-project-id: - run.allowedIngress: - inherit_from_parent: true - rules: - - allow: ['internal'] - condition: - description: allow internal ingress - expression: resource.matchTag("123456789/environment", "prod") - location: test.log - title: allow-for-prod - iam.allowServiceAccountCredentialLifetimeExtension: - rules: - - deny: [] - compute.disableGlobalLoadBalancing: - reset: true diff --git a/tests/modules/organization_policy/fixture/variables.tf b/tests/modules/organization_policy/fixture/variables.tf deleted file mode 100644 index 8196bcff34..0000000000 --- a/tests/modules/organization_policy/fixture/variables.tf +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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 "config_directory" { - description = "Paths to a folder where organization policy configs are stored in yaml format. Files suffix must be `.yaml`." - type = string - default = null -} - -variable "policies" { - description = "Organization policies keyed by parent in format `projects/project-id`, `folders/1234567890` or `organizations/1234567890`." - type = map(map(object({ - inherit_from_parent = optional(bool) # List policy only. - reset = optional(bool) - rules = optional( - list(object({ - allow = optional(list(string)) # List policy only. Stands for `allow_all` if set to empty list `[]` or to `values.allowed_values` if set to a list of values - deny = optional(list(string)) # List policy only. Stands for `deny_all` if set to empty list `[]` or to `values.denied_values` if set to a list of values - enforce = optional(bool) # Boolean policy only. - condition = optional( - object({ - description = optional(string) - expression = optional(string) - location = optional(string) - title = optional(string) - }) - ) - })) - ) - }))) - default = {} -} diff --git a/tests/modules/organization_policy/test_plan.py b/tests/modules/organization_policy/test_plan.py deleted file mode 100644 index fa7e5cd7a7..0000000000 --- a/tests/modules/organization_policy/test_plan.py +++ /dev/null @@ -1,89 +0,0 @@ -# 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. - -def test_org_policy_simple(plan_runner): - "Test vpc with no extra options." - org_policies = ( - '{' - '"folders/1234567890" = {' - ' "constraints/iam.disableServiceAccountKeyUpload" = {' - ' rules = [' - ' {' - ' enforce = true,' - ' }' - ' ]' - ' }' - ' },' - ' "organizations/1234567890" = {' - ' "run.allowedIngress" = {' - ' rules = [' - ' {' - ' allow = ["internal"],' - ' condition = {' - ' description= "allow ingress",' - ' expression = "resource.matchTag(\'123456789/environment\', \'prod\')",' - ' title = "allow-for-prod-org"' - ' }' - ' }' - ' ]' - ' }' - ' }' - '}' - ) - _, resources = plan_runner( - policies = org_policies - ) - assert len(resources) == 2 - - org_policy = [r for r in resources if r["values"] - ["name"].endswith('iam.disableServiceAccountKeyUpload')][0]["values"] - assert org_policy["parent"] == "folders/1234567890" - assert org_policy["spec"][0]["rules"][0]["enforce"] == "TRUE" - - -def test_org_policy_factory(plan_runner): - "Test yaml based configuration" - _, resources = plan_runner( - config_directory="./policies", - ) - assert len(resources) == 5 - - org_policy = [r for r in resources if r["values"] - ["name"].endswith('run.allowedIngress')][0]["values"]["spec"][0] - assert org_policy["inherit_from_parent"] == True - assert org_policy["rules"][0]["condition"][0]["title"] == "allow-for-prod" - assert set(org_policy["rules"][0]["values"][0]["allowed_values"]) == set(["internal"]) - - -def test_combined_org_policy_config(plan_runner): - "Test combined (yaml, hcl) policy configuration" - org_policies = ( - '{' - '"folders/3456789012" = {' - ' "constraints/iam.disableServiceAccountKeyUpload" = {' - ' rules = [' - ' {' - ' enforce = true' - ' }' - ' ]' - ' }' - ' }' - '}' - ) - _, resources = plan_runner( - config_directory="./policies", - policies = org_policies - ) - - assert len(resources) == 6 diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf index 4c7441ac55..08cf49dc60 100644 --- a/tests/modules/project/fixture/main.tf +++ b/tests/modules/project/fixture/main.tf @@ -26,6 +26,7 @@ module "test" { labels = var.labels lien_reason = var.lien_reason org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path oslogin = var.oslogin oslogin_admins = var.oslogin_admins oslogin_users = var.oslogin_users diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf index 236cb69f32..938433967b 100644 --- a/tests/modules/project/fixture/variables.tf +++ b/tests/modules/project/fixture/variables.tf @@ -69,6 +69,11 @@ variable "org_policies" { default = {} } +variable "org_policies_data_path" { + type = any + default = null +} + variable "oslogin" { type = bool default = false diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index a9c4df68b3..2849446762 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -12,31 +12,106 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hcl2 +import yaml -def test_policy_boolean(plan_runner): - "Test boolean org policy." - policies = '''{ - "iam.disableServiceAccountKeyCreation" = { - enforce = true - } - "iam.disableServiceAccountKeyUpload" = { - enforce = false - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - enforce = true +BOOLEAN_POLICIES = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" } - ] + enforce = true + } + ] + } +}''' + +LIST_POLICIES = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] } - }''' - _, resources = plan_runner(org_policies=policies) - assert len(resources) == 6 + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(aa, bb)" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(cc, dd)" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } +}''' + + +def test_policy_boolean(plan_runner): + "Test boolean org policy." + _, resources = plan_runner(org_policies=BOOLEAN_POLICIES) + validate_policy_boolean_resources(resources) + +def test_policy_list(plan_runner): + "Test list org policy." + _, resources = plan_runner(org_policies=LIST_POLICIES) + validate_policy_list_resources(resources) + + +def test_policy_boolean_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {BOOLEAN_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_boolean_resources(resources) + + +def test_policy_list_factory(plan_runner, tmp_path): + # convert hcl policies to yaml + hcl_policies = f'p = {LIST_POLICIES}' + yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p']) + + yaml_file = tmp_path / 'policies.yaml' + yaml_file.write_text(yaml_policies) + + _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') + validate_policy_list_resources(resources) + + +def validate_policy_boolean_resources(resources): + assert len(resources) == 6 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] assert len(policies) == 2 assert all(x['values']['parent'] == 'projects/my-project' for x in policies) @@ -77,7 +152,7 @@ def test_policy_boolean(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -87,46 +162,7 @@ def test_policy_boolean(plan_runner): } -def test_policy_list(plan_runner): - "Test list org policy." - policies = '''{ - "compute.vmExternalIpAccess" = { - deny = { all = true } - } - "iam.allowedPolicyMemberDomains" = { - allow = { - values = ["C0xxxxxxx", "C0yyyyyyy"] - } - } - "compute.restrictLoadBalancerCreationForTypes" = { - deny = { values = ["in:EXTERNAL"] } - rules = [ - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" - title = "condition" - description = "test condition" - location = "xxx" - } - allow = { - values = ["EXTERNAL_1"] - } - }, - { - condition = { - expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" - title = "condition2" - description = "test condition2" - location = "xxx" - } - allow = { - all = true - } - } - ] - } - }''' - _, resources = plan_runner(org_policies=policies) +def validate_policy_list_resources(resources): assert len(resources) == 7 policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] @@ -195,7 +231,7 @@ def test_policy_list(plan_runner): 'allow_all': None, 'condition': [{ 'description': 'test condition', - 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'expression': 'resource.matchTagId(aa, bb)', 'location': 'xxx', 'title': 'condition' }], @@ -210,14 +246,10 @@ def test_policy_list(plan_runner): assert p3['rules'][2] == { 'allow_all': 'TRUE', 'condition': [{ - 'description': - 'test condition2', - 'expression': - 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', - 'location': - 'xxx', - 'title': - 'condition2' + 'description': 'test condition2', + 'expression': 'resource.matchTagId(cc, dd)', + 'location': 'xxx', + 'title': 'condition2' }], 'deny_all': None, 'enforce': None, diff --git a/tests/requirements.txt b/tests/requirements.txt index 931b173077..3eb583abf5 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ pytest>=6.2.5 -pytest-xdist PyYAML>=6.0 -tftest>=1.6.3 +tftest>=1.7.6 marko>=1.2.0 deepdiff>=5.7.0 +python-hcl2>=3.0.5