diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc71108..2599dd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#1024](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1024)] Fix Apigee PAYG environment node config ([g-greatdevaks](https://github.com/g-greatdevaks)) - [[#1019](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1019)] Added endpoint attachments to Apigee module ([apichick](https://github.com/apichick)) - [[#1000](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1000)] ADFS blueprint fixes ([apichick](https://github.com/apichick)) - [[#1001](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1001)] Binauthz blueprint fixes related to project creation ([apichick](https://github.com/apichick)) @@ -114,6 +115,12 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#1026](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1026)] add lifecycle ignore_changes for apigee PAYG env ([g-greatdevaks](https://github.com/g-greatdevaks)) +- [[#1031](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1031)] Fix default_rules_config description in firewall module ([ludoo](https://github.com/ludoo)) +- [[#1028](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1028)] **incompatible change:** Align rest of vpn modules with #1027 ([juliocc](https://github.com/juliocc)) +- [[#1027](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1027)] **incompatible change:** Update VPN-HA module to tf1.3 ([juliocc](https://github.com/juliocc)) +- [[#1025](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1025)] fix apigee PAYG env node config dynamic block ([g-greatdevaks](https://github.com/g-greatdevaks)) +- [[#1024](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1024)] Fix Apigee PAYG environment node config ([g-greatdevaks](https://github.com/g-greatdevaks)) - [[#1019](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1019)] Added endpoint attachments to Apigee module ([apichick](https://github.com/apichick)) - [[#1018](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1018)] Apigee instance doc examples ([danistrebel](https://github.com/danistrebel)) - [[#1016](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1016)] Fix memory/cpu typo in gke cluster module ([joeheaton](https://github.com/joeheaton)) @@ -191,6 +198,8 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#1022](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1022)] Replace `set-output` with env variable and remove single quotes on labels ([kunzese](https://github.com/kunzese)) +- [[#1021](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1021)] Add OpenContainers annotations to published container images ([kunzese](https://github.com/kunzese)) - [[#1017](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1017)] Fix auto-labeling ([ludoo](https://github.com/ludoo)) - [[#1013](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1013)] Update labeler.yml ([ludoo](https://github.com/ludoo)) - [[#1010](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1010)] Enforce nonempty descriptions ending in a dot ([juliocc](https://github.com/juliocc)) diff --git a/blueprints/cloud-operations/apigee/README.md b/blueprints/cloud-operations/apigee/README.md index 922f038e..0802c433 100644 --- a/blueprints/cloud-operations/apigee/README.md +++ b/blueprints/cloud-operations/apigee/README.md @@ -59,15 +59,15 @@ Do the following to verify that everything works as expected. | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [envgroups](variables.tf#L24) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | ✓ | | -| [environments](variables.tf#L30) | Environments. | map(object({…})) | ✓ | | -| [instances](variables.tf#L46) | Instance. | map(object({…})) | ✓ | | -| [project_id](variables.tf#L92) | Project ID. | string | ✓ | | -| [psc_config](variables.tf#L98) | PSC configuration. | map(string) | ✓ | | +| [environments](variables.tf#L30) | Environments. | map(object({…})) | ✓ | | +| [instances](variables.tf#L45) | Instance. | map(object({…})) | ✓ | | +| [project_id](variables.tf#L91) | Project ID. | string | ✓ | | +| [psc_config](variables.tf#L97) | PSC configuration. | map(string) | ✓ | | | [datastore_name](variables.tf#L17) | Datastore. | string | | "gcs" | -| [organization](variables.tf#L60) | Apigee organization. | object({…}) | | {…} | -| [path](variables.tf#L76) | Bucket path. | string | | "/analytics" | -| [project_create](variables.tf#L83) | Parameters for the creation of the new project. | object({…}) | | null | -| [vpc_create](variables.tf#L104) | Boolean flag indicating whether the VPC should be created or not. | bool | | true | +| [organization](variables.tf#L59) | Apigee organization. | object({…}) | | {…} | +| [path](variables.tf#L75) | Bucket path. | string | | "/analytics" | +| [project_create](variables.tf#L82) | Parameters for the creation of the new project. | object({…}) | | null | +| [vpc_create](variables.tf#L103) | Boolean flag indicating whether the VPC should be created or not. | bool | | true | ## Outputs diff --git a/blueprints/cloud-operations/apigee/functions/export/package-lock.json b/blueprints/cloud-operations/apigee/functions/export/package-lock.json index 77925575..737005be 100644 --- a/blueprints/cloud-operations/apigee/functions/export/package-lock.json +++ b/blueprints/cloud-operations/apigee/functions/export/package-lock.json @@ -869,9 +869,9 @@ } }, "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -1224,30 +1224,19 @@ } }, "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3827,9 +3816,9 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "requires": { "asap": "^2.0.0", "wrappy": "1" @@ -4106,21 +4095,14 @@ } }, "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "dependencies": { - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" - } + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" } }, "forwarded": { diff --git a/blueprints/cloud-operations/apigee/variables.tf b/blueprints/cloud-operations/apigee/variables.tf index 1c86621b..ba7f5d78 100644 --- a/blueprints/cloud-operations/apigee/variables.tf +++ b/blueprints/cloud-operations/apigee/variables.tf @@ -33,9 +33,8 @@ variable "environments" { display_name = optional(string) description = optional(string) node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - current_aggregate_node_count = number + min_node_count = optional(number) + max_node_count = optional(number) })) iam = optional(map(list(string))) envgroups = list(string) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py index 83e93fbb..8e7640dd 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py @@ -21,7 +21,7 @@ import time from google.cloud import monitoring_v3, asset_v1 from google.protobuf import field_mask_pb2 from googleapiclient import discovery -from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls +from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls, secondarys CF_VERSION = os.environ.get("CF_VERSION") @@ -158,6 +158,9 @@ def main(event, context=None): # IP utilization subnet level metrics subnets.get_subnets(config, metrics_dict) + # IP utilization secondary range metrics + secondarys.get_secondaries(config, metrics_dict) + # Asset inventory queries gce_instance_dict = instances.get_gce_instance_dict(config) l4_forwarding_rules_dict = ilb_fwrules.get_forwarding_rules_dict(config, "L4") diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml index 0c5bb8cc..21759963 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml @@ -25,6 +25,16 @@ metrics_per_subnet: limit: name: number_of_max_ip description: Number of available IP addresses in the subnet. + ip_usage_per_secondaryRange: + usage: + name: number_of_sr_ip_used + description: Number of used IP addresses in the secondary range. + utilization: + name: ip_addresses_per_sr_utilization + description: Percentage of IP used in the secondary range. + limit: + name: number_of_max_sr_ip + description: Number of available IP addresses in the secondary range. metrics_per_network: instance_per_network: usage: diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py index b26e30d4..8e0c4082 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py @@ -53,7 +53,9 @@ def create_metrics(monitoring_project, config): monitoring_project, config) # Parse limits for network and peering group metrics # Subnet level metrics have a different limit: the subnet IP range size - if sub_metric_key == "limit" and metric_name != "ip_usage_per_subnet": + if sub_metric_key == "limit" and ( + metric_name != "ip_usage_per_subnet" and + metric_name != "ip_usage_per_secondaryRange"): limits_dict_for_metric = {} if "values" in sub_metric: for network_link, limit_value in sub_metric["values"].items(): @@ -262,4 +264,4 @@ def customize_quota_view(quota_results): for val in result.points: quotaViewJson.update({'value': val.value.int64_value}) quotaViewList.append(quotaViewJson) - return quotaViewList \ No newline at end of file + return quotaViewList diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py new file mode 100644 index 00000000..6030ddaf --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py @@ -0,0 +1,266 @@ +# +# 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 time + +from . import metrics +from google.protobuf import field_mask_pb2 +from google.protobuf.json_format import MessageToDict +import ipaddress + + +def get_all_secondaryRange(config): + ''' + Returns a dictionary with secondary range informations + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + secondary_dict (dictionary of String: dictionary): Key is the project_id, + value is a nested dictionary with subnet_name/secondary_range_name as the key. + ''' + secondary_dict = {} + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + + response = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ['compute.googleapis.com/Subnetwork'], + "read_mask": read_mask, + "page_size": config["page_size"], + }) + + for asset in response: + for versioned in asset.versioned_resources: + subnet_name = versioned.resource.get('name') + # Network self link format: + # "https://www.googleapis.com/compute/v1/projects//global/networks/" + project_id = versioned.resource.get('network').split('/')[6] + network_name = versioned.resource.get('network').split('/')[-1] + subnet_region = versioned.resource.get('region').split('/')[-1] + + # Check first if the subnet has any secondary ranges to begin with + if versioned.resource.get('secondaryIpRanges'): + for items in versioned.resource.get('secondaryIpRanges'): + # Each subnet can have multiple secondary ranges + secondaryRange_name = items.get('rangeName') + secondaryCidrBlock = items.get('ipCidrRange') + + net = ipaddress.ip_network(secondaryCidrBlock) + total_ip_addresses = int(net.num_addresses) + + if project_id not in secondary_dict: + secondary_dict[project_id] = {} + secondary_dict[project_id][f"{subnet_name}/{secondaryRange_name}"] = { + 'name': secondaryRange_name, + 'region': subnet_region, + 'subnetName': subnet_name, + 'ip_cidr_range': secondaryCidrBlock, + 'total_ip_addresses': total_ip_addresses, + 'used_ip_addresses': 0, + 'network_name': network_name + } + return secondary_dict + + +def compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict): + ''' + Counts the IP Addresses used by GKE (Pods and Services) + Parameters: + config (dict): The dict containing config like clients and limits + read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory + all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization + Returns: + all_secondary_dict (dict): Same dict but populated with GKE IP utilization information + ''' + cluster_secondary_dict = {} + node_secondary_dict = {} + + # Creating cluster dict + # Cluster dict has subnet information + response_cluster = config["clients"]["asset_client"].list_assets( + request={ + "parent": f"organizations/{config['organization']}", + "asset_types": ['container.googleapis.com/Cluster'], + "content_type": 'RESOURCE', + "page_size": config["page_size"], + }) + + for asset in response_cluster: + cluster_project = asset.resource.data['selfLink'].split('/')[5] + cluster_parent = "/".join(asset.resource.data['selfLink'].split('/')[5:10]) + cluster_subnetwork = asset.resource.data['subnetwork'] + cluster_service_rangeName = asset.resource.data['ipAllocationPolicy'][ + 'servicesSecondaryRangeName'] + + cluster_secondary_dict[f"{cluster_parent}/Service"] = { + "project": cluster_project, + "subnet": cluster_subnetwork, + "secondaryRange_name": cluster_service_rangeName, + 'used_ip_addresses': 0, + } + + for node_pool in asset.resource.data['nodePools']: + nodepool_name = node_pool['name'] + node_IPrange = node_pool['networkConfig']['podRange'] + cluster_secondary_dict[f"{cluster_parent}/{nodepool_name}"] = { + "project": cluster_project, + "subnet": cluster_subnetwork, + "secondaryRange_name": node_IPrange, + 'used_ip_addresses': 0, + } + + # Creating node dict + # Node dict allows 1:1 mapping of pod IP utilization, and which secondary Range it is using + response_node = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ['k8s.io/Node'], + "read_mask": read_mask, + "page_size": config["page_size"], + }) + + for asset in response_node: + # Node name link format: + # "//container.googleapis.com/projects////clusters//k8s/nodes/" + node_parent = "/".join(asset.name.split('/')[4:9]) + node_name = asset.name.split('/')[-1] + node_full_name = f"{node_parent}/{node_name}" + + for versioned in asset.versioned_resources: + node_secondary_dict[node_full_name] = { + 'node_parent': + node_parent, + 'this_node_pool': + versioned.resource['metadata']['labels'] + ['cloud.google.com/gke-nodepool'], + 'used_ip_addresses': + 0 + } + + # Counting IP addresses used by pods in GKE + response_pods = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ['k8s.io/Pod'], + "read_mask": read_mask, + "page_size": config["page_size"], + }) + + for asset in response_pods: + # Pod name link format: + # "//container.googleapis.com/projects////clusters//k8s/namespaces//pods/" + pod_parent = "/".join(asset.name.split('/')[4:9]) + + for versioned in asset.versioned_resources: + cur_PodIP = versioned.resource['status']['podIP'] + cur_HostIP = versioned.resource['status']['hostIP'] + host_node_name = versioned.resource['spec']['nodeName'] + pod_full_path = f"{pod_parent}/{host_node_name}" + + # A check to make sure pod is not using node IP + if cur_PodIP != cur_HostIP: + node_secondary_dict[pod_full_path]['used_ip_addresses'] += 1 + + # Counting IP addresses used by Service in GKE + response_service = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ['k8s.io/Service'], + "read_mask": read_mask, + "page_size": config["page_size"], + }) + + for asset in response_service: + service_parent = "/".join(asset.name.split('/')[4:9]) + service_fullpath = f"{service_parent}/Service" + cluster_secondary_dict[service_fullpath]['used_ip_addresses'] += 1 + + for item in node_secondary_dict.values(): + itemKey = f"{item['node_parent']}/{item['this_node_pool']}" + cluster_secondary_dict[itemKey]['used_ip_addresses'] += item['used_ip_addresses'] + + for item in cluster_secondary_dict.values(): + itemKey = f"{item['subnet']}/{item['secondaryRange_name']}" + all_secondary_dict[item['project']][itemKey]['used_ip_addresses'] += item[ + 'used_ip_addresses'] + + +def compute_secondary_utilization(config, all_secondary_dict): + ''' + Counts resources (GKE, GCE) using IPs in secondary ranges. + Parameters: + config (dict): Dict containing config like clients and limits + all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization + Returns: + None + ''' + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + + compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict) + # TODO: Other Secondary IP like GCE VM using alias IPs + + +def get_secondaries(config, metrics_dict): + ''' + Writes all secondary rang IP address usage metrics to custom metrics. + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + None + ''' + + secondaryRange_dict = get_all_secondaryRange(config) + # Updates all_subnets_dict with the IP utilization info + compute_secondary_utilization(config, secondaryRange_dict) + + timestamp = time.time() + for project_id in config["monitored_projects"]: + if project_id not in secondaryRange_dict: + continue + for secondary_dict in secondaryRange_dict[project_id].values(): + ip_utilization = 0 + if secondary_dict['used_ip_addresses'] > 0: + ip_utilization = secondary_dict['used_ip_addresses'] / secondary_dict[ + 'total_ip_addresses'] + + # Building unique identifier with subnet region/name + subnet_id = f"{secondary_dict['region']}/{secondary_dict['name']}" + metric_labels = { + 'project': project_id, + 'network_name': secondary_dict['network_name'], + 'region' : secondary_dict['region'], + 'subnet' : secondary_dict['subnetName'], + 'secondary_range' : secondary_dict['name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"] + ["ip_usage_per_secondaryRange"]["usage"]["name"], + secondary_dict['used_ip_addresses'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"] + ["ip_usage_per_secondaryRange"]["limit"]["name"], + secondary_dict['total_ip_addresses'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"] + ["ip_usage_per_secondaryRange"]["utilization"]["name"], + ip_utilization, metric_labels, timestamp=timestamp) + + print("Buffered metrics for secondary ip utilization for VPCs in project", + project_id) 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 cb16c9c8..46fbc756 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py @@ -149,7 +149,8 @@ def compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict): 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']: + '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 diff --git a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json index c9eb8bd1..e26d6926 100644 --- a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json +++ b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json @@ -701,6 +701,49 @@ "width": 6, "xPos": 6, "yPos": 24 + }, + { + "height": 4, + "widget": { + "title": "secondary_ip_address_utilization", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "apiSource": "DEFAULT_CLOUD", + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"custom.googleapis.com/ip_addresses_per_sr_utilization\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", + "perSeriesAligner": "ALIGN_NONE" + } + } + } + } + ], + "thresholds": [], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 0, + "yPos": 36 } ] } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md index 3af34289..9b292de6 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md @@ -13,6 +13,5 @@ The codebase provisions the following list of resources: |---|---|:---:|:---:|:---:| | [impersonate_service_account_email](variables.tf#L16) | Service account to be impersonated by workload identity. | string | ✓ | | | [project_id](variables.tf#L21) | GCP project ID. | string | ✓ | | -| [workload_identity_pool_provider_id](variables.tf#L26) | GCP workload identity pool provider ID. | string | ✓ | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf index 47f24620..ae132fd4 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf @@ -16,8 +16,7 @@ module "tfe_oidc" { source = "./tfc-oidc" - workload_identity_pool_provider_id = var.workload_identity_pool_provider_id - impersonate_service_account_email = var.impersonate_service_account_email + impersonate_service_account_email = var.impersonate_service_account_email } provider "google" { diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template index efea4cc9..fc2811db 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template @@ -13,5 +13,4 @@ # limitations under the License. project_id = "tfe-oidc-workflow" -workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" impersonate_service_account_email = "sa-tfe@tfe-oidc-workflow2.iam.gserviceaccount.com" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md index 240d6d02..534d6599 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md @@ -5,9 +5,8 @@ This is a helper module to prepare GCP Credentials from Terraform Enterprise wor ## Example ```hcl module "tfe_oidc" { - source = "./tfe_oidc" + source = "./tfc-oidc" - workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" } @@ -28,7 +27,6 @@ provider "google-beta" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [impersonate_service_account_email](variables.tf#L17) | Service account to be impersonated by workload identity federation. | string | ✓ | | -| [workload_identity_pool_provider_id](variables.tf#L28) | GCP workload identity pool provider ID. | string | ✓ | | | [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | ## Outputs diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh new file mode 100644 index 00000000..251fe321 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Exit if any of the intermediate steps fail +set -e + +cat <map(string) | | {…} | | | [router_configs](variables.tf#L192) | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | | [service_accounts](variables.tf#L215) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L229) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [vpn_onprem_configs](variables.tf#L229) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-nva/variables.tf b/fast/stages/02-networking-nva/variables.tf index b55eaabc..ea6c0bf0 100644 --- a/fast/stages/02-networking-nva/variables.tf +++ b/fast/stages/02-networking-nva/variables.tf @@ -235,10 +235,7 @@ variable "vpn_onprem_configs" { }) peer_external_gateway = object({ redundancy_type = string - interfaces = list(object({ - id = number - ip_address = string - })) + interfaces = list(string) }) tunnels = list(object({ peer_asn = number @@ -258,9 +255,7 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] } tunnels = [ { @@ -288,9 +283,7 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] } tunnels = [ { diff --git a/fast/stages/02-networking-nva/vpn-onprem.tf b/fast/stages/02-networking-nva/vpn-onprem.tf index c860c099..03658088 100644 --- a/fast/stages/02-networking-nva/vpn-onprem.tf +++ b/fast/stages/02-networking-nva/vpn-onprem.tf @@ -33,16 +33,19 @@ locals { } module "landing-to-onprem-ew1-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-trusted-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" - router_create = true - router_name = "landing-onprem-vpn-ew1" - router_asn = var.router_configs.landing-trusted-ew1.asn - peer_external_gateway = var.vpn_onprem_configs.landing-trusted-ew1.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-trusted-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_config = { + name = "landing-onprem-vpn-ew1" + asn = var.router_configs.landing-trusted-ew1.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.landing-trusted-ew1.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.landing-trusted-ew1.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -52,9 +55,7 @@ module "landing-to-onprem-ew1-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.landing-trusted-ew1 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } @@ -62,16 +63,19 @@ module "landing-to-onprem-ew1-vpn" { } module "landing-to-onprem-ew4-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-trusted-vpc.self_link - region = "europe-west4" - name = "vpn-to-onprem-ew4" - router_create = true - router_name = "landing-onprem-vpn-ew4" - router_asn = var.router_configs.landing-trusted-ew4.asn - peer_external_gateway = var.vpn_onprem_configs.landing-trusted-ew4.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-trusted-vpc.self_link + region = "europe-west4" + name = "vpn-to-onprem-ew4" + router_config = { + name = "landing-onprem-vpn-ew4" + asn = var.router_configs.landing-trusted-ew4.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.landing-trusted-ew4.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.landing-trusted-ew4.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -81,9 +85,7 @@ module "landing-to-onprem-ew4-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.landing-trusted-ew4 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } diff --git a/fast/stages/02-networking-peering/README.md b/fast/stages/02-networking-peering/README.md index f6a82809..c7829f0f 100644 --- a/fast/stages/02-networking-peering/README.md +++ b/fast/stages/02-networking-peering/README.md @@ -311,7 +311,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [region_trigram](variables.tf#L166) | Short names for GCP regions. | map(string) | | {…} | | | [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | | [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-peering/variables.tf b/fast/stages/02-networking-peering/variables.tf index 111633e6..faa91698 100644 --- a/fast/stages/02-networking-peering/variables.tf +++ b/fast/stages/02-networking-peering/variables.tf @@ -213,10 +213,7 @@ variable "vpn_onprem_configs" { }) peer_external_gateway = object({ redundancy_type = string - interfaces = list(object({ - id = number - ip_address = string - })) + interfaces = list(string) }) tunnels = list(object({ peer_asn = number @@ -236,9 +233,7 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] } tunnels = [ { diff --git a/fast/stages/02-networking-peering/vpn-onprem.tf b/fast/stages/02-networking-peering/vpn-onprem.tf index 48cad54b..5237a05a 100644 --- a/fast/stages/02-networking-peering/vpn-onprem.tf +++ b/fast/stages/02-networking-peering/vpn-onprem.tf @@ -33,16 +33,19 @@ locals { } module "landing-to-onprem-ew1-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" - router_create = true - router_name = "landing-onprem-vpn-ew1" - router_asn = var.router_onprem_configs.landing-ew1.asn - peer_external_gateway = var.vpn_onprem_configs.landing-ew1.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_config = { + name = "landing-onprem-vpn-ew1" + asn = var.router_onprem_configs.landing-ew1.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.landing-ew1.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.landing-ew1.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -52,9 +55,7 @@ module "landing-to-onprem-ew1-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.landing-ew1 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } diff --git a/fast/stages/02-networking-separate-envs/README.md b/fast/stages/02-networking-separate-envs/README.md index a874311a..66b31646 100644 --- a/fast/stages/02-networking-separate-envs/README.md +++ b/fast/stages/02-networking-separate-envs/README.md @@ -252,7 +252,7 @@ You're now ready to run `terraform init` and `apply`. | [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | | [router_onprem_configs](variables.tf#L166) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | | [service_accounts](variables.tf#L189) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L201) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [vpn_onprem_configs](variables.tf#L201) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-separate-envs/variables.tf b/fast/stages/02-networking-separate-envs/variables.tf index d71534db..019d0b2e 100644 --- a/fast/stages/02-networking-separate-envs/variables.tf +++ b/fast/stages/02-networking-separate-envs/variables.tf @@ -207,10 +207,7 @@ variable "vpn_onprem_configs" { }) peer_external_gateway = object({ redundancy_type = string - interfaces = list(object({ - id = number - ip_address = string - })) + interfaces = list(string) }) tunnels = list(object({ peer_asn = number @@ -230,9 +227,8 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] + } tunnels = [ { @@ -260,9 +256,7 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] } tunnels = [ { diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf b/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf index 313a073d..1b0ff117 100644 --- a/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf +++ b/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf @@ -33,16 +33,19 @@ locals { } module "dev-to-onprem-ew1-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" - router_create = true - router_name = "dev-onprem-vpn-ew1" - router_asn = var.router_onprem_configs.dev-ew1.asn - peer_external_gateway = var.vpn_onprem_configs.dev-ew1.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_config = { + name = "dev-onprem-vpn-ew1" + asn = var.router_onprem_configs.dev-ew1.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.dev-ew1.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.dev-ew1.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -52,9 +55,7 @@ module "dev-to-onprem-ew1-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.dev-ew1 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf b/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf index 0a8e9655..d4b2af24 100644 --- a/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf +++ b/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf @@ -17,16 +17,19 @@ # tfdoc:file:description VPN between prod and onprem. module "prod-to-onprem-ew1-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" - router_create = true - router_name = "prod-onprem-vpn-ew1" - router_asn = var.router_onprem_configs.prod-ew1.asn - peer_external_gateway = var.vpn_onprem_configs.prod-ew1.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_config = { + name = "prod-onprem-vpn-ew1" + asn = var.router_onprem_configs.prod-ew1.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.prod-ew1.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.prod-ew1.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -36,9 +39,7 @@ module "prod-to-onprem-ew1-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.prod-ew1 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } diff --git a/fast/stages/02-networking-vpn/README.md b/fast/stages/02-networking-vpn/README.md index 8a884c09..047a1189 100644 --- a/fast/stages/02-networking-vpn/README.md +++ b/fast/stages/02-networking-vpn/README.md @@ -336,7 +336,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | | [router_spoke_configs](variables-vpn.tf#L18) | Configurations for routers used for internal connectivity. | map(object({…})) | | {…} | | | [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | | [vpn_spoke_configs](variables-vpn.tf#L37) | VPN gateway configuration for spokes. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-vpn/variables.tf b/fast/stages/02-networking-vpn/variables.tf index 111633e6..faa91698 100644 --- a/fast/stages/02-networking-vpn/variables.tf +++ b/fast/stages/02-networking-vpn/variables.tf @@ -213,10 +213,7 @@ variable "vpn_onprem_configs" { }) peer_external_gateway = object({ redundancy_type = string - interfaces = list(object({ - id = number - ip_address = string - })) + interfaces = list(string) }) tunnels = list(object({ peer_asn = number @@ -236,9 +233,7 @@ variable "vpn_onprem_configs" { } peer_external_gateway = { redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [ - { id = 0, ip_address = "8.8.8.8" }, - ] + interfaces = ["8.8.8.8"] } tunnels = [ { diff --git a/fast/stages/02-networking-vpn/vpn-onprem.tf b/fast/stages/02-networking-vpn/vpn-onprem.tf index 48cad54b..5237a05a 100644 --- a/fast/stages/02-networking-vpn/vpn-onprem.tf +++ b/fast/stages/02-networking-vpn/vpn-onprem.tf @@ -33,16 +33,19 @@ locals { } module "landing-to-onprem-ew1-vpn" { - count = local.enable_onprem_vpn ? 1 : 0 - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" - router_create = true - router_name = "landing-onprem-vpn-ew1" - router_asn = var.router_onprem_configs.landing-ew1.asn - peer_external_gateway = var.vpn_onprem_configs.landing-ew1.peer_external_gateway + count = local.enable_onprem_vpn ? 1 : 0 + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-onprem-ew1" + router_config = { + name = "landing-onprem-vpn-ew1" + asn = var.router_onprem_configs.landing-ew1.asn + } + peer_gateway = { + external = var.vpn_onprem_configs.landing-ew1.peer_external_gateway + } tunnels = { for t in var.vpn_onprem_configs.landing-ew1.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { @@ -52,9 +55,7 @@ module "landing-to-onprem-ew1-vpn" { } bgp_peer_options = local.bgp_peer_options_onprem.landing-ew1 bgp_session_range = "${cidrhost(t.session_range, 2)}/30" - ike_version = 2 peer_external_gateway_interface = t.peer_external_gateway_interface - router = null shared_secret = t.secret vpn_gateway_interface = t.vpn_gateway_interface } diff --git a/fast/stages/02-networking-vpn/vpn-spoke-dev.tf b/fast/stages/02-networking-vpn/vpn-spoke-dev.tf index 1ad329a0..317560af 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-dev.tf +++ b/fast/stages/02-networking-vpn/vpn-spoke-dev.tf @@ -39,11 +39,13 @@ module "landing-to-dev-ew1-vpn" { network = module.landing-vpc.self_link region = "europe-west1" name = "vpn-to-dev-ew1" - # The router used for this VPN is managed in vpn-prod.tf - router_create = false - router_name = "landing-vpn-ew1" - router_asn = var.router_spoke_configs.landing-ew1.asn - peer_gcp_gateway = module.dev-to-landing-ew1-vpn.self_link + router_config = { + # The router used for this VPN is managed in vpn-prod.tf + create = false + name = "landing-vpn-ew1" + asn = var.router_spoke_configs.landing-ew1.asn + } + peer_gateway = { gcp = module.dev-to-landing-ew1-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -54,11 +56,7 @@ module "landing-to-dev-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.0/27", 2) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 0 + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -69,11 +67,7 @@ module "landing-to-dev-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.0/27", 6) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 1 + vpn_gateway_interface = 1 } } depends_on = [ @@ -82,15 +76,16 @@ module "landing-to-dev-ew1-vpn" { } module "dev-to-landing-ew1-vpn" { - source = "../../../modules/net-vpn-ha" - project_id = module.dev-spoke-project.project_id - network = module.dev-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-landing-ew1" - router_create = true - router_name = "dev-spoke-vpn-ew1" - router_asn = var.router_spoke_configs.spoke-dev-ew1.asn - peer_gcp_gateway = module.landing-to-dev-ew1-vpn.self_link + source = "../../../modules/net-vpn-ha" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-landing-ew1" + router_config = { + name = "dev-spoke-vpn-ew1" + asn = var.router_spoke_configs.spoke-dev-ew1.asn + } + peer_gateway = { gcp = module.landing-to-dev-ew1-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -101,11 +96,8 @@ module "dev-to-landing-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.0/27", 1) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-dev-ew1-vpn.random_secret - vpn_gateway_interface = 0 + shared_secret = module.landing-to-dev-ew1-vpn.random_secret + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -116,11 +108,8 @@ module "dev-to-landing-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.0/27", 5) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-dev-ew1-vpn.random_secret - vpn_gateway_interface = 1 + shared_secret = module.landing-to-dev-ew1-vpn.random_secret + vpn_gateway_interface = 1 } } } diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf b/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf index 9562e4ce..a215ad4e 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf +++ b/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf @@ -19,15 +19,16 @@ # local.vpn_spoke_bgp_peer_options is defined in the dev VPN file module "landing-to-prod-ew1-vpn" { - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-vpc.self_link - region = "europe-west1" - name = "vpn-to-prod-ew1" - router_create = true - router_name = "landing-vpn-ew1" - router_asn = var.router_spoke_configs.landing-ew1.asn - peer_gcp_gateway = module.prod-to-landing-ew1-vpn.self_link + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west1" + name = "vpn-to-prod-ew1" + router_config = { + name = "landing-vpn-ew1" + asn = var.router_spoke_configs.landing-ew1.asn + } + peer_gateway = { gcp = module.prod-to-landing-ew1-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -38,11 +39,7 @@ module "landing-to-prod-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.64/27", 2) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 0 + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -53,25 +50,22 @@ module "landing-to-prod-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.64/27", 6) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 1 + vpn_gateway_interface = 1 } } } module "prod-to-landing-ew1-vpn" { - source = "../../../modules/net-vpn-ha" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-landing-ew1" - router_create = true - router_name = "prod-spoke-vpn-ew1" - router_asn = var.router_spoke_configs.spoke-prod-ew1.asn - peer_gcp_gateway = module.landing-to-prod-ew1-vpn.self_link + source = "../../../modules/net-vpn-ha" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.self_link + region = "europe-west1" + name = "vpn-to-landing-ew1" + router_config = { + name = "prod-spoke-vpn-ew1" + asn = var.router_spoke_configs.spoke-prod-ew1.asn + } + peer_gateway = { gcp = module.landing-to-prod-ew1-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -82,11 +76,8 @@ module "prod-to-landing-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.64/27", 1) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-prod-ew1-vpn.random_secret - vpn_gateway_interface = 0 + shared_secret = module.landing-to-prod-ew1-vpn.random_secret + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -97,11 +88,8 @@ module "prod-to-landing-ew1-vpn" { bgp_session_range = "${ cidrhost("169.254.0.64/27", 5) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-prod-ew1-vpn.random_secret - vpn_gateway_interface = 1 + shared_secret = module.landing-to-prod-ew1-vpn.random_secret + vpn_gateway_interface = 1 } } } diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf b/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf index cbee0bef..994fba0b 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf +++ b/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf @@ -19,15 +19,16 @@ # local.vpn_spoke_bgp_peer_options is defined in the dev VPN file module "landing-to-prod-ew4-vpn" { - source = "../../../modules/net-vpn-ha" - project_id = module.landing-project.project_id - network = module.landing-vpc.self_link - region = "europe-west4" - name = "vpn-to-prod-ew4" - router_create = true - router_name = "landing-vpn-ew4" - router_asn = var.router_spoke_configs.landing-ew4.asn - peer_gcp_gateway = module.prod-to-landing-ew4-vpn.self_link + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-vpc.self_link + region = "europe-west4" + name = "vpn-to-prod-ew4" + router_config = { + name = "landing-vpn-ew4" + asn = var.router_spoke_configs.landing-ew4.asn + } + peer_gateway = { gcp = module.prod-to-landing-ew4-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -38,11 +39,7 @@ module "landing-to-prod-ew4-vpn" { bgp_session_range = "${ cidrhost("169.254.0.96/27", 2) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 0 + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -53,25 +50,22 @@ module "landing-to-prod-ew4-vpn" { bgp_session_range = "${ cidrhost("169.254.0.96/27", 6) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = null - vpn_gateway_interface = 1 + vpn_gateway_interface = 1 } } } module "prod-to-landing-ew4-vpn" { - source = "../../../modules/net-vpn-ha" - project_id = module.prod-spoke-project.project_id - network = module.prod-spoke-vpc.self_link - region = "europe-west4" - name = "vpn-to-landing-ew4" - router_create = true - router_name = "prod-spoke-vpn-ew4" - router_asn = var.router_spoke_configs.spoke-prod-ew4.asn - peer_gcp_gateway = module.landing-to-prod-ew4-vpn.self_link + source = "../../../modules/net-vpn-ha" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.self_link + region = "europe-west4" + name = "vpn-to-landing-ew4" + router_config = { + name = "prod-spoke-vpn-ew4" + asn = var.router_spoke_configs.spoke-prod-ew4.asn + } + peer_gateway = { gcp = module.landing-to-prod-ew4-vpn.self_link } tunnels = { 0 = { bgp_peer = { @@ -82,11 +76,8 @@ module "prod-to-landing-ew4-vpn" { bgp_session_range = "${ cidrhost("169.254.0.96/27", 1) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-prod-ew4-vpn.random_secret - vpn_gateway_interface = 0 + shared_secret = module.landing-to-prod-ew4-vpn.random_secret + vpn_gateway_interface = 0 } 1 = { bgp_peer = { @@ -97,11 +88,8 @@ module "prod-to-landing-ew4-vpn" { bgp_session_range = "${ cidrhost("169.254.0.96/27", 5) }/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.landing-to-prod-ew4-vpn.random_secret - vpn_gateway_interface = 1 + shared_secret = module.landing-to-prod-ew4-vpn.random_secret + vpn_gateway_interface = 1 } } } diff --git a/fast/stages/02-security/README.md b/fast/stages/02-security/README.md index 72e93606..026ef771 100644 --- a/fast/stages/02-security/README.md +++ b/fast/stages/02-security/README.md @@ -224,17 +224,17 @@ vpc_sc_perimeters = { ingress_policies = ["iac"] resources = ["projects/1111111111"] } - dev = { - egress_policies = ["iac-gcs"] - ingress_policies = ["iac"] - resources = ["projects/0000000000"] - } - dev = { + landing = { access_levels = ["onprem"] egress_policies = ["iac-gcs"] ingress_policies = ["iac"] resources = ["projects/2222222222"] } + prod = { + egress_policies = ["iac-gcs"] + ingress_policies = ["iac"] + resources = ["projects/0000000000"] + } } ``` diff --git a/modules/apigee/README.md b/modules/apigee/README.md index 1425c8f9..62035e3c 100644 --- a/modules/apigee/README.md +++ b/modules/apigee/README.md @@ -169,12 +169,12 @@ module "apigee" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L76) | Project ID. | string | ✓ | | +| [project_id](variables.tf#L75) | Project ID. | string | ✓ | | | [endpoint_attachments](variables.tf#L17) | Endpoint attachments. | map(object({…})) | | null | | [envgroups](variables.tf#L26) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | null | -| [environments](variables.tf#L32) | Environments. | map(object({…})) | | null | -| [instances](variables.tf#L48) | Instances. | map(object({…})) | | null | -| [organization](variables.tf#L62) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +| [environments](variables.tf#L32) | Environments. | map(object({…})) | | null | +| [instances](variables.tf#L47) | Instances. | map(object({…})) | | null | +| [organization](variables.tf#L61) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | ## Outputs diff --git a/modules/apigee/main.tf b/modules/apigee/main.tf index bc5dab48..fe34a738 100644 --- a/modules/apigee/main.tf +++ b/modules/apigee/main.tf @@ -47,12 +47,16 @@ resource "google_apigee_environment" "environments" { dynamic "node_config" { for_each = try(each.value.node_config, null) != null ? [""] : [] content { - min_node_count = node_config.min_node_count - max_node_count = node_config.max_node_count - current_aggregate_node_count = node_config.current_aggregate_node_count + min_node_count = each.value.node_config.min_node_count + max_node_count = each.value.node_config.max_node_count } } org_id = local.org_id + lifecycle { + ignore_changes = [ + node_config["current_aggregate_node_count"] + ] + } } resource "google_apigee_envgroup_attachment" "envgroup_attachments" { diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf index 8cddf9a4..266f0d34 100644 --- a/modules/apigee/variables.tf +++ b/modules/apigee/variables.tf @@ -35,9 +35,8 @@ variable "environments" { display_name = optional(string) description = optional(string, "Terraform-managed") node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - current_aggregate_node_count = number + min_node_count = optional(number) + max_node_count = optional(number) })) iam = optional(map(list(string))) envgroups = list(string) @@ -76,4 +75,4 @@ variable "organization" { variable "project_id" { description = "Project ID." type = string -} \ No newline at end of file +} diff --git a/modules/cloud-config-container/onprem/README.md b/modules/cloud-config-container/onprem/README.md index fa29e48c..1a668a8d 100644 --- a/modules/cloud-config-container/onprem/README.md +++ b/modules/cloud-config-container/onprem/README.md @@ -24,17 +24,15 @@ The test instance is optional, as described above. ```hcl module "cloud-vpn" { - source = "./fabric/modules/net-vpn-static" - project_id = "my-project" - region = "europe-west1" - network = "my-vpc" - name = "to-on-prem" + source = "./fabric/modules/net-vpn-static" + project_id = "my-project" + region = "europe-west1" + network = "my-vpc" + name = "to-on-prem" remote_ranges = ["192.168.192.0/24"] tunnels = { remote-0 = { - ike_version = 2 peer_ip = module.on-prem.external_address - shared_secret = "" traffic_selectors = { local = ["0.0.0.0/0"], remote = null } } } diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile b/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile index 7a22d943..8bb6165b 100644 --- a/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile +++ b/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM alpine:latest +FROM debian:bullseye-slim -RUN set -xe \ - && apk add --no-cache strongswan bash sudo +ENV STRONGSWAN_VERSION=5.9 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y sudo iptables procps strongswan=${STRONGSWAN_VERSION}* \ + && rm -rf /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh RUN chmod 0755 /entrypoint.sh diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh b/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh index e99d1ec8..bf596bc0 100644 --- a/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh +++ b/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh @@ -22,7 +22,7 @@ _stop_ipsec() { echo "Shutting down strongSwan/ipsec..." ipsec stop } -trap _stop_ipsec SIGTERM +trap _stop_ipsec TERM # Making the containter to work as a default gateway for LAN_NETWORKS iptables -t nat -A POSTROUTING -s ${LAN_NETWORKS} -o ${VPN_DEVICE} -m policy --dir out --pol ipsec -j ACCEPT diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md index eb6912a7..06cca4fb 100644 --- a/modules/net-vpc-firewall/README.md +++ b/modules/net-vpc-firewall/README.md @@ -176,7 +176,7 @@ healthchecks: |---|---|:---:|:---:|:---:| | [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({…}) | | {} | +| [default_rules_config](variables.tf#L17) | Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable. | object({…}) | | {} | | [egress_rules](variables.tf#L37) | List of egress rule definitions, default to deny action. | map(object({…})) | | {} | | [factories_config](variables.tf#L60) | Paths to data files and folders that enable factory functionality. | object({…}) | | null | | [ingress_rules](variables.tf#L69) | List of ingress rule definitions, default to allow action. | map(object({…})) | | {} | diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf index 86aea9e2..3e458acd 100644 --- a/modules/net-vpc-firewall/variables.tf +++ b/modules/net-vpc-firewall/variables.tf @@ -15,7 +15,7 @@ */ variable "default_rules_config" { - description = "Optionally created convenience rules. Set the variable or individual members to null to disable." + description = "Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable." type = object({ admin_ranges = optional(list(string)) disabled = optional(bool, false) diff --git a/modules/net-vpn-dynamic/README.md b/modules/net-vpn-dynamic/README.md index 1c8fb7b6..378ba6b7 100644 --- a/modules/net-vpn-dynamic/README.md +++ b/modules/net-vpn-dynamic/README.md @@ -8,35 +8,50 @@ This example shows how to configure a single VPN tunnel using a couple of extra - internally generated shared secret, which can be fetched from the module's `random_secret` output for reuse; a predefined secret can be used instead by assigning it to the `shared_secret` attribute ```hcl +module "vm" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west1-b" + name = "my-vm" + network_interfaces = [{ + nat = true + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + service_account_create = true +} + + module "vpn-dynamic" { source = "./fabric/modules/net-vpn-dynamic" project_id = "my-project" region = "europe-west1" - network = "my-vpc" + network = var.vpc.name name = "gateway-1" + router_config = { + asn = 64514 + } + tunnels = { remote-1 = { bgp_peer = { address = "169.254.139.134" asn = 64513 + custom_advertise = { + all_subnets = true + all_vpc_subnets = false + all_peer_vpc_subnets = false + ip_ranges = { + "192.168.0.0/24" = "Advertised range description" + } + } } bgp_session_range = "169.254.139.133/30" - ike_version = 2 - peer_ip = "1.1.1.1" - router = null - shared_secret = null - bgp_peer_options = { - advertise_groups = ["ALL_SUBNETS"] - advertise_ip_ranges = { - "192.168.0.0/24" = "Advertised range description" - } - advertise_mode = "CUSTOM" - route_priority = 1000 - } + peer_ip = module.vm.external_ip } } } -# tftest modules=1 resources=10 +# tftest modules=2 resources=12 ``` @@ -48,14 +63,10 @@ module "vpn-dynamic" { | [network](variables.tf#L34) | VPC used for the gateway and routes. | string | ✓ | | | [project_id](variables.tf#L39) | Project where resources will be created. | string | ✓ | | | [region](variables.tf#L44) | Region used for resources. | string | ✓ | | -| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | "" | +| [router_config](variables.tf#L49) | Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router. | object({…}) | ✓ | | +| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | null | | [gateway_address_create](variables.tf#L23) | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool | | true | -| [route_priority](variables.tf#L49) | Route priority, defaults to 1000. | number | | 1000 | -| [router_advertise_config](variables.tf#L55) | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({…}) | | null | -| [router_asn](variables.tf#L65) | Router ASN used for auto-created router. | number | | 64514 | -| [router_create](variables.tf#L71) | Create router. | bool | | true | -| [router_name](variables.tf#L77) | Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router. | string | | "" | -| [tunnels](variables.tf#L83) | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({…})) | | {} | +| [tunnels](variables.tf#L64) | VPN tunnel configurations. | map(object({…})) | | {} | ## Outputs diff --git a/modules/net-vpn-dynamic/main.tf b/modules/net-vpn-dynamic/main.tf index 4da48c12..fcf26934 100644 --- a/modules/net-vpn-dynamic/main.tf +++ b/modules/net-vpn-dynamic/main.tf @@ -21,9 +21,9 @@ locals { : var.gateway_address ) router = ( - var.router_create - ? google_compute_router.router[0].name - : var.router_name + var.router_config.create + ? try(google_compute_router.router[0].name, null) + : var.router_config.name ) secret = random_id.secret.b64_url } @@ -65,75 +65,56 @@ resource "google_compute_forwarding_rule" "udp-4500" { } resource "google_compute_router" "router" { - count = var.router_create ? 1 : 0 - name = var.router_name == "" ? "vpn-${var.name}" : var.router_name + count = var.router_config.create ? 1 : 0 + name = coalesce(var.router_config.name, "vpn-${var.name}") project = var.project_id region = var.region network = var.network bgp { advertise_mode = ( - var.router_advertise_config == null - ? null - : var.router_advertise_config.mode + var.router_config.custom_advertise != null + ? "CUSTOM" + : "DEFAULT" ) advertised_groups = ( - var.router_advertise_config == null ? null : ( - var.router_advertise_config.mode != "CUSTOM" - ? null - : var.router_advertise_config.groups - ) + try(var.router_config.custom_advertise.all_subnets, false) + ? ["ALL_SUBNETS"] + : [] ) dynamic "advertised_ip_ranges" { - for_each = ( - var.router_advertise_config == null ? {} : ( - var.router_advertise_config.mode != "CUSTOM" - ? null - : var.router_advertise_config.ip_ranges - ) - ) + for_each = try(var.router_config.custom_advertise.ip_ranges, {}) iterator = range content { range = range.key description = range.value } } - asn = var.router_asn + keepalive_interval = try(var.router_config.keepalive, null) + asn = var.router_config.asn } } resource "google_compute_router_peer" "bgp_peer" { - for_each = var.tunnels - region = var.region - project = var.project_id - name = "${var.name}-${each.key}" - router = each.value.router == null ? local.router : each.value.router - peer_ip_address = each.value.bgp_peer.address - peer_asn = each.value.bgp_peer.asn - advertised_route_priority = ( - each.value.bgp_peer_options == null ? var.route_priority : ( - each.value.bgp_peer_options.route_priority == null - ? var.route_priority - : each.value.bgp_peer_options.route_priority - ) - ) + for_each = var.tunnels + region = var.region + project = var.project_id + name = "${var.name}-${each.key}" + router = coalesce(each.value.router, local.router) + peer_ip_address = each.value.bgp_peer.address + peer_asn = each.value.bgp_peer.asn + advertised_route_priority = each.value.bgp_peer.route_priority advertise_mode = ( - each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode + try(each.value.bgp_peer.custom_advertise, null) != null + ? "CUSTOM" + : "DEFAULT" ) - advertised_groups = ( - each.value.bgp_peer_options == null ? null : ( - each.value.bgp_peer_options.advertise_mode != "CUSTOM" - ? null - : each.value.bgp_peer_options.advertise_groups - ) + advertised_groups = concat( + try(each.value.bgp_peer.custom_advertise.all_subnets, false) ? ["ALL_SUBNETS"] : [], + try(each.value.bgp_peer.custom_advertise.all_vpc_subnets, false) ? ["ALL_VPC_SUBNETS"] : [], + try(each.value.bgp_peer.custom_advertise.all_peer_vpc_subnets, false) ? ["ALL_PEER_VPC_SUBNETS"] : [] ) dynamic "advertised_ip_ranges" { - for_each = ( - each.value.bgp_peer_options == null ? {} : ( - each.value.bgp_peer_options.advertise_mode != "CUSTOM" - ? {} - : each.value.bgp_peer_options.advertise_ip_ranges - ) - ) + for_each = try(each.value.bgp_peer.custom_advertise.ip_ranges, {}) iterator = range content { range = range.key @@ -144,11 +125,12 @@ resource "google_compute_router_peer" "bgp_peer" { } resource "google_compute_router_interface" "router_interface" { - for_each = var.tunnels - project = var.project_id - region = var.region - name = "${var.name}-${each.key}" - router = each.value.router == null ? local.router : each.value.router + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = coalesce(each.value.router, local.router) + # FIXME: can bgp_session_range be null? ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name } @@ -161,18 +143,14 @@ resource "google_compute_vpn_gateway" "gateway" { } resource "google_compute_vpn_tunnel" "tunnels" { - for_each = var.tunnels - project = var.project_id - region = var.region - name = "${var.name}-${each.key}" - router = each.value.router == null ? local.router : each.value.router - peer_ip = each.value.peer_ip - ike_version = each.value.ike_version - shared_secret = ( - each.value.shared_secret == "" || each.value.shared_secret == null - ? local.secret - : each.value.shared_secret - ) + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = coalesce(each.value.router, local.router) + peer_ip = each.value.peer_ip + ike_version = each.value.ike_version + shared_secret = coalesce(each.value.shared_secret, local.secret) target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link depends_on = [google_compute_forwarding_rule.esp] } diff --git a/modules/net-vpn-dynamic/outputs.tf b/modules/net-vpn-dynamic/outputs.tf index 09e5959e..f049df1d 100644 --- a/modules/net-vpn-dynamic/outputs.tf +++ b/modules/net-vpn-dynamic/outputs.tf @@ -37,7 +37,7 @@ output "random_secret" { output "router" { description = "Router resource (only if auto-created)." - value = var.router_create ? google_compute_router.router[0] : null + value = one(google_compute_router.router[*]) } output "router_name" { @@ -54,7 +54,7 @@ output "tunnel_names" { description = "VPN tunnel names." value = { for name in keys(var.tunnels) : - name => google_compute_vpn_tunnel.tunnels[name].name + name => try(google_compute_vpn_tunnel.tunnels[name].name, null) } } @@ -62,7 +62,7 @@ output "tunnel_self_links" { description = "VPN tunnel self links." value = { for name in keys(var.tunnels) : - name => google_compute_vpn_tunnel.tunnels[name].self_link + name => try(google_compute_vpn_tunnel.tunnels[name].self_link, null) } } @@ -70,6 +70,6 @@ output "tunnels" { description = "VPN tunnel resources." value = { for name in keys(var.tunnels) : - name => google_compute_vpn_tunnel.tunnels[name] + name => try(google_compute_vpn_tunnel.tunnels[name], null) } } diff --git a/modules/net-vpn-dynamic/variables.tf b/modules/net-vpn-dynamic/variables.tf index 6da5c0ac..33d23a04 100644 --- a/modules/net-vpn-dynamic/variables.tf +++ b/modules/net-vpn-dynamic/variables.tf @@ -17,7 +17,7 @@ variable "gateway_address" { description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false." type = string - default = "" + default = null } variable "gateway_address_create" { @@ -46,60 +46,43 @@ variable "region" { type = string } -variable "route_priority" { - description = "Route priority, defaults to 1000." - type = number - default = 1000 -} - -variable "router_advertise_config" { - description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions." +variable "router_config" { + description = "Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router." type = object({ - groups = list(string) - ip_ranges = map(string) - mode = string + create = optional(bool, true) + asn = number + name = optional(string) + keepalive = optional(number) + custom_advertise = optional(object({ + all_subnets = bool + ip_ranges = map(string) + })) }) - default = null -} - -variable "router_asn" { - description = "Router ASN used for auto-created router." - type = number - default = 64514 -} - -variable "router_create" { - description = "Create router." - type = bool - default = true -} - -variable "router_name" { - description = "Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router." - type = string - default = "" + nullable = false } variable "tunnels" { - description = "VPN tunnel configurations, bgp_peer_options is usually null." + description = "VPN tunnel configurations." type = map(object({ bgp_peer = object({ - address = string - asn = number - }) - bgp_peer_options = object({ - advertise_groups = list(string) - advertise_ip_ranges = map(string) - advertise_mode = string - route_priority = number + address = string + asn = number + route_priority = optional(number, 1000) + custom_advertise = optional(object({ + all_subnets = bool + all_vpc_subnets = bool + all_peer_vpc_subnets = bool + ip_ranges = map(string) + })) }) # each BGP session on the same Cloud Router must use a unique /30 CIDR # from the 169.254.0.0/16 block. bgp_session_range = string - ike_version = number + ike_version = optional(number, 2) peer_ip = string - router = string - shared_secret = string + router = optional(string) + shared_secret = optional(string) })) - default = {} + default = {} + nullable = false } diff --git a/modules/net-vpn-ha/README.md b/modules/net-vpn-ha/README.md index b79a25f1..e6cd752c 100644 --- a/modules/net-vpn-ha/README.md +++ b/modules/net-vpn-ha/README.md @@ -5,20 +5,21 @@ This module makes it easy to deploy either GCP-to-GCP or GCP-to-On-prem [Cloud H ### GCP to GCP ```hcl -module "vpn_ha-1" { - source = "./fabric/modules/net-vpn-ha" - project_id = "" - region = "europe-west4" - network = "https://www.googleapis.com/compute/v1/projects//global/networks/network-1" - name = "net1-to-net-2" - peer_gcp_gateway = module.vpn_ha-2.self_link - router_asn = 64514 - router_advertise_config = { - groups = ["ALL_SUBNETS"] - ip_ranges = { - "10.0.0.0/8" = "default" +module "vpn-1" { + source = "./fabric/modules/net-vpn-ha" + project_id = var.project_id + region = "europe-west4" + network = var.vpc1.self_link + name = "net1-to-net-2" + peer_gateway = { gcp = module.vpn-2.self_link } + router_config = { + asn = 64514 + custom_advertise = { + all_subnets = true + ip_ranges = { + "10.0.0.0/8" = "default" + } } - mode = "CUSTOM" } tunnels = { remote-0 = { @@ -26,64 +27,46 @@ module "vpn_ha-1" { address = "169.254.1.1" asn = 64513 } - bgp_peer_options = null - bgp_session_range = "169.254.1.2/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = "" - vpn_gateway_interface = 0 + bgp_session_range = "169.254.1.2/30" + vpn_gateway_interface = 0 } remote-1 = { bgp_peer = { address = "169.254.2.1" asn = 64513 } - bgp_peer_options = null - bgp_session_range = "169.254.2.2/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = "" - vpn_gateway_interface = 1 + bgp_session_range = "169.254.2.2/30" + vpn_gateway_interface = 1 } } } -module "vpn_ha-2" { - source = "./fabric/modules/net-vpn-ha" - project_id = "" - region = "europe-west4" - network = "https://www.googleapis.com/compute/v1/projects//global/networks/local-network" - name = "net2-to-net1" - router_asn = 64513 - peer_gcp_gateway = module.vpn_ha-1.self_link +module "vpn-2" { + source = "./fabric/modules/net-vpn-ha" + project_id = var.project_id + region = "europe-west4" + network = var.vpc2.self_link + name = "net2-to-net1" + router_config = { asn = 64513 } + peer_gateway = { gcp = module.vpn-1.self_link} tunnels = { remote-0 = { bgp_peer = { address = "169.254.1.2" asn = 64514 } - bgp_peer_options = null - bgp_session_range = "169.254.1.1/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.vpn_ha-1.random_secret - vpn_gateway_interface = 0 + bgp_session_range = "169.254.1.1/30" + shared_secret = module.vpn-1.random_secret + vpn_gateway_interface = 0 } remote-1 = { bgp_peer = { address = "169.254.2.2" asn = 64514 } - bgp_peer_options = null - bgp_session_range = "169.254.2.1/30" - ike_version = 2 - peer_external_gateway_interface = null - router = null - shared_secret = module.vpn_ha-1.random_secret - vpn_gateway_interface = 1 + bgp_session_range = "169.254.2.1/30" + shared_secret = module.vpn-1.random_secret + vpn_gateway_interface = 1 } } } @@ -101,25 +84,21 @@ module "vpn_ha" { region = var.region network = var.vpc.self_link name = "mynet-to-onprem" - peer_external_gateway = { - redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" - interfaces = [{ - id = 0 - ip_address = "8.8.8.8" # on-prem router ip address - }] + peer_gateway = { + external = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = ["8.8.8.8"] # on-prem router ip address + } } - router_asn = 64514 + router_config = { asn = 64514 } tunnels = { remote-0 = { bgp_peer = { address = "169.254.1.1" asn = 64513 } - bgp_peer_options = null bgp_session_range = "169.254.1.2/30" - ike_version = 2 peer_external_gateway_interface = 0 - router = null shared_secret = "mySecret" vpn_gateway_interface = 0 } @@ -128,11 +107,8 @@ module "vpn_ha" { address = "169.254.2.1" asn = 64513 } - bgp_peer_options = null bgp_session_range = "169.254.2.2/30" - ike_version = 2 peer_external_gateway_interface = 0 - router = null shared_secret = "mySecret" vpn_gateway_interface = 1 } @@ -148,18 +124,13 @@ module "vpn_ha" { |---|---|:---:|:---:|:---:| | [name](variables.tf#L17) | VPN Gateway name (if an existing VPN Gateway is not used), and prefix used for dependent resources. | string | ✓ | | | [network](variables.tf#L22) | VPC used for the gateway and routes. | string | ✓ | | -| [project_id](variables.tf#L45) | Project where resources will be created. | string | ✓ | | -| [region](variables.tf#L50) | Region used for resources. | string | ✓ | | -| [peer_external_gateway](variables.tf#L27) | Configuration of an external VPN gateway to which this VPN is connected. | object({…}) | | null | -| [peer_gcp_gateway](variables.tf#L39) | Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected. | string | | null | -| [route_priority](variables.tf#L55) | Route priority, defaults to 1000. | number | | 1000 | -| [router_advertise_config](variables.tf#L61) | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({…}) | | null | -| [router_asn](variables.tf#L71) | Router ASN used for auto-created router. | number | | 64514 | -| [router_create](variables.tf#L77) | Create router. | bool | | true | -| [router_name](variables.tf#L83) | Router name used for auto created router, or to specify an existing router to use if `router_create` is set to `true`. Leave blank to use VPN name for auto created router. | string | | "" | -| [tunnels](variables.tf#L89) | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({…})) | | {} | -| [vpn_gateway](variables.tf#L114) | HA VPN Gateway Self Link for using an existing HA VPN Gateway, leave empty if `vpn_gateway_create` is set to `true`. | string | | null | -| [vpn_gateway_create](variables.tf#L120) | Create HA VPN Gateway. | bool | | true | +| [peer_gateway](variables.tf#L27) | Configuration of the (external or GCP) peer gateway. | object({…}) | ✓ | | +| [project_id](variables.tf#L43) | Project where resources will be created. | string | ✓ | | +| [region](variables.tf#L48) | Region used for resources. | string | ✓ | | +| [router_config](variables.tf#L53) | Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router. | object({…}) | ✓ | | +| [tunnels](variables.tf#L68) | VPN tunnel configurations. | map(object({…})) | | {} | +| [vpn_gateway](variables.tf#L95) | HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`. | string | | null | +| [vpn_gateway_create](variables.tf#L101) | Create HA VPN Gateway. | bool | | true | ## Outputs @@ -167,14 +138,14 @@ module "vpn_ha" { |---|---|:---:| | [bgp_peers](outputs.tf#L18) | BGP peer resources. | | | [external_gateway](outputs.tf#L25) | External VPN gateway resource. | | -| [gateway](outputs.tf#L34) | VPN gateway resource (only if auto-created). | | -| [name](outputs.tf#L43) | VPN gateway name (only if auto-created). . | | -| [random_secret](outputs.tf#L52) | Generated secret. | | -| [router](outputs.tf#L57) | Router resource (only if auto-created). | | -| [router_name](outputs.tf#L66) | Router name. | | -| [self_link](outputs.tf#L71) | HA VPN gateway self link. | | -| [tunnel_names](outputs.tf#L76) | VPN tunnel names. | | -| [tunnel_self_links](outputs.tf#L84) | VPN tunnel self links. | | -| [tunnels](outputs.tf#L92) | VPN tunnel resources. | | +| [gateway](outputs.tf#L30) | VPN gateway resource (only if auto-created). | | +| [name](outputs.tf#L35) | VPN gateway name (only if auto-created). . | | +| [random_secret](outputs.tf#L40) | Generated secret. | | +| [router](outputs.tf#L45) | Router resource (only if auto-created). | | +| [router_name](outputs.tf#L50) | Router name. | | +| [self_link](outputs.tf#L55) | HA VPN gateway self link. | | +| [tunnel_names](outputs.tf#L60) | VPN tunnel names. | | +| [tunnel_self_links](outputs.tf#L68) | VPN tunnel self links. | | +| [tunnels](outputs.tf#L76) | VPN tunnel resources. | | diff --git a/modules/net-vpn-ha/main.tf b/modules/net-vpn-ha/main.tf index 8c487031..9d53ee08 100644 --- a/modules/net-vpn-ha/main.tf +++ b/modules/net-vpn-ha/main.tf @@ -16,16 +16,10 @@ */ locals { - peer_external_gateway = ( - var.peer_external_gateway != null - ? google_compute_external_vpn_gateway.external_gateway[0].self_link - : null - - ) router = ( - var.router_create + var.router_config.create ? try(google_compute_router.router[0].name, null) - : var.router_name + : var.router_config.name ) vpn_gateway = ( var.vpn_gateway_create @@ -36,100 +30,79 @@ locals { } resource "google_compute_ha_vpn_gateway" "ha_gateway" { - provider = google-beta - count = var.vpn_gateway_create ? 1 : 0 - name = var.name - project = var.project_id - region = var.region - network = var.network + count = var.vpn_gateway_create ? 1 : 0 + name = var.name + project = var.project_id + region = var.region + network = var.network } resource "google_compute_external_vpn_gateway" "external_gateway" { - provider = google-beta - count = var.peer_external_gateway != null ? 1 : 0 + count = var.peer_gateway.external != null ? 1 : 0 name = "external-${var.name}" project = var.project_id - redundancy_type = var.peer_external_gateway.redundancy_type + redundancy_type = var.peer_gateway.external.redundancy_type description = "Terraform managed external VPN gateway" dynamic "interface" { - for_each = var.peer_external_gateway.interfaces + for_each = var.peer_gateway.external.interfaces content { - id = interface.value.id - ip_address = interface.value.ip_address + id = interface.key + ip_address = interface.value } } } resource "google_compute_router" "router" { - count = var.router_create ? 1 : 0 - name = var.router_name == "" ? "vpn-${var.name}" : var.router_name + count = var.router_config.create ? 1 : 0 + name = coalesce(var.router_config.name, "vpn-${var.name}") project = var.project_id region = var.region network = var.network bgp { advertise_mode = ( - var.router_advertise_config == null - ? null - : var.router_advertise_config.mode + var.router_config.custom_advertise != null + ? "CUSTOM" + : "DEFAULT" ) advertised_groups = ( - var.router_advertise_config == null ? null : ( - var.router_advertise_config.mode != "CUSTOM" - ? null - : var.router_advertise_config.groups - ) + try(var.router_config.custom_advertise.all_subnets, false) + ? ["ALL_SUBNETS"] + : [] ) dynamic "advertised_ip_ranges" { - for_each = ( - var.router_advertise_config == null ? {} : ( - var.router_advertise_config.mode != "CUSTOM" - ? null - : var.router_advertise_config.ip_ranges - ) - ) + for_each = try(var.router_config.custom_advertise.ip_ranges, {}) iterator = range content { range = range.key description = range.value } } - asn = var.router_asn + keepalive_interval = try(var.router_config.keepalive, null) + asn = var.router_config.asn } } resource "google_compute_router_peer" "bgp_peer" { - for_each = var.tunnels - region = var.region - project = var.project_id - name = "${var.name}-${each.key}" - router = local.router - peer_ip_address = each.value.bgp_peer.address - peer_asn = each.value.bgp_peer.asn - advertised_route_priority = ( - each.value.bgp_peer_options == null ? var.route_priority : ( - each.value.bgp_peer_options.route_priority == null - ? var.route_priority - : each.value.bgp_peer_options.route_priority - ) - ) + for_each = var.tunnels + region = var.region + project = var.project_id + name = "${var.name}-${each.key}" + router = coalesce(each.value.router, local.router) + peer_ip_address = each.value.bgp_peer.address + peer_asn = each.value.bgp_peer.asn + advertised_route_priority = each.value.bgp_peer.route_priority advertise_mode = ( - each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode + try(each.value.bgp_peer.custom_advertise, null) != null + ? "CUSTOM" + : "DEFAULT" ) - advertised_groups = ( - each.value.bgp_peer_options == null ? null : ( - each.value.bgp_peer_options.advertise_mode != "CUSTOM" - ? null - : each.value.bgp_peer_options.advertise_groups - ) + advertised_groups = concat( + try(each.value.bgp_peer.custom_advertise.all_subnets, false) ? ["ALL_SUBNETS"] : [], + try(each.value.bgp_peer.custom_advertise.all_vpc_subnets, false) ? ["ALL_VPC_SUBNETS"] : [], + try(each.value.bgp_peer.custom_advertise.all_peer_vpc_subnets, false) ? ["ALL_PEER_VPC_SUBNETS"] : [] ) dynamic "advertised_ip_ranges" { - for_each = ( - each.value.bgp_peer_options == null ? {} : ( - each.value.bgp_peer_options.advertise_mode != "CUSTOM" - ? {} - : each.value.bgp_peer_options.advertise_ip_ranges - ) - ) + for_each = try(each.value.bgp_peer.custom_advertise.ip_ranges, {}) iterator = range content { range = range.key @@ -140,33 +113,29 @@ resource "google_compute_router_peer" "bgp_peer" { } resource "google_compute_router_interface" "router_interface" { - for_each = var.tunnels - project = var.project_id - region = var.region - name = "${var.name}-${each.key}" - router = local.router + for_each = var.tunnels + project = var.project_id + region = var.region + name = "${var.name}-${each.key}" + router = local.router + # FIXME: can bgp_session_range be null? ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name } resource "google_compute_vpn_tunnel" "tunnels" { - provider = google-beta for_each = var.tunnels project = var.project_id region = var.region name = "${var.name}-${each.key}" router = local.router - peer_external_gateway = local.peer_external_gateway + peer_external_gateway = one(google_compute_external_vpn_gateway.external_gateway[*].self_link) peer_external_gateway_interface = each.value.peer_external_gateway_interface - peer_gcp_gateway = var.peer_gcp_gateway + peer_gcp_gateway = var.peer_gateway.gcp vpn_gateway_interface = each.value.vpn_gateway_interface ike_version = each.value.ike_version - shared_secret = ( - each.value.shared_secret == "" || each.value.shared_secret == null - ? local.secret - : each.value.shared_secret - ) - vpn_gateway = local.vpn_gateway + shared_secret = coalesce(each.value.shared_secret, local.secret) + vpn_gateway = local.vpn_gateway } resource "random_id" "secret" { diff --git a/modules/net-vpn-ha/outputs.tf b/modules/net-vpn-ha/outputs.tf index 94aac982..98b83394 100644 --- a/modules/net-vpn-ha/outputs.tf +++ b/modules/net-vpn-ha/outputs.tf @@ -24,29 +24,17 @@ output "bgp_peers" { output "external_gateway" { description = "External VPN gateway resource." - value = ( - var.peer_external_gateway != null - ? google_compute_external_vpn_gateway.external_gateway[0] - : null - ) + value = one(google_compute_external_vpn_gateway.external_gateway[*]) } output "gateway" { description = "VPN gateway resource (only if auto-created)." - value = ( - var.vpn_gateway_create - ? google_compute_ha_vpn_gateway.ha_gateway[0] - : null - ) + value = one(google_compute_ha_vpn_gateway.ha_gateway[*]) } output "name" { description = "VPN gateway name (only if auto-created). ." - value = ( - var.vpn_gateway_create - ? google_compute_ha_vpn_gateway.ha_gateway[0].name - : null - ) + value = one(google_compute_ha_vpn_gateway.ha_gateway[*].name) } output "random_secret" { @@ -56,11 +44,7 @@ output "random_secret" { output "router" { description = "Router resource (only if auto-created)." - value = ( - var.router_name == "" - ? google_compute_router.router[0] - : null - ) + value = one(google_compute_router.router[*]) } output "router_name" { diff --git a/modules/net-vpn-ha/variables.tf b/modules/net-vpn-ha/variables.tf index 4e8d17ac..a423eab1 100644 --- a/modules/net-vpn-ha/variables.tf +++ b/modules/net-vpn-ha/variables.tf @@ -24,22 +24,20 @@ variable "network" { type = string } -variable "peer_external_gateway" { - description = "Configuration of an external VPN gateway to which this VPN is connected." +variable "peer_gateway" { + description = "Configuration of the (external or GCP) peer gateway." type = object({ - redundancy_type = string - interfaces = list(object({ - id = number - ip_address = string + external = optional(object({ + redundancy_type = string + interfaces = list(string) })) + gcp = optional(string) }) - default = null -} - -variable "peer_gcp_gateway" { - description = "Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected." - type = string - default = null + nullable = false + validation { + condition = (var.peer_gateway.external != null) != (var.peer_gateway.gcp != null) + error_message = "Peer gateway configuration must define exactly one between `external` and `gcp`." + } } variable "project_id" { @@ -52,67 +50,50 @@ variable "region" { type = string } -variable "route_priority" { - description = "Route priority, defaults to 1000." - type = number - default = 1000 -} - -variable "router_advertise_config" { - description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions." +variable "router_config" { + description = "Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router." type = object({ - groups = list(string) - ip_ranges = map(string) - mode = string + create = optional(bool, true) + asn = number + name = optional(string) + keepalive = optional(number) + custom_advertise = optional(object({ + all_subnets = bool + ip_ranges = map(string) + })) }) - default = null -} - -variable "router_asn" { - description = "Router ASN used for auto-created router." - type = number - default = 64514 -} - -variable "router_create" { - description = "Create router." - type = bool - default = true -} - -variable "router_name" { - description = "Router name used for auto created router, or to specify an existing router to use if `router_create` is set to `true`. Leave blank to use VPN name for auto created router." - type = string - default = "" + nullable = false } variable "tunnels" { - description = "VPN tunnel configurations, bgp_peer_options is usually null." + description = "VPN tunnel configurations." type = map(object({ bgp_peer = object({ - address = string - asn = number - }) - bgp_peer_options = object({ - advertise_groups = list(string) - advertise_ip_ranges = map(string) - advertise_mode = string - route_priority = number + address = string + asn = number + route_priority = optional(number, 1000) + custom_advertise = optional(object({ + all_subnets = bool + all_vpc_subnets = bool + all_peer_vpc_subnets = bool + ip_ranges = map(string) + })) }) # each BGP session on the same Cloud Router must use a unique /30 CIDR # from the 169.254.0.0/16 block. bgp_session_range = string - ike_version = number - peer_external_gateway_interface = number - router = string - shared_secret = string + ike_version = optional(number, 2) + peer_external_gateway_interface = optional(number) + router = optional(string) + shared_secret = optional(string) vpn_gateway_interface = number })) - default = {} + default = {} + nullable = false } variable "vpn_gateway" { - description = "HA VPN Gateway Self Link for using an existing HA VPN Gateway, leave empty if `vpn_gateway_create` is set to `true`." + description = "HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`." type = string default = null } diff --git a/modules/net-vpn-static/README.md b/modules/net-vpn-static/README.md index 92745e2d..836746dc 100644 --- a/modules/net-vpn-static/README.md +++ b/modules/net-vpn-static/README.md @@ -12,17 +12,16 @@ module "addresses" { } module "vpn" { - source = "./fabric/modules/net-vpn-static" - project_id = var.project_id - region = var.region - network = var.vpc.self_link - name = "remote" + source = "./fabric/modules/net-vpn-static" + project_id = var.project_id + region = var.region + network = var.vpc.self_link + name = "remote" gateway_address_create = false gateway_address = module.addresses.external_addresses["vpn"].address - remote_ranges = ["10.10.0.0/24"] + remote_ranges = ["10.10.0.0/24"] tunnels = { remote-0 = { - ike_version = 2 peer_ip = "1.1.1.1" shared_secret = "mysecret" traffic_selectors = { local = ["0.0.0.0/0"], remote = ["0.0.0.0/0"] } @@ -41,11 +40,11 @@ module "vpn" { | [network](variables.tf#L34) | VPC used for the gateway and routes. | string | ✓ | | | [project_id](variables.tf#L39) | Project where resources will be created. | string | ✓ | | | [region](variables.tf#L44) | Region used for resources. | string | ✓ | | -| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | "" | +| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string | | null | | [gateway_address_create](variables.tf#L23) | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool | | true | | [remote_ranges](variables.tf#L49) | Remote IP CIDR ranges. | list(string) | | [] | -| [route_priority](variables.tf#L55) | Route priority, defaults to 1000. | number | | 1000 | -| [tunnels](variables.tf#L61) | VPN tunnel configurations. | map(object({…})) | | {} | +| [route_priority](variables.tf#L56) | Route priority, defaults to 1000. | number | | 1000 | +| [tunnels](variables.tf#L62) | VPN tunnel configurations. | map(object({…})) | | {} | ## Outputs diff --git a/modules/net-vpn-static/main.tf b/modules/net-vpn-static/main.tf index 3c8f5cb8..f05771c1 100644 --- a/modules/net-vpn-static/main.tf +++ b/modules/net-vpn-static/main.tf @@ -91,7 +91,7 @@ resource "google_compute_vpn_tunnel" "tunnels" { local_traffic_selector = each.value.traffic_selectors.local remote_traffic_selector = each.value.traffic_selectors.remote ike_version = each.value.ike_version - shared_secret = each.value.shared_secret == "" ? local.secret : each.value.shared_secret + shared_secret = coalesce(each.value.shared_secret, local.secret) target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link depends_on = [google_compute_forwarding_rule.esp] } diff --git a/modules/net-vpn-static/variables.tf b/modules/net-vpn-static/variables.tf index 90b15f53..935c543a 100644 --- a/modules/net-vpn-static/variables.tf +++ b/modules/net-vpn-static/variables.tf @@ -17,7 +17,7 @@ variable "gateway_address" { description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false." type = string - default = "" + default = null } variable "gateway_address_create" { @@ -50,6 +50,7 @@ variable "remote_ranges" { description = "Remote IP CIDR ranges." type = list(string) default = [] + nullable = false } variable "route_priority" { @@ -61,13 +62,14 @@ variable "route_priority" { variable "tunnels" { description = "VPN tunnel configurations." type = map(object({ - ike_version = number + ike_version = optional(number, 2) peer_ip = string - shared_secret = string + shared_secret = optional(string) traffic_selectors = object({ local = list(string) remote = list(string) }) })) - default = {} + default = {} + nullable = false } diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index a78158e1..8ba38c10 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -193,7 +193,7 @@ module "test" { | [egress_policies](variables.tf#L70) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | [ingress_policies](variables.tf#L99) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | [service_perimeters_bridge](variables.tf#L130) | Bridge service perimeters. | map(object({…})) | | {} | -| [service_perimeters_regular](variables.tf#L140) | Regular service perimeters. | map(object({…})) | | {} | +| [service_perimeters_regular](variables.tf#L140) | Regular service perimeters. | map(object({…})) | | {} | ## Outputs diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf index 5ef41c32..5b87ca3f 100644 --- a/modules/vpc-sc/service-perimeters-regular.tf +++ b/modules/vpc-sc/service-perimeters-regular.tf @@ -28,20 +28,21 @@ resource "google_access_context_manager_service_perimeter" "regular" { perimeter_type = "PERIMETER_TYPE_REGULAR" use_explicit_dry_run_spec = each.value.use_explicit_dry_run_spec dynamic "spec" { - for_each = each.value.spec == null ? [] : [""] + for_each = each.value.spec == null ? [] : [each.value.spec] + iterator = spec content { access_levels = ( - each.value.spec.access_levels == null ? null : [ - for k in each.value.spec.access_levels : + spec.value.access_levels == null ? null : [ + for k in spec.value.access_levels : try(google_access_context_manager_access_level.basic[k].id, k) ] ) - resources = each.value.spec.resources - restricted_services = each.value.spec.restricted_services + resources = spec.value.resources + restricted_services = spec.value.restricted_services dynamic "egress_policies" { - for_each = each.value.spec.egress_policies == null ? {} : { - for k in each.value.spec.egress_policies : + for_each = spec.value.egress_policies == null ? {} : { + for k in spec.value.egress_policies : k => lookup(var.egress_policies, k, null) if contains(keys(var.egress_policies), k) } @@ -77,8 +78,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "ingress_policies" { - for_each = each.value.spec.ingress_policies == null ? {} : { - for k in each.value.spec.ingress_policies : + for_each = spec.value.ingress_policies == null ? {} : { + for k in spec.value.ingress_policies : k => lookup(var.ingress_policies, k, null) if contains(keys(var.ingress_policies), k) } @@ -129,30 +130,31 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "vpc_accessible_services" { - for_each = each.value.spec.vpc_accessible_services == null ? {} : { 1 = 1 } + for_each = spec.value.vpc_accessible_services == null ? {} : { 1 = 1 } content { - allowed_services = each.value.spec.vpc_accessible_services.allowed_services - enable_restriction = each.value.spec.vpc_accessible_services.enable_restriction + allowed_services = spec.value.vpc_accessible_services.allowed_services + enable_restriction = spec.value.vpc_accessible_services.enable_restriction } } } } dynamic "status" { - for_each = each.value.status == null ? {} : { 1 = 1 } + for_each = each.value.status == null ? [] : [each.value.status] + iterator = status content { access_levels = ( - each.value.status.access_levels == null ? null : [ - for k in each.value.status.access_levels : + status.value.access_levels == null ? null : [ + for k in status.value.access_levels : try(google_access_context_manager_access_level.basic[k].id, k) ] ) - resources = each.value.status.resources - restricted_services = each.value.status.restricted_services + resources = status.value.resources + restricted_services = status.value.restricted_services dynamic "egress_policies" { - for_each = each.value.spec.egress_policies == null ? {} : { - for k in each.value.spec.egress_policies : + for_each = status.value.egress_policies == null ? {} : { + for k in status.value.egress_policies : k => lookup(var.egress_policies, k, null) if contains(keys(var.egress_policies), k) } @@ -188,8 +190,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "ingress_policies" { - for_each = each.value.spec.ingress_policies == null ? {} : { - for k in each.value.spec.ingress_policies : + for_each = status.value.ingress_policies == null ? {} : { + for k in status.value.ingress_policies : k => lookup(var.ingress_policies, k, null) if contains(keys(var.ingress_policies), k) } @@ -205,7 +207,8 @@ resource "google_access_context_manager_service_perimeter" "regular" { iterator = s content { access_level = try( - google_access_context_manager_access_level.basic[s.value].id, s.value + google_access_context_manager_access_level.basic[s.value].id, + s.value ) } } @@ -240,10 +243,10 @@ resource "google_access_context_manager_service_perimeter" "regular" { } dynamic "vpc_accessible_services" { - for_each = each.value.status.vpc_accessible_services == null ? {} : { 1 = 1 } + for_each = status.value.vpc_accessible_services == null ? {} : { 1 = 1 } content { - allowed_services = each.value.status.vpc_accessible_services.allowed_services - enable_restriction = each.value.status.vpc_accessible_services.enable_restriction + allowed_services = status.value.vpc_accessible_services.allowed_services + enable_restriction = status.value.vpc_accessible_services.enable_restriction } } diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index 1be23ae3..a196cc52 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.tf @@ -92,7 +92,7 @@ variable "egress_policies" { "ANY_USER", "ANY_SERVICE_ACCOUNT" ], v.from.identity_type) ]) - error_message = "Invalid `from.identity_type` value in eress policy." + error_message = "Invalid `from.identity_type` value in egress policy." } } @@ -150,7 +150,7 @@ variable "service_perimeters_regular" { allowed_services = list(string) enable_restriction = bool })) - }), {}) + })) status = optional(object({ access_levels = optional(list(string)) resources = optional(list(string)) @@ -161,7 +161,7 @@ variable "service_perimeters_regular" { allowed_services = list(string) enable_restriction = bool })) - }), {}) + })) use_explicit_dry_run_spec = optional(bool, false) })) default = {} diff --git a/tests/blueprints/cloud_operations/apigee/fixture/variables.tf b/tests/blueprints/cloud_operations/apigee/fixture/variables.tf index fc79ae07..d66f3874 100644 --- a/tests/blueprints/cloud_operations/apigee/fixture/variables.tf +++ b/tests/blueprints/cloud_operations/apigee/fixture/variables.tf @@ -79,9 +79,8 @@ variable "environments" { display_name = optional(string) description = optional(string) node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - current_aggregate_node_count = number + min_node_count = optional(number) + max_node_count = optional(number) })) iam = optional(map(list(string))) envgroups = list(string) diff --git a/tests/collectors.py b/tests/collectors.py new file mode 100644 index 00000000..e69782f0 --- /dev/null +++ b/tests/collectors.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. +"""Pytest plugin to discover tests specified in YAML files. + +This plugin uses the pytest_collect_file hook to collect all files +matching tftest*.yaml and runs plan_validate for each test found. +See FabricTestFile for details on the file structure. + +""" + +import pytest +import yaml + +from .fixtures import plan_summary, plan_validator + + +class FabricTestFile(pytest.File): + + def collect(self): + """Read yaml test spec and yield test items for each test definition. + + The test spec should contain a `module` key with the path of the + terraform module to test, relative to the root of the repository + + Tests are defined within the top-level `tests` key, and should + have the following structure: + + test-name: + tfvars: + - tfvars1.tfvars + - tfvars2.tfvars + inventory: + - inventory1.yaml + - inventory2.yaml + + All paths specifications are relative to the location of the test + spec. The inventory key is optional, if omitted, the inventory + will be taken from the file test-name.yaml + + """ + + try: + raw = yaml.safe_load(self.path.open()) + module = raw.pop('module') + except (IOError, OSError, yaml.YAMLError) as e: + raise Exception(f'cannot read test spec {self.path}: {e}') + except KeyError as e: + raise Exception(f'`module` key not found in {self.path}: {e}') + common = raw.pop('common_tfvars', []) + for test_name, spec in raw.get('tests', {}).items(): + spec = {} if spec is None else spec + inventories = spec.get('inventory', [f'{test_name}.yaml']) + tfvars = common + [f'{test_name}.tfvars'] + spec.get('tfvars', []) + for i in inventories: + name = test_name + if isinstance(inventories, list) and len(inventories) > 1: + name = f'{test_name}[{i}]' + yield FabricTestItem.from_parent(self, name=name, module=module, + inventory=[i], tfvars=tfvars) + + +class FabricTestItem(pytest.Item): + + def __init__(self, name, parent, module, inventory, tfvars): + super().__init__(name, parent) + self.module = module + self.inventory = inventory + self.tfvars = tfvars + + def runtest(self): + s = plan_validator(self.module, self.inventory, self.parent.path.parent, + self.tfvars) + + def reportinfo(self): + return self.path, None, self.name + + +def pytest_collect_file(parent, file_path): + 'Collect tftest*.yaml files and run plan_validator from them.' + if file_path.suffix == '.yaml' and file_path.name.startswith('tftest'): + return FabricTestFile.from_parent(parent, path=file_path) diff --git a/tests/conftest.py b/tests/conftest.py index d676bbc0..167c74f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,144 +11,12 @@ # 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. -"Shared fixtures" - -import inspect -import os -import shutil -import tempfile +'Pytest configuration.' import pytest -import tftest -BASEDIR = os.path.dirname(os.path.dirname(__file__)) - - -@pytest.fixture(scope='session') -def _plan_runner(): - "Returns a function to run Terraform plan on a fixture." - - 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 - caller = inspect.stack()[2] - fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture") - - fixture_parent = os.path.dirname(fixture_path) - fixture_prefix = os.path.basename(fixture_path) + "_" - with tempfile.TemporaryDirectory(prefix=fixture_prefix, - dir=fixture_parent) as tmp_path: - # copy fixture to a temporary directory so we can execute - # multiple tests in parallel - if tmpdir: - 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(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 - - -@pytest.fixture(scope='session') -def plan_runner(_plan_runner): - "Returns a function to run Terraform plan on a module fixture." - - 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, 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'] - - return run_plan - - -@pytest.fixture(scope='session') -def e2e_plan_runner(_plan_runner): - "Returns a function to run Terraform plan on an end-to-end fixture." - - def run_plan(fixture_path=None, tf_var_file=None, targets=None, - refresh=True, include_bare_resources=False, **tf_vars): - "Runs Terraform plan on an end-to-end module using defaults, returns data." - plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets, - refresh=refresh, **tf_vars) - # skip the fixture - root_module = plan.root_module['child_modules'][0] - modules = dict((mod['address'], mod['resources']) - for mod in root_module['child_modules']) - resources = [r for m in modules.values() for r in m] - if include_bare_resources: - bare_resources = root_module['resources'] - resources.extend(bare_resources) - return modules, resources - - return run_plan - - -@pytest.fixture(scope='session') -def recursive_e2e_plan_runner(_plan_runner): - """Plan runner for end-to-end root module, returns total number of - (nested) modules and resources""" - - 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.get('child_modules', []) - resources += node.get('resources', []) - modules += new_modules - for module in new_modules: - walk_plan(module, modules, resources) - - def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True, - include_bare_resources=False, compute_sums=True, tmpdir=True, - **tf_vars): - "Runs Terraform plan on a root module using defaults, returns data." - plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets, - refresh=refresh, tmpdir=tmpdir, **tf_vars) - modules = [] - resources = [] - walk_plan(plan.root_module, modules, resources) - return len(modules), len(resources) - - return run_plan - - -@pytest.fixture(scope='session') -def apply_runner(): - "Returns a function to run Terraform apply on a fixture." - - def run_apply(fixture_path=None, **tf_vars): - "Runs Terraform plan and returns parsed output." - if fixture_path is None: - # find out the fixture directory from the caller's directory - caller = inspect.stack()[1] - fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture") - - fixture_parent = os.path.dirname(fixture_path) - fixture_prefix = os.path.basename(fixture_path) + "_" - - with tempfile.TemporaryDirectory(prefix=fixture_prefix, - dir=fixture_parent) as tmp_path: - # copy fixture to a temporary directory so we can execute - # multiple tests in parallel - shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True) - tf = tftest.TerraformTest(tmp_path, BASEDIR, - os.environ.get('TERRAFORM', 'terraform')) - tf.setup(upgrade=True) - apply = tf.apply(tf_vars=tf_vars) - output = tf.output(json_format=True) - return apply, output - - return run_apply - - -@pytest.fixture -def basedir(): - return BASEDIR +pytest_plugins = ( + 'tests.fixtures', + 'tests.legacy_fixtures', + 'tests.collectors', +) diff --git a/tests/examples/variables.tf b/tests/examples/variables.tf index 1924ac40..76c3770d 100644 --- a/tests/examples/variables.tf +++ b/tests/examples/variables.tf @@ -68,14 +68,21 @@ variable "subnet" { variable "vpc" { default = { name = "vpc_name" - self_link = "projects/xxx/global/networks/yyy" + self_link = "projects/xxx/global/networks/aaa" + } +} + +variable "vpc1" { + default = { + name = "vpc_name" + self_link = "projects/xxx/global/networks/bbb" } } variable "vpc2" { default = { name = "vpc2_name" - self_link = "vpc2_self_link" + self_link = "projects/xxx/global/networks/ccc" } } diff --git a/tests/fast/stages/s00_bootstrap/fixture/main.tf b/tests/fast/stages/s00_bootstrap/fixture/main.tf deleted file mode 100644 index 1f07048a..00000000 --- a/tests/fast/stages/s00_bootstrap/fixture/main.tf +++ /dev/null @@ -1,29 +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. - */ - -module "stage" { - source = "../../../../../fast/stages/00-bootstrap" - prefix = "fast" - organization = { - domain = "fast.example.com" - id = 123456789012 - customer_id = "C00000000" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } -} diff --git a/tests/fast/stages/s00_bootstrap/simple.tfvars b/tests/fast/stages/s00_bootstrap/simple.tfvars new file mode 100644 index 00000000..f8ef5735 --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/simple.tfvars @@ -0,0 +1,11 @@ +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +billing_account = { + id = "000000-111111-222222" + organization_id = 123456789012 +} +prefix = "fast" +outputs_location = "/fast-config" diff --git a/tests/fast/stages/s00_bootstrap/simple.yaml b/tests/fast/stages/s00_bootstrap/simple.yaml new file mode 100644 index 00000000..703b84b4 --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/simple.yaml @@ -0,0 +1,49 @@ +# 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. + +counts: + google_bigquery_dataset: 2 + google_bigquery_dataset_iam_member: 2 + google_bigquery_default_service_account: 3 + google_logging_organization_sink: 2 + google_organization_iam_binding: 19 + google_organization_iam_custom_role: 2 + google_organization_iam_member: 16 + google_project: 3 + google_project_iam_binding: 9 + google_project_iam_member: 1 + google_project_service: 29 + google_project_service_identity: 2 + google_service_account: 3 + google_service_account_iam_binding: 3 + google_storage_bucket: 4 + google_storage_bucket_iam_binding: 2 + google_storage_bucket_iam_member: 3 + google_storage_bucket_object: 5 + google_storage_project_service_account: 3 + local_file: 5 + +outputs: + custom_roles: + organization_iam_admin: organizations/123456789012/roles/organizationIamAdmin + service_project_network_admin: organizations/123456789012/roles/serviceProjectNetworkAdmin + outputs_bucket: fast-prod-iac-core-outputs-0 + project_ids: + automation: fast-prod-iac-core-0 + billing-export: fast-prod-billing-exp-0 + log-export: fast-prod-audit-logs-0 + service_accounts: + bootstrap: fast-prod-bootstrap-0@fast-prod-iac-core-0.iam.gserviceaccount.com + cicd: fast-prod-cicd-0@fast-prod-iac-core-0.iam.gserviceaccount.com + resman: fast-prod-resman-0@fast-prod-iac-core-0.iam.gserviceaccount.com diff --git a/tests/fast/stages/s00_bootstrap/simple_projects.yaml b/tests/fast/stages/s00_bootstrap/simple_projects.yaml new file mode 100644 index 00000000..c4d359f3 --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/simple_projects.yaml @@ -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. + +values: + module.automation-project.google_project.project[0]: + auto_create_network: false + billing_account: 000000-111111-222222 + name: fast-prod-iac-core-0 + org_id: '123456789012' + project_id: fast-prod-iac-core-0 + module.billing-export-project[0].google_project.project[0]: + auto_create_network: false + billing_account: 000000-111111-222222 + name: fast-prod-billing-exp-0 + org_id: '123456789012' + project_id: fast-prod-billing-exp-0 + module.log-export-project.google_project.project[0]: + auto_create_network: false + billing_account: 000000-111111-222222 + name: fast-prod-audit-logs-0 + org_id: '123456789012' + project_id: fast-prod-audit-logs-0 diff --git a/tests/fast/stages/s00_bootstrap/simple_sas.yaml b/tests/fast/stages/s00_bootstrap/simple_sas.yaml new file mode 100644 index 00000000..ba84948d --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/simple_sas.yaml @@ -0,0 +1,27 @@ +# 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. + +values: + module.automation-tf-bootstrap-sa.google_service_account.service_account[0]: + account_id: fast-prod-bootstrap-0 + display_name: Terraform organization bootstrap service account. + project: fast-prod-iac-core-0 + module.automation-tf-cicd-provisioning-sa.google_service_account.service_account[0]: + account_id: fast-prod-cicd-0 + display_name: Terraform stage 1 CICD service account. + project: fast-prod-iac-core-0 + module.automation-tf-resman-sa.google_service_account.service_account[0]: + account_id: fast-prod-resman-0 + display_name: Terraform stage 1 resman service account. + project: fast-prod-iac-core-0 diff --git a/tests/fast/stages/s00_bootstrap/test_plan.py b/tests/fast/stages/s00_bootstrap/test_plan.py deleted file mode 100644 index 78146970..00000000 --- a/tests/fast/stages/s00_bootstrap/test_plan.py +++ /dev/null @@ -1,33 +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. - -# _RESOURCE_COUNT = { -# 'module.organization': 28, -# 'module.automation-project': 23, -# 'module.automation-tf-bootstrap-gcs': 1, -# 'module.automation-tf-bootstrap-sa': 1, -# 'module.automation-tf-resman-gcs': 2, -# 'module.automation-tf-resman-sa': 1, -# 'module.billing-export-dataset': 1, -# 'module.billing-export-project': 7, -# 'module.log-export-dataset': 1, -# 'module.log-export-project': 7, -# } - - -def test_counts(recursive_e2e_plan_runner): - "Test stage." - # TODO: to re-enable per-module resource count check print _, then test - num_modules, num_resources = recursive_e2e_plan_runner() - assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s00_bootstrap/tftest.yaml b/tests/fast/stages/s00_bootstrap/tftest.yaml new file mode 100644 index 00000000..4656859b --- /dev/null +++ b/tests/fast/stages/s00_bootstrap/tftest.yaml @@ -0,0 +1,12 @@ +# skip boilerplate check + +module: fast/stages/00-bootstrap + +tests: + simple: + tfvars: + - simple.tfvars + inventory: + - simple.yaml + - simple_projects.yaml + - simple_sas.yaml diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..c483063f --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,235 @@ +# 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. +"""Common fixtures.""" + +import collections +import contextlib +import itertools +import os +import shutil +import tempfile +from pathlib import Path + +import pytest +import tftest +import yaml + +PlanSummary = collections.namedtuple('PlanSummary', 'values counts outputs') + + +@contextlib.contextmanager +def _prepare_root_module(path): + """Context manager to prepare a terraform module to be tested. + + If the TFTEST_COPY environment variable is set, `path` is copied to + a temporary directory and a few terraform files (e.g. + terraform.tfvars) are delete to ensure a clean test environment. + Otherwise, `path` is simply returned untouched. + """ + if os.environ.get('TFTEST_COPY'): + # if the TFTEST_COPY is set, create temp dir and copy the root + # module there + with tempfile.TemporaryDirectory(dir=path.parent) as tmp_path: + tmp_path = Path(tmp_path) + + # if we're copying the module, we might as well ignore files and + # directories that are automatically read by terraform. Useful + # to avoid surprises if, for example, you have an active fast + # deployment with links to configs) + ignore_patterns = shutil.ignore_patterns('*.auto.tfvars', + '*.auto.tfvars.json', + 'terraform.tfstate*', + 'terraform.tfvars', '.terraform') + + shutil.copytree(path, tmp_path, dirs_exist_ok=True, + ignore=ignore_patterns) + + yield tmp_path + else: + # if TFTEST_COPY is not set, just return the same path + yield path + + +def plan_summary(module_path, basedir, tf_var_files=None, **tf_vars): + """ + Run a Terraform plan on the module located at `module_path`. + + - module_path: terraform root module to run. Can be an absolute + path or relative to the root of the repository + + - basedir: directory root to use for relative paths in + tf_var_files. + + - tf_var_files: set of terraform variable files (tfvars) to pass + in to terraform + + Returns a PlanSummary object containing 3 attributes: + - values: dictionary where the keys are terraform plan addresses + and values are the JSON representation (converted to python + types) of the attribute values of the resource. + + - counts: dictionary where the keys are the terraform resource + types and the values are the number of times that type appears + in the plan + + - outputs: dictionary of the modules outputs that can be + determined at plan type. + + Consult [1] for mode details on the structure of values and outputs + + [1] https://developer.hashicorp.com/terraform/internals/json-format + """ + # make the module_path relative to the root of the repo while still + # supporting absolute paths + module_path = Path(__file__).parents[1] / module_path + with _prepare_root_module(module_path) as test_path: + binary = os.environ.get('TERRAFORM', 'terraform') + tf = tftest.TerraformTest(test_path, binary=binary) + tf.setup(upgrade=True) + tf_var_files = [(basedir / x).resolve() for x in tf_var_files or []] + plan = tf.plan(output=True, tf_var_file=tf_var_files, tf_vars=tf_vars) + + # compute resource type counts and address->values map + values = {} + counts = collections.defaultdict(int) + q = collections.deque([plan.root_module]) + while q: + e = q.popleft() + + if 'type' in e: + counts[e['type']] += 1 + if 'values' in e: + values[e['address']] = e['values'] + + for x in e.get('resources', []): + q.append(x) + for x in e.get('child_modules', []): + q.append(x) + + # extract planned outputs + outputs = plan.get('planned_values', {}).get('outputs', {}) + + return PlanSummary(values, dict(counts), outputs) + + +@pytest.fixture(name='plan_summary') +def plan_summary_fixture(request): + """Return a function to generate a PlanSummary. + + In the returned function `basedir` becomes optional and it defaults + to the directory of the calling test + """ + + def inner(module_path, basedir=None, tf_var_files=None, **tf_vars): + if basedir is None: + basedir = Path(request.fspath).parent + return plan_summary(module_path=module_path, basedir=basedir, + tf_var_files=tf_var_files, **tf_vars) + + return inner + + +def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, + **tf_vars): + summary = plan_summary(module_path=module_path, tf_var_files=tf_var_files, + basedir=basedir, **tf_vars) + + # allow single single string for inventory_paths + if not isinstance(inventory_paths, list): + inventory_paths = [inventory_paths] + + for path in inventory_paths: + # allow tfvars and inventory to be relative to the caller + path = basedir / path + try: + inventory = yaml.safe_load(path.read_text()) + except (IOError, OSError, yaml.YAMLError) as e: + raise Exception(f'cannot read test inventory {path}: {e}') + + # don't fail if the inventory is empty + inventory = inventory or {} + + # If you add additional asserts to this function: + # - put the values coming from the plan on the left side of + # any comparison operators + # - put the values coming from user's inventory the right + # side of any comparison operators. + # - include a descriptive error message to the assert + + # for values: + # - verify each address in the user's inventory exists in the plan + # - for those address that exist on both the user's inventory and + # the plan output, ensure the set of keys on the inventory are a + # subset of the keys in the plan, and compare their values by + # equality + if 'values' in inventory: + expected_values = inventory['values'] + for address, expected_value in expected_values.items(): + assert address in summary.values, \ + f'{address} is not a valid address in the plan' + for k, v in expected_value.items(): + assert k in summary.values[address], \ + f'{k} not found at {address}' + plan_value = summary.values[address][k] + assert plan_value == v, \ + f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`' + + if 'counts' in inventory: + expected_counts = inventory['counts'] + for type_, expected_count in expected_counts.items(): + assert type_ in summary.counts, \ + f'module does not create any resources of type `{type_}`' + plan_count = summary.counts[type_] + assert plan_count == expected_count, \ + f'count of {type_} resources failed. Got {plan_count}, expected {expected_count}' + + if 'outputs' in inventory: + expected_outputs = inventory['outputs'] + for output_name, expected_output in expected_outputs.items(): + assert output_name in summary.outputs, \ + f'module does not output `{output_name}`' + output = summary.outputs[output_name] + # assert 'value' in output, \ + # f'output `{output_name}` does not have a value (is it sensitive or dynamic?)' + plan_output = output.get('value', '__missing__') + assert plan_output == expected_output, \ + f'output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`' + + return summary + + +@pytest.fixture(name='plan_validator') +def plan_validator_fixture(request): + """Return a function to build a PlanSummary and compare it to a YAML inventory. + + In the returned function `basedir` becomes optional and it defaults + to the directory of the calling test' + + """ + + def inner(module_path, inventory_paths, basedir=None, tf_var_files=None, + **tf_vars): + if basedir is None: + basedir = Path(request.fspath).parent + return plan_validator(module_path=module_path, + inventory_paths=inventory_paths, basedir=basedir, + tf_var_files=tf_var_paths, **tf_vars) + + return inner + + +# @pytest.fixture +# def repo_root(): +# 'Return a pathlib.Path to the root of the repository' +# return Path(__file__).parents[1] diff --git a/tests/legacy_fixtures.py b/tests/legacy_fixtures.py new file mode 100644 index 00000000..5891d704 --- /dev/null +++ b/tests/legacy_fixtures.py @@ -0,0 +1,153 @@ +# 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. +"""Legacy pytest fixtures. + +The fixtures contained in this file will eventually go away. Consider +using one of the fixtures in fixtures.py +""" + +import inspect +import os +import shutil +import tempfile + +import pytest +import tftest + +BASEDIR = os.path.dirname(os.path.dirname(__file__)) + + +@pytest.fixture(scope='session') +def _plan_runner(): + 'Return a function to run Terraform plan on a fixture.' + + def run_plan(fixture_path=None, extra_files=None, tf_var_file=None, + targets=None, refresh=True, tmpdir=True, **tf_vars): + 'Run Terraform plan and returns parsed output.' + if fixture_path is None: + # find out the fixture directory from the caller's directory + caller = inspect.stack()[2] + fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture') + + fixture_parent = os.path.dirname(fixture_path) + fixture_prefix = os.path.basename(fixture_path) + '_' + with tempfile.TemporaryDirectory(prefix=fixture_prefix, + dir=fixture_parent) as tmp_path: + # copy fixture to a temporary directory so we can execute + # multiple tests in parallel + if tmpdir: + 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(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 + + +@pytest.fixture(scope='session') +def plan_runner(_plan_runner): + 'Return a function to run Terraform plan on a module fixture.' + + def run_plan(fixture_path=None, extra_files=None, tf_var_file=None, + targets=None, **tf_vars): + 'Run Terraform plan and returns plan and module resources.' + 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'] + + return run_plan + + +@pytest.fixture(scope='session') +def e2e_plan_runner(_plan_runner): + 'Return a function to run Terraform plan on an end-to-end fixture.' + + def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True, + include_bare_resources=False, **tf_vars): + 'Run Terraform plan on an end-to-end module using defaults, returns data.' + plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets, + refresh=refresh, **tf_vars) + # skip the fixture + root_module = plan.root_module['child_modules'][0] + modules = dict((mod['address'], mod['resources']) + for mod in root_module['child_modules']) + resources = [r for m in modules.values() for r in m] + if include_bare_resources: + bare_resources = root_module['resources'] + resources.extend(bare_resources) + return modules, resources + + return run_plan + + +@pytest.fixture(scope='session') +def recursive_e2e_plan_runner(_plan_runner): + """ + Plan runner for end-to-end root module, returns total number of + (nested) modules and resources + """ + + def walk_plan(node, modules, resources): + new_modules = node.get('child_modules', []) + resources += node.get('resources', []) + modules += new_modules + for module in new_modules: + walk_plan(module, modules, resources) + + def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True, + include_bare_resources=False, compute_sums=True, tmpdir=True, + **tf_vars): + 'Run Terraform plan on a root module using defaults, returns data.' + plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets, + refresh=refresh, tmpdir=tmpdir, **tf_vars) + modules = [] + resources = [] + walk_plan(plan.root_module, modules, resources) + return len(modules), len(resources) + + return run_plan + + +@pytest.fixture(scope='session') +def apply_runner(): + 'Return a function to run Terraform apply on a fixture.' + + def run_apply(fixture_path=None, **tf_vars): + 'Run Terraform plan and returns parsed output.' + if fixture_path is None: + # find out the fixture directory from the caller's directory + caller = inspect.stack()[1] + fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture') + + fixture_parent = os.path.dirname(fixture_path) + fixture_prefix = os.path.basename(fixture_path) + '_' + + with tempfile.TemporaryDirectory(prefix=fixture_prefix, + dir=fixture_parent) as tmp_path: + # copy fixture to a temporary directory so we can execute + # multiple tests in parallel + shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True) + tf = tftest.TerraformTest(tmp_path, BASEDIR, + os.environ.get('TERRAFORM', 'terraform')) + tf.setup(upgrade=True) + apply = tf.apply(tf_vars=tf_vars) + output = tf.output(json_format=True) + return apply, output + + return run_apply diff --git a/tests/modules/apigee/fixture/test.env_only.tfvars b/tests/modules/apigee/fixture/test.env_only.tfvars index 5cc6f49d..0e4edc48 100644 --- a/tests/modules/apigee/fixture/test.env_only.tfvars +++ b/tests/modules/apigee/fixture/test.env_only.tfvars @@ -4,5 +4,9 @@ environments = { display_name = "APIs test" description = "APIs Test" envgroups = ["test"] + node_config = { + min_node_count = 2 + max_node_count = 5 + } } -} \ No newline at end of file +} diff --git a/tests/modules/apigee/fixture/variables.tf b/tests/modules/apigee/fixture/variables.tf index 8cddf9a4..266f0d34 100644 --- a/tests/modules/apigee/fixture/variables.tf +++ b/tests/modules/apigee/fixture/variables.tf @@ -35,9 +35,8 @@ variable "environments" { display_name = optional(string) description = optional(string, "Terraform-managed") node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - current_aggregate_node_count = number + min_node_count = optional(number) + max_node_count = optional(number) })) iam = optional(map(list(string))) envgroups = list(string) @@ -76,4 +75,4 @@ variable "organization" { variable "project_id" { description = "Project ID." type = string -} \ No newline at end of file +} diff --git a/tests/modules/conftest.py b/tests/modules/conftest.py index a34cef65..c199cff7 100644 --- a/tests/modules/conftest.py +++ b/tests/modules/conftest.py @@ -20,16 +20,11 @@ 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(): +@pytest.fixture() +def tfvars_to_yaml(request): def converter(source, dest, from_var, to_var=None): - p_fixture = pathlib.Path(inspect.stack()[1].filename).parent / 'fixture' + p_fixture = pathlib.Path(request.path).parent p_source = p_fixture / source if not p_source.exists(): raise ValueError(f"tfvars '{source}' not found") diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index d4e4559d..8463761e 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -31,13 +31,14 @@ def test_policy_list(plan_runner): 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') + tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest, + 'org_policies') _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') validate_policy_boolean(resources) 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') + tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies') _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') validate_policy_list(resources) diff --git a/tests/modules/net_vpc/common.tfvars b/tests/modules/net_vpc/common.tfvars new file mode 100644 index 00000000..8d9d8a95 --- /dev/null +++ b/tests/modules/net_vpc/common.tfvars @@ -0,0 +1,2 @@ +project_id = "test-project" +name = "test" diff --git a/tests/modules/net_vpc/fixture/data/factory-subnet.yaml b/tests/modules/net_vpc/data/factory-subnet.yaml similarity index 100% rename from tests/modules/net_vpc/fixture/data/factory-subnet.yaml rename to tests/modules/net_vpc/data/factory-subnet.yaml diff --git a/tests/modules/net_vpc/fixture/data/factory-subnet2.yaml b/tests/modules/net_vpc/data/factory-subnet2.yaml similarity index 100% rename from tests/modules/net_vpc/fixture/data/factory-subnet2.yaml rename to tests/modules/net_vpc/data/factory-subnet2.yaml diff --git a/tests/modules/net_vpc/factory.tfvars b/tests/modules/net_vpc/factory.tfvars new file mode 100644 index 00000000..8c4d4a28 --- /dev/null +++ b/tests/modules/net_vpc/factory.tfvars @@ -0,0 +1 @@ +data_folder = "../../tests/modules/net_vpc/data" diff --git a/tests/modules/net_vpc/factory.yaml b/tests/modules/net_vpc/factory.yaml new file mode 100644 index 00000000..9cf628d0 --- /dev/null +++ b/tests/modules/net_vpc/factory.yaml @@ -0,0 +1,44 @@ +# 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. + +values: + google_compute_subnetwork.subnetwork["europe-west1/factory-subnet"]: + description: 'Sample description' + ip_cidr_range: '10.128.0.0/24' + ipv6_access_type: null + log_config: [] + name: 'factory-subnet' + private_ip_google_access: false + project: 'test-project' + region: 'europe-west1' + role: null + secondary_ip_range: + - ip_cidr_range: '192.168.128.0/24' + range_name: 'secondary-range-a' + google_compute_subnetwork.subnetwork["europe-west4/factory-subnet2"]: + description: 'Sample description' + ip_cidr_range: '10.129.0.0/24' + log_config: [] + name: 'factory-subnet2' + private_ip_google_access: true + project: 'test-project' + region: 'europe-west4' + role: null + secondary_ip_range: [] + + # FIXME: should we have some bindings here? + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/peering.tfvars b/tests/modules/net_vpc/peering.tfvars new file mode 100644 index 00000000..eccd7ae7 --- /dev/null +++ b/tests/modules/net_vpc/peering.tfvars @@ -0,0 +1,5 @@ +peering_config = { + peer_vpc_self_link = "projects/my-project/global/networks/peer" + export_routes = true + import_routes = null +} diff --git a/tests/modules/net_vpc/peering.yaml b/tests/modules/net_vpc/peering.yaml new file mode 100644 index 00000000..8d0bbed7 --- /dev/null +++ b/tests/modules/net_vpc/peering.yaml @@ -0,0 +1,47 @@ +# 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. + +values: + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: test + project: test-project + routing_mode: GLOBAL + google_compute_network_peering.local[0]: + export_custom_routes: true + import_custom_routes: false + name: test-peer + peer_network: projects/my-project/global/networks/peer + google_compute_network_peering.remote[0]: + export_custom_routes: false + import_custom_routes: true + name: peer-test + network: projects/my-project/global/networks/peer + +counts: + google_compute_network: 1 + google_compute_network_peering: 2 + +outputs: + bindings: {} + project_id: test-project + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/psa_routes_export.tfvars b/tests/modules/net_vpc/psa_routes_export.tfvars new file mode 100644 index 00000000..9fbe4ddf --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_export.tfvars @@ -0,0 +1,7 @@ +psa_config = { + ranges = { + bar = "172.16.100.0/24" + } + export_routes = true + import_routes = false +} diff --git a/tests/modules/net_vpc/psa_routes_export.yaml b/tests/modules/net_vpc/psa_routes_export.yaml new file mode 100644 index 00000000..c8cf631b --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_export.yaml @@ -0,0 +1,60 @@ +# 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. + +values: + google_compute_global_address.psa_ranges["bar"]: + address: 172.16.100.0 + address_type: INTERNAL + description: null + ip_version: null + name: bar + prefix_length: 24 + project: test-project + purpose: VPC_PEERING + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: test + project: test-project + routing_mode: GLOBAL + google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: true + import_custom_routes: false + project: test-project + google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - bar + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_service_networking_connection: 1 + +outputs: + bindings: {} + name: __missing__ + network: __missing__ + project_id: test-project + self_link: __missing__ + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/psa_routes_import.tfvars b/tests/modules/net_vpc/psa_routes_import.tfvars new file mode 100644 index 00000000..beeaf433 --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_import.tfvars @@ -0,0 +1,7 @@ +psa_config = { + ranges = { + bar = "172.16.100.0/24" + } + export_routes = false + import_routes = true +} diff --git a/tests/modules/net_vpc/psa_routes_import.yaml b/tests/modules/net_vpc/psa_routes_import.yaml new file mode 100644 index 00000000..771d881f --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_import.yaml @@ -0,0 +1,60 @@ +# 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. + +values: + google_compute_global_address.psa_ranges["bar"]: + address: 172.16.100.0 + address_type: INTERNAL + description: null + ip_version: null + name: bar + prefix_length: 24 + project: test-project + purpose: VPC_PEERING + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: test + project: test-project + routing_mode: GLOBAL + google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: false + import_custom_routes: true + project: test-project + google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - bar + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_service_networking_connection: 1 + +outputs: + bindings: {} + name: __missing__ + network: __missing__ + project_id: test-project + self_link: __missing__ + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/psa_routes_import_export.tfvars b/tests/modules/net_vpc/psa_routes_import_export.tfvars new file mode 100644 index 00000000..20592231 --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_import_export.tfvars @@ -0,0 +1,7 @@ +psa_config = { + ranges = { + bar = "172.16.100.0/24" + } + export_routes = true + import_routes = true +} diff --git a/tests/modules/net_vpc/psa_routes_import_export.yaml b/tests/modules/net_vpc/psa_routes_import_export.yaml new file mode 100644 index 00000000..561fa50a --- /dev/null +++ b/tests/modules/net_vpc/psa_routes_import_export.yaml @@ -0,0 +1,60 @@ +# 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. + +values: + google_compute_global_address.psa_ranges["bar"]: + address: 172.16.100.0 + address_type: INTERNAL + description: null + ip_version: null + name: bar + prefix_length: 24 + project: test-project + purpose: VPC_PEERING + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: test + project: test-project + routing_mode: GLOBAL + google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: true + import_custom_routes: true + project: test-project + google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - bar + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_service_networking_connection: 1 + +outputs: + bindings: {} + name: __missing__ + network: __missing__ + project_id: test-project + self_link: __missing__ + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/psa_simple.tfvars b/tests/modules/net_vpc/psa_simple.tfvars new file mode 100644 index 00000000..51289fe0 --- /dev/null +++ b/tests/modules/net_vpc/psa_simple.tfvars @@ -0,0 +1,7 @@ +psa_config = { + ranges = { + bar = "172.16.100.0/24" + foo = "172.16.101.0/24" + } + routes = null +} diff --git a/tests/modules/net_vpc/psa_simple.yaml b/tests/modules/net_vpc/psa_simple.yaml new file mode 100644 index 00000000..019b443f --- /dev/null +++ b/tests/modules/net_vpc/psa_simple.yaml @@ -0,0 +1,70 @@ +# 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. + +values: + google_compute_global_address.psa_ranges["bar"]: + address: 172.16.100.0 + address_type: INTERNAL + description: null + ip_version: null + name: bar + prefix_length: 24 + project: test-project + purpose: VPC_PEERING + google_compute_global_address.psa_ranges["foo"]: + address: 172.16.101.0 + address_type: INTERNAL + description: null + ip_version: null + name: foo + prefix_length: 24 + project: test-project + purpose: VPC_PEERING + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: test + project: test-project + routing_mode: GLOBAL + google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: false + import_custom_routes: false + project: test-project + google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - bar + - foo + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 2 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_service_networking_connection: 1 + +outputs: + bindings: {} + name: __missing__ + network: __missing__ + project_id: test-project + self_link: __missing__ + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/shared_vpc.tfvars b/tests/modules/net_vpc/shared_vpc.tfvars new file mode 100644 index 00000000..3171539a --- /dev/null +++ b/tests/modules/net_vpc/shared_vpc.tfvars @@ -0,0 +1,2 @@ +shared_vpc_host = true +shared_vpc_service_projects = ["tf-a", "tf-b"] diff --git a/tests/modules/net_vpc/shared_vpc.yaml b/tests/modules/net_vpc/shared_vpc.yaml new file mode 100644 index 00000000..0837dbc4 --- /dev/null +++ b/tests/modules/net_vpc/shared_vpc.yaml @@ -0,0 +1,46 @@ +# 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. + +values: + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: test + project: test-project + routing_mode: GLOBAL + google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: test-project + google_compute_shared_vpc_service_project.service_projects["tf-a"]: + host_project: test-project + service_project: tf-a + google_compute_shared_vpc_service_project.service_projects["tf-b"]: + host_project: test-project + service_project: tf-b + +counts: + google_compute_network: 1 + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 2 + +outputs: + bindings: {} + project_id: test-project + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/simple.tfvars b/tests/modules/net_vpc/simple.tfvars new file mode 100644 index 00000000..6f848aa9 --- /dev/null +++ b/tests/modules/net_vpc/simple.tfvars @@ -0,0 +1 @@ +# skip boilerplate check diff --git a/tests/modules/net_vpc/simple.yaml b/tests/modules/net_vpc/simple.yaml new file mode 100644 index 00000000..004be7ec --- /dev/null +++ b/tests/modules/net_vpc/simple.yaml @@ -0,0 +1,36 @@ +# 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. + +values: + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: test + project: test-project + routing_mode: GLOBAL + +counts: + google_compute_network: 1 + +outputs: + bindings: {} + project_id: test-project + subnet_ips: {} + subnet_regions: {} + subnet_secondary_ranges: {} + subnet_self_links: {} + subnets: {} + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/subnets.tfvars b/tests/modules/net_vpc/subnets.tfvars new file mode 100644 index 00000000..499e498f --- /dev/null +++ b/tests/modules/net_vpc/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/subnets.yaml b/tests/modules/net_vpc/subnets.yaml new file mode 100644 index 00000000..9ccf31e6 --- /dev/null +++ b/tests/modules/net_vpc/subnets.yaml @@ -0,0 +1,120 @@ +# 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. + +values: + google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: test + project: test-project + routing_mode: GLOBAL + google_compute_subnetwork.subnetwork["europe-west1/a"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + log_config: [] + name: a + private_ip_google_access: true + project: test-project + region: europe-west1 + role: null + secondary_ip_range: [] + google_compute_subnetwork.subnetwork["europe-west1/b"]: + description: Subnet b + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: b + private_ip_google_access: false + project: test-project + region: europe-west1 + role: null + secondary_ip_range: [] + google_compute_subnetwork.subnetwork["europe-west1/c"]: + description: Terraform-managed. + ip_cidr_range: 10.0.2.0/24 + ipv6_access_type: null + log_config: [] + name: c + private_ip_google_access: true + project: test-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 192.168.0.0/24 + range_name: a + - ip_cidr_range: 192.168.1.0/24 + range_name: b + google_compute_subnetwork.subnetwork["europe-west1/d"]: + description: Terraform-managed. + ip_cidr_range: 10.0.3.0/24 + log_config: + - aggregation_interval: INTERVAL_10_MIN + filter_expr: 'true' + flow_sampling: 0.5 + metadata: INCLUDE_ALL_METADATA + metadata_fields: null + name: d + private_ip_google_access: true + project: test-project + region: europe-west1 + role: null + secondary_ip_range: [] + google_compute_subnetwork_iam_binding.binding["europe-west1/a.roles/compute.networkUser"]: + condition: [] + members: + - group:g-a@example.com + - user:a@example.com + project: test-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: a + google_compute_subnetwork_iam_binding.binding["europe-west1/c.roles/compute.networkUser"]: + condition: [] + members: + - group:g-c@example.com + - user:c@example.com + project: test-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: c + +counts: + google_compute_network: 1 + google_compute_subnetwork: 4 + google_compute_subnetwork_iam_binding: 2 + +outputs: + bindings: __missing__ + project_id: test-project + subnet_ips: + europe-west1/a: 10.0.0.0/24 + europe-west1/b: 10.0.1.0/24 + europe-west1/c: 10.0.2.0/24 + europe-west1/d: 10.0.3.0/24 + subnet_regions: + europe-west1/a: europe-west1 + europe-west1/b: europe-west1 + europe-west1/c: europe-west1 + europe-west1/d: europe-west1 + subnet_secondary_ranges: + europe-west1/a: {} + europe-west1/b: {} + europe-west1/c: + a: 192.168.0.0/24 + b: 192.168.1.0/24 + europe-west1/d: {} + subnet_self_links: __missing__ + subnets: __missing__ + subnets_proxy_only: {} + subnets_psc: {} diff --git a/tests/modules/net_vpc/test_plan.py b/tests/modules/net_vpc/test_plan.py deleted file mode 100644 index 2f49f801..00000000 --- a/tests/modules/net_vpc/test_plan.py +++ /dev/null @@ -1,81 +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. - -_VAR_PEER_VPC_CONFIG = '''{ - peer_vpc_self_link="projects/my-project/global/networks/peer", - export_routes=true, import_routes=null -}''' -_VAR_ROUTES_TEMPLATE = '''{ - next-hop = { - dest_range="192.168.128.0/24", tags=null, - next_hop_type="%s", next_hop="%s"}, - gateway = { - dest_range="0.0.0.0/0", priority=100, tags=["tag-a"], - next_hop_type="gateway", - next_hop="global/gateways/default-internet-gateway"} -}''' -_VAR_ROUTES_NEXT_HOPS = { - 'gateway': 'global/gateways/default-internet-gateway', - 'instance': 'zones/europe-west1-b/test', - 'ip': '192.168.0.128', - 'ilb': 'regions/europe-west1/forwardingRules/test', - 'vpn_tunnel': 'regions/europe-west1/vpnTunnels/foo' -} - - -def test_vpc_simple(plan_runner): - "Test vpc with no extra options." - _, resources = plan_runner() - assert len(resources) == 1 - assert [r['type'] for r in resources] == ['google_compute_network'] - assert [r['values']['name'] for r in resources] == ['test'] - assert [r['values']['project'] for r in resources] == ['test-project'] - - -def test_vpc_shared(plan_runner): - "Test shared vpc variables." - _, resources = plan_runner(shared_vpc_host='true', - shared_vpc_service_projects='["tf-a", "tf-b"]') - assert len(resources) == 4 - assert set(r['type'] for r in resources) == set([ - 'google_compute_network', 'google_compute_shared_vpc_host_project', - 'google_compute_shared_vpc_service_project' - ]) - - -def test_vpc_peering(plan_runner): - "Test vpc peering variables." - _, resources = plan_runner(peering_config=_VAR_PEER_VPC_CONFIG) - assert len(resources) == 3 - assert set(r['type'] for r in resources) == set( - ['google_compute_network', 'google_compute_network_peering']) - peerings = [ - r['values'] - for r in resources - if r['type'] == 'google_compute_network_peering' - ] - assert [p['name'] for p in peerings] == ['test-peer', 'peer-test'] - assert [p['export_custom_routes'] for p in peerings] == [True, False] - assert [p['import_custom_routes'] for p in peerings] == [False, True] - - -def test_vpc_routes(plan_runner): - "Test vpc routes." - for next_hop_type, next_hop in _VAR_ROUTES_NEXT_HOPS.items(): - _var_routes = _VAR_ROUTES_TEMPLATE % (next_hop_type, next_hop) - _, resources = plan_runner(routes=_var_routes) - assert len(resources) == 3 - resource = [r for r in resources if r['values']['name'] == 'test-next-hop' - ][0] - assert resource['values']['next_hop_%s' % next_hop_type] diff --git a/tests/modules/net_vpc/test_plan_psa.py b/tests/modules/net_vpc/test_plan_psa.py deleted file mode 100644 index 359977d6..00000000 --- a/tests/modules/net_vpc/test_plan_psa.py +++ /dev/null @@ -1,79 +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_single_range(plan_runner): - "Test single PSA range." - psa_config = '''{ - ranges = { - bar = "172.16.100.0/24" - foo = "172.16.101.0/24" - }, - routes = null - }''' - _, resources = plan_runner(psa_config=psa_config) - assert len(resources) == 5 - for r in resources: - if r['type'] == 'google_compute_network_peering_routes_config': - assert not r['values']['export_custom_routes'] - assert not r['values']['import_custom_routes'] - - -def test_routes_export(plan_runner): - "Test routes export." - psa_config = '''{ - ranges = { - bar = "172.16.100.0/24" - } - export_routes = true - import_routes = false - }''' - _, resources = plan_runner(psa_config=psa_config) - assert len(resources) == 4 - for r in resources: - if r['type'] == 'google_compute_network_peering_routes_config': - assert r['values']['export_custom_routes'] - assert not r['values']['import_custom_routes'] - - -def test_routes_import(plan_runner): - "Test routes import." - psa_config = '''{ - ranges = { - bar = "172.16.100.0/24" - }, - export_routes = false - import_routes = true - }''' - _, resources = plan_runner(psa_config=psa_config) - for r in resources: - if r['type'] == 'google_compute_network_peering_routes_config': - assert not r['values']['export_custom_routes'] - assert r['values']['import_custom_routes'] - - -def test_routes_export_import(plan_runner): - "Test routes export and import." - psa_config = '''{ - ranges = { - bar = "172.16.100.0/24" - }, - export_routes = true - import_routes = true - }''' - _, resources = plan_runner(psa_config=psa_config) - for r in resources: - if r['type'] == 'google_compute_network_peering_routes_config': - assert r['values']['export_custom_routes'] - assert r['values']['import_custom_routes'] diff --git a/tests/modules/net_vpc/test_plan_subnets.py b/tests/modules/net_vpc/test_plan_subnets.py deleted file mode 100644 index affea44c..00000000 --- a/tests/modules/net_vpc/test_plan_subnets.py +++ /dev/null @@ -1,71 +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. - -DATA_FOLDER = "data" - - -def test_subnet_factory(plan_runner): - "Test subnet factory." - _, resources = plan_runner(data_folder=DATA_FOLDER) - assert len(resources) == 3 - subnets = [ - r['values'] for r in resources if r['type'] == 'google_compute_subnetwork' - ] - assert {s['name'] for s in subnets} == {'factory-subnet', 'factory-subnet2'} - assert {len(s['secondary_ip_range']) for s in subnets} == {0, 1} - assert {s['private_ip_google_access'] for s in subnets} == {True, False} - - -def test_subnets(plan_runner): - "Test subnets variable." - _, 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' - ] - assert {s['name'] for s in subnets} == {'a', 'b', 'c', 'd'} - assert {len(s['secondary_ip_range']) for s in subnets} == {0, 0, 2, 0} - log_config = {s['name']: s['log_config'] for s in subnets if s['log_config']} - assert log_config == { - 'd': [{ - 'aggregation_interval': 'INTERVAL_10_MIN', - 'filter_expr': 'true', - 'flow_sampling': 0.5, - 'metadata': 'INCLUDE_ALL_METADATA', - 'metadata_fields': None - }] - } - bindings = { - r['index']: r['values'] - for r in resources - if r['type'] == 'google_compute_subnetwork_iam_binding' - } - assert bindings == { - 'europe-west1/a.roles/compute.networkUser': { - 'condition': [], - 'members': ['group:g-a@example.com', 'user:a@example.com'], - 'project': 'test-project', - 'region': 'europe-west1', - 'role': 'roles/compute.networkUser', - 'subnetwork': 'a' - }, - 'europe-west1/c.roles/compute.networkUser': { - 'condition': [], - 'members': ['group:g-c@example.com', 'user:c@example.com'], - 'project': 'test-project', - 'region': 'europe-west1', - 'role': 'roles/compute.networkUser', - 'subnetwork': 'c' - }, - } diff --git a/tests/modules/net_vpc/test_routes.py b/tests/modules/net_vpc/test_routes.py new file mode 100644 index 00000000..01d9673d --- /dev/null +++ b/tests/modules/net_vpc/test_routes.py @@ -0,0 +1,47 @@ +# 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 + +_route_parameters = [('gateway', 'global/gateways/default-internet-gateway'), + ('instance', 'zones/europe-west1-b/test'), + ('ip', '192.168.0.128'), + ('ilb', 'regions/europe-west1/forwardingRules/test'), + ('vpn_tunnel', 'regions/europe-west1/vpnTunnels/foo')] + + +@pytest.mark.parametrize('next_hop_type,next_hop', _route_parameters) +def test_vpc_routes(plan_summary, next_hop_type, next_hop): + 'Test vpc routes.' + + var_routes = '''{ + next-hop = { + dest_range = "192.168.128.0/24" + tags = null + next_hop_type = "%s" + next_hop = "%s" + } + gateway = { + dest_range = "0.0.0.0/0", + priority = 100 + tags = ["tag-a"] + next_hop_type = "gateway", + next_hop = "global/gateways/default-internet-gateway" + } + }''' % (next_hop_type, next_hop) + summary = plan_summary('modules/net-vpc', tf_var_files=['common.tfvars'], + routes=var_routes) + assert len(summary.values) == 3 + route = summary.values[f'google_compute_route.{next_hop_type}["next-hop"]'] + assert route[f'next_hop_{next_hop_type}'] == next_hop diff --git a/tests/blueprints/conftest.py b/tests/modules/net_vpc/tftest.yaml similarity index 73% rename from tests/blueprints/conftest.py rename to tests/modules/net_vpc/tftest.yaml index ed29d5bb..b2b09798 100644 --- a/tests/blueprints/conftest.py +++ b/tests/modules/net_vpc/tftest.yaml @@ -12,10 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest +module: modules/net-vpc +common_tfvars: + - common.tfvars - -def pytest_collection_modifyitems(config, items): - for item in items: - item.add_marker( - pytest.mark.xdist_group(name='/'.join(item.path.parent.parts[-2:]))) +tests: + simple: + subnets: + peering: + shared_vpc: + factory: + psa_simple: + psa_routes_export: + psa_routes_import: + psa_routes_import_export: diff --git a/tests/modules/organization/audit_config.tfvars b/tests/modules/organization/audit_config.tfvars new file mode 100644 index 00000000..b071033a --- /dev/null +++ b/tests/modules/organization/audit_config.tfvars @@ -0,0 +1,6 @@ +iam_audit_config = { + allServices = { + DATA_READ = [], + DATA_WRITE = ["user:me@example.org"] + } +} diff --git a/tests/fast/conftest.py b/tests/modules/organization/audit_config.yaml similarity index 78% rename from tests/fast/conftest.py rename to tests/modules/organization/audit_config.yaml index 3d559e6f..56e90b62 100644 --- a/tests/fast/conftest.py +++ b/tests/modules/organization/audit_config.yaml @@ -12,9 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest - - -def pytest_collection_modifyitems(config, items): - for item in items: - item.add_marker(pytest.mark.xdist_group(name=item.path.parent.name)) +counts: + google_organization_iam_audit_config: 1 diff --git a/tests/modules/organization/common.tfvars b/tests/modules/organization/common.tfvars new file mode 100644 index 00000000..d5f1c3d7 --- /dev/null +++ b/tests/modules/organization/common.tfvars @@ -0,0 +1 @@ +organization_id = "organizations/1234567890" diff --git a/tests/modules/organization/fixture/data/firewall-cidrs.yaml b/tests/modules/organization/data/firewall-cidrs.yaml similarity index 100% rename from tests/modules/organization/fixture/data/firewall-cidrs.yaml rename to tests/modules/organization/data/firewall-cidrs.yaml diff --git a/tests/modules/organization/fixture/data/firewall-rules.yaml b/tests/modules/organization/data/firewall-rules.yaml similarity index 100% rename from tests/modules/organization/fixture/data/firewall-rules.yaml rename to tests/modules/organization/data/firewall-rules.yaml diff --git a/tests/modules/organization/firewall_policies.tfvars b/tests/modules/organization/firewall_policies.tfvars new file mode 100644 index 00000000..603cd3a4 --- /dev/null +++ b/tests/modules/organization/firewall_policies.tfvars @@ -0,0 +1,45 @@ +firewall_policies = { + policy1 = { + allow-ingress = { + description = "" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["10.0.0.0/8"] + ports = { + tcp = ["22"] + } + target_service_accounts = null + target_resources = null + logging = false + } + deny-egress = { + description = "" + direction = "EGRESS" + action = "deny" + priority = 200 + ranges = ["192.168.0.0/24"] + ports = { + tcp = ["443"] + } + target_service_accounts = null + target_resources = null + logging = false + } + } + policy2 = { + allow-ingress = { + description = "" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["10.0.0.0/8"] + ports = { + tcp = ["22"] + } + target_service_accounts = null + target_resources = null + logging = false + } + } +} diff --git a/tests/modules/organization/firewall_policies.yaml b/tests/modules/organization/firewall_policies.yaml new file mode 100644 index 00000000..4ecc5c72 --- /dev/null +++ b/tests/modules/organization/firewall_policies.yaml @@ -0,0 +1,73 @@ +# 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. + +values: + google_compute_firewall_policy.policy["policy1"]: + parent: organizations/1234567890 + short_name: policy1 + google_compute_firewall_policy.policy["policy2"]: + parent: organizations/1234567890 + short_name: policy2 + google_compute_firewall_policy_rule.rule["policy1-allow-ingress"]: + action: allow + direction: INGRESS + disabled: null + enable_logging: false + match: + - dest_ip_ranges: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_ip_ranges: + - 10.0.0.0/8 + priority: 100 + target_resources: null + target_service_accounts: null + google_compute_firewall_policy_rule.rule["policy1-deny-egress"]: + action: deny + direction: EGRESS + disabled: null + enable_logging: false + match: + - dest_ip_ranges: + - 192.168.0.0/24 + layer4_configs: + - ip_protocol: tcp + ports: + - '443' + src_ip_ranges: null + priority: 200 + target_resources: null + target_service_accounts: null + google_compute_firewall_policy_rule.rule["policy2-allow-ingress"]: + action: allow + direction: INGRESS + disabled: null + enable_logging: false + match: + - dest_ip_ranges: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_ip_ranges: + - 10.0.0.0/8 + priority: 100 + target_resources: null + target_service_accounts: null + +counts: + google_compute_firewall_policy: 2 + google_compute_firewall_policy_rule: 3 diff --git a/tests/modules/organization/firewall_policies_factory.tfvars b/tests/modules/organization/firewall_policies_factory.tfvars new file mode 100644 index 00000000..3e1cf181 --- /dev/null +++ b/tests/modules/organization/firewall_policies_factory.tfvars @@ -0,0 +1,5 @@ +firewall_policy_factory = { + cidr_file = "../../tests/modules/organization/data/firewall-cidrs.yaml" + policy_name = "factory-1" + rules_file = "../../tests/modules/organization/data/firewall-rules.yaml" +} diff --git a/tests/modules/organization/firewall_policies_factory.yaml b/tests/modules/organization/firewall_policies_factory.yaml new file mode 100644 index 00000000..85e565fd --- /dev/null +++ b/tests/modules/organization/firewall_policies_factory.yaml @@ -0,0 +1,61 @@ +# 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. + +values: + google_compute_firewall_policy.policy["factory-1"]: + description: null + parent: organizations/1234567890 + short_name: factory-1 + timeouts: null + google_compute_firewall_policy_rule.rule["factory-1-allow-admins"]: + action: allow + description: Access from the admin subnet to all subnets + direction: INGRESS + disabled: null + enable_logging: null + match: + - dest_ip_ranges: null + layer4_configs: + - ip_protocol: all + ports: [] + src_ip_ranges: + - 10.0.0.0/8 + - 172.168.0.0/12 + - 192.168.0.0/16 + priority: 1000 + target_resources: null + target_service_accounts: null + timeouts: null + google_compute_firewall_policy_rule.rule["factory-1-allow-ssh-from-iap"]: + action: allow + description: Enable SSH from IAP + direction: INGRESS + disabled: null + enable_logging: null + match: + - dest_ip_ranges: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_ip_ranges: + - 35.235.240.0/20 + priority: 1002 + target_resources: null + target_service_accounts: null + timeouts: null + +counts: + google_compute_firewall_policy: 1 + google_compute_firewall_policy_rule: 2 diff --git a/tests/modules/organization/firewall_policies_factory_combined.tfvars b/tests/modules/organization/firewall_policies_factory_combined.tfvars new file mode 100644 index 00000000..6f848aa9 --- /dev/null +++ b/tests/modules/organization/firewall_policies_factory_combined.tfvars @@ -0,0 +1 @@ +# skip boilerplate check diff --git a/tests/modules/organization/firewall_policies_factory_combined.yaml b/tests/modules/organization/firewall_policies_factory_combined.yaml new file mode 100644 index 00000000..3b5cf6cc --- /dev/null +++ b/tests/modules/organization/firewall_policies_factory_combined.yaml @@ -0,0 +1,27 @@ +# 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. + +values: + google_compute_firewall_policy.policy["factory-1"]: {} + google_compute_firewall_policy.policy["policy1"]: {} + google_compute_firewall_policy.policy["policy2"]: {} + google_compute_firewall_policy_rule.rule["factory-1-allow-admins"]: {} + google_compute_firewall_policy_rule.rule["factory-1-allow-ssh-from-iap"]: {} + google_compute_firewall_policy_rule.rule["policy1-allow-ingress"]: {} + google_compute_firewall_policy_rule.rule["policy1-deny-egress"]: {} + google_compute_firewall_policy_rule.rule["policy2-allow-ingress"]: {} + +counts: + google_compute_firewall_policy: 3 + google_compute_firewall_policy_rule: 5 diff --git a/tests/modules/organization/fixture/test.network_tags.tfvars b/tests/modules/organization/fixture/test.network_tags.tfvars deleted file mode 100644 index 6c5c3ccd..00000000 --- a/tests/modules/organization/fixture/test.network_tags.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -network_tags = { - net_environment = { - network = "foobar" - } -} diff --git a/tests/modules/organization/iam.tfvars b/tests/modules/organization/iam.tfvars new file mode 100644 index 00000000..69963127 --- /dev/null +++ b/tests/modules/organization/iam.tfvars @@ -0,0 +1,18 @@ +group_iam = { + "owners@example.org" = [ + "roles/owner", + "roles/resourcemanager.folderAdmin" + ], + "viewers@example.org" = [ + "roles/viewer" + ] +} +iam = { + "roles/owner" = [ + "user:one@example.org", + "user:two@example.org" + ], + "roles/browser" = [ + "domain:example.org" + ] +} diff --git a/tests/modules/organization/iam.yaml b/tests/modules/organization/iam.yaml new file mode 100644 index 00000000..7b1a8cb9 --- /dev/null +++ b/tests/modules/organization/iam.yaml @@ -0,0 +1,44 @@ +# 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. + +values: + google_organization_iam_binding.authoritative["roles/browser"]: + condition: [] + members: + - domain:example.org + org_id: '1234567890' + role: roles/browser + google_organization_iam_binding.authoritative["roles/owner"]: + condition: [] + members: + - group:owners@example.org + - user:one@example.org + - user:two@example.org + org_id: '1234567890' + role: roles/owner + google_organization_iam_binding.authoritative["roles/resourcemanager.folderAdmin"]: + condition: [] + members: + - group:owners@example.org + org_id: '1234567890' + role: roles/resourcemanager.folderAdmin + google_organization_iam_binding.authoritative["roles/viewer"]: + condition: [] + members: + - group:viewers@example.org + org_id: '1234567890' + role: roles/viewer + +counts: + google_organization_iam_binding: 4 diff --git a/tests/modules/organization/iam_additive.tfvars b/tests/modules/organization/iam_additive.tfvars new file mode 100644 index 00000000..823d70ad --- /dev/null +++ b/tests/modules/organization/iam_additive.tfvars @@ -0,0 +1,4 @@ +iam = { + "user:one@example.org" = ["roles/owner"], + "user:two@example.org" = ["roles/owner", "roles/editor"] +} diff --git a/tests/modules/organization/iam_additive.yaml b/tests/modules/organization/iam_additive.yaml new file mode 100644 index 00000000..68eda8c2 --- /dev/null +++ b/tests/modules/organization/iam_additive.yaml @@ -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. + +values: + google_organization_iam_binding.authoritative["user:one@example.org"]: + condition: [] + members: + - roles/owner + org_id: '1234567890' + role: user:one@example.org + google_organization_iam_binding.authoritative["user:two@example.org"]: + condition: [] + members: + - roles/editor + - roles/owner + org_id: '1234567890' + role: user:two@example.org + +counts: + google_organization_iam_binding: 2 diff --git a/tests/modules/organization/logging.tfvars b/tests/modules/organization/logging.tfvars new file mode 100644 index 00000000..95a272e1 --- /dev/null +++ b/tests/modules/organization/logging.tfvars @@ -0,0 +1,29 @@ +logging_sinks = { + warning = { + destination = "mybucket" + type = "storage" + filter = "severity=WARNING" + } + info = { + destination = "projects/myproject/datasets/mydataset" + type = "bigquery" + filter = "severity=INFO" + disabled = true + } + notice = { + destination = "projects/myproject/topics/mytopic" + type = "pubsub" + filter = "severity=NOTICE" + include_children = false + } + debug = { + destination = "projects/myproject/locations/global/buckets/mybucket" + type = "logging" + filter = "severity=DEBUG" + include_children = false + exclusions = { + no-compute = "logName:compute" + no-container = "logName:container" + } + } +} diff --git a/tests/modules/organization/logging.yaml b/tests/modules/organization/logging.yaml new file mode 100644 index 00000000..8038c9ab --- /dev/null +++ b/tests/modules/organization/logging.yaml @@ -0,0 +1,86 @@ +# 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. + +values: + google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: + condition: [] + dataset_id: mydataset + project: myproject + role: roles/bigquery.dataEditor + google_logging_organization_sink.sink["debug"]: + description: debug (Terraform-managed). + destination: logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket + disabled: false + exclusions: + - description: null + disabled: false + filter: logName:compute + name: no-compute + - description: null + disabled: false + filter: logName:container + name: no-container + filter: severity=DEBUG + include_children: false + name: debug + org_id: '1234567890' + google_logging_organization_sink.sink["info"]: + description: info (Terraform-managed). + destination: bigquery.googleapis.com/projects/myproject/datasets/mydataset + disabled: true + exclusions: [] + filter: severity=INFO + include_children: true + name: info + org_id: '1234567890' + google_logging_organization_sink.sink["notice"]: + description: notice (Terraform-managed). + destination: pubsub.googleapis.com/projects/myproject/topics/mytopic + disabled: false + exclusions: [] + filter: severity=NOTICE + include_children: false + name: notice + org_id: '1234567890' + google_logging_organization_sink.sink["warning"]: + description: warning (Terraform-managed). + destination: storage.googleapis.com/mybucket + disabled: false + exclusions: [] + filter: severity=WARNING + include_children: true + name: warning + org_id: '1234567890' + google_project_iam_member.bucket-sinks-binding["debug"]: + condition: + - expression: resource.name.endsWith('projects/myproject/locations/global/buckets/mybucket') + title: debug bucket writer + project: myproject + role: roles/logging.bucketWriter + google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: + condition: [] + project: myproject + role: roles/pubsub.publisher + topic: mytopic + google_storage_bucket_iam_member.storage-sinks-binding["warning"]: + bucket: mybucket + condition: [] + role: roles/storage.objectCreator + +counts: + google_bigquery_dataset_iam_member: 1 + google_logging_organization_sink: 4 + google_project_iam_member: 1 + google_pubsub_topic_iam_member: 1 + google_storage_bucket_iam_member: 1 diff --git a/tests/modules/organization/logging_exclusions.tfvars b/tests/modules/organization/logging_exclusions.tfvars new file mode 100644 index 00000000..75c35604 --- /dev/null +++ b/tests/modules/organization/logging_exclusions.tfvars @@ -0,0 +1,4 @@ +logging_exclusions = { + exclusion1 = "resource.type=gce_instance" + exclusion2 = "severity=NOTICE" +} diff --git a/tests/modules/organization/logging_exclusions.yaml b/tests/modules/organization/logging_exclusions.yaml new file mode 100644 index 00000000..4d51dd7c --- /dev/null +++ b/tests/modules/organization/logging_exclusions.yaml @@ -0,0 +1,30 @@ +# 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. + +values: + google_logging_organization_exclusion.logging-exclusion["exclusion1"]: + description: exclusion1 (Terraform-managed). + disabled: null + filter: resource.type=gce_instance + name: exclusion1 + org_id: '1234567890' + google_logging_organization_exclusion.logging-exclusion["exclusion2"]: + description: exclusion2 (Terraform-managed). + disabled: null + filter: severity=NOTICE + name: exclusion2 + org_id: '1234567890' + +counts: + google_logging_organization_exclusion: 2 diff --git a/tests/modules/organization/fixture/test.orgpolicies-boolean.tfvars b/tests/modules/organization/org_policies_boolean.tfvars similarity index 100% rename from tests/modules/organization/fixture/test.orgpolicies-boolean.tfvars rename to tests/modules/organization/org_policies_boolean.tfvars diff --git a/tests/modules/organization/org_policies_boolean.yaml b/tests/modules/organization/org_policies_boolean.yaml new file mode 100644 index 00000000..310997a4 --- /dev/null +++ b/tests/modules/organization/org_policies_boolean.yaml @@ -0,0 +1,53 @@ +# 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. + +values: + google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]: + name: organizations/1234567890/policies/iam.disableServiceAccountKeyCreation + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + timeouts: null + google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]: + name: organizations/1234567890/policies/iam.disableServiceAccountKeyUpload + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId(aa, bb) + location: xxx + title: condition + deny_all: null + enforce: 'TRUE' + values: [] + timeouts: null + +counts: + google_org_policy_policy: 2 diff --git a/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars b/tests/modules/organization/org_policies_custom_constraints.tfvars similarity index 100% rename from tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars rename to tests/modules/organization/org_policies_custom_constraints.tfvars diff --git a/tests/modules/organization/org_policies_custom_constraints.yaml b/tests/modules/organization/org_policies_custom_constraints.yaml new file mode 100644 index 00000000..c558c066 --- /dev/null +++ b/tests/modules/organization/org_policies_custom_constraints.yaml @@ -0,0 +1,37 @@ +# 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. + +values: + google_org_policy_custom_constraint.constraint["custom.dataprocNoMoreThan10Workers"]: + action_type: DENY + condition: resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10 + method_types: + - CREATE + - UPDATE + name: custom.dataprocNoMoreThan10Workers + parent: organizations/1234567890 + resource_types: + - dataproc.googleapis.com/Cluster + google_org_policy_custom_constraint.constraint["custom.gkeEnableAutoUpgrade"]: + action_type: ALLOW + condition: resource.management.autoUpgrade == true + method_types: + - CREATE + name: custom.gkeEnableAutoUpgrade + parent: organizations/1234567890 + resource_types: + - container.googleapis.com/NodePool + +counts: + google_org_policy_custom_constraint: 2 diff --git a/tests/modules/organization/fixture/test.orgpolicies-list.tfvars b/tests/modules/organization/org_policies_list.tfvars similarity index 96% rename from tests/modules/organization/fixture/test.orgpolicies-list.tfvars rename to tests/modules/organization/org_policies_list.tfvars index 73807173..f9de8dba 100644 --- a/tests/modules/organization/fixture/test.orgpolicies-list.tfvars +++ b/tests/modules/organization/org_policies_list.tfvars @@ -3,6 +3,7 @@ org_policies = { deny = { all = true } } "iam.allowedPolicyMemberDomains" = { + inherit_from_parent = true allow = { values = ["C0xxxxxxx", "C0yyyyyyy"] } diff --git a/tests/modules/organization/org_policies_list.yaml b/tests/modules/organization/org_policies_list.yaml new file mode 100644 index 00000000..39c3a389 --- /dev/null +++ b/tests/modules/organization/org_policies_list.yaml @@ -0,0 +1,85 @@ +# 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. + +values: + google_org_policy_policy.default["compute.restrictLoadBalancerCreationForTypes"]: + name: organizations/1234567890/policies/compute.restrictLoadBalancerCreationForTypes + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: null + denied_values: + - in:EXTERNAL + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId(aa, bb) + location: xxx + title: condition + deny_all: null + enforce: null + values: + - allowed_values: + - EXTERNAL_1 + denied_values: null + - allow_all: 'TRUE' + condition: + - description: test condition2 + expression: resource.matchTagId(cc, dd) + location: xxx + title: condition2 + deny_all: null + enforce: null + values: [] + timeouts: null + google_org_policy_policy.default["compute.vmExternalIpAccess"]: + name: organizations/1234567890/policies/compute.vmExternalIpAccess + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: 'TRUE' + enforce: null + values: [] + timeouts: null + google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]: + name: organizations/1234567890/policies/iam.allowedPolicyMemberDomains + parent: organizations/1234567890 + spec: + - inherit_from_parent: true + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - C0xxxxxxx + - C0yyyyyyy + denied_values: null + timeouts: null + +counts: + google_org_policy_policy: 3 diff --git a/tests/modules/organization/fixture/test.resource_tags.tfvars b/tests/modules/organization/tags.tfvars similarity index 91% rename from tests/modules/organization/fixture/test.resource_tags.tfvars rename to tests/modules/organization/tags.tfvars index 42cf7e0f..31c6764e 100644 --- a/tests/modules/organization/fixture/test.resource_tags.tfvars +++ b/tests/modules/organization/tags.tfvars @@ -1,3 +1,8 @@ +network_tags = { + net_environment = { + network = "foobar" + } +} tags = { foo = {} bar = { diff --git a/tests/modules/organization/tags.yaml b/tests/modules/organization/tags.yaml new file mode 100644 index 00000000..7da6f77b --- /dev/null +++ b/tests/modules/organization/tags.yaml @@ -0,0 +1,76 @@ +# 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. + +values: + google_tags_tag_key.default["bar"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: bar + google_tags_tag_key.default["foo"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: foo + google_tags_tag_key.default["foobar"]: + description: Foobar tag. + parent: organizations/1234567890 + purpose: null + purpose_data: null + short_name: foobar + google_tags_tag_key.default["net_environment"]: + description: Managed by the Terraform organization module. + parent: organizations/1234567890 + purpose: GCE_FIREWALL + purpose_data: + network: foobar + short_name: net_environment + google_tags_tag_key_iam_binding.default["foobar:roles/resourcemanager.tagAdmin"]: + condition: [] + members: + - user:user1@example.com + - user:user2@example.com + role: roles/resourcemanager.tagAdmin + google_tags_tag_value.default["foobar/one"]: + description: Managed by the Terraform organization module. + short_name: one + google_tags_tag_value.default["foobar/three"]: + description: Foobar 3. + short_name: three + google_tags_tag_value.default["foobar/two"]: + description: Foobar 2. + short_name: two + google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagAdmin"]: + condition: [] + members: + - user:user4@example.com + role: roles/resourcemanager.tagAdmin + google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagViewer"]: + condition: [] + members: + - user:user3@example.com + role: roles/resourcemanager.tagViewer + google_tags_tag_value_iam_binding.default["foobar/two:roles/resourcemanager.tagViewer"]: + condition: [] + members: + - user:user3@example.com + role: roles/resourcemanager.tagViewer + +counts: + google_tags_tag_key: 4 + google_tags_tag_key_iam_binding: 1 + google_tags_tag_value: 3 + google_tags_tag_value_iam_binding: 3 diff --git a/tests/modules/organization/test_plan.py b/tests/modules/organization/test_plan.py deleted file mode 100644 index 37860ab6..00000000 --- a/tests/modules/organization/test_plan.py +++ /dev/null @@ -1,62 +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_audit_config(plan_runner): - "Test audit config." - iam_audit_config = '{allServices={DATA_READ=[], DATA_WRITE=["user:me@example.org"]}}' - _, resources = plan_runner(iam_audit_config=iam_audit_config) - assert len(resources) == 1 - log_types = set( - r['log_type'] for r in resources[0]['values']['audit_log_config']) - assert log_types == set(['DATA_READ', 'DATA_WRITE']) - - -def test_iam(plan_runner): - "Test IAM." - group_iam = ( - '{' - '"owners@example.org" = ["roles/owner", "roles/resourcemanager.folderAdmin"],' - '"viewers@example.org" = ["roles/viewer"]' - '}') - iam = ('{' - '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' - '"roles/browser" = ["domain:example.org"]' - '}') - _, resources = plan_runner(group_iam=group_iam, iam=iam) - roles = sorted([(r['values']['role'], sorted(r['values']['members'])) - for r in resources - if r['type'] == 'google_organization_iam_binding']) - assert roles == [ - ('roles/browser', ['domain:example.org']), - ('roles/owner', [ - 'group:owners@example.org', 'user:one@example.org', - 'user:two@example.org' - ]), - ('roles/resourcemanager.folderAdmin', ['group:owners@example.org']), - ('roles/viewer', ['group:viewers@example.org']), - ] - - -def test_iam_additive_members(plan_runner): - "Test IAM additive members." - iam = ('{"user:one@example.org" = ["roles/owner"],' - '"user:two@example.org" = ["roles/owner", "roles/editor"]}') - _, resources = plan_runner(iam_additive_members=iam) - roles = set((r['values']['role'], r['values']['member']) - for r in resources - if r['type'] == 'google_organization_iam_member') - assert roles == set([('roles/owner', 'user:one@example.org'), - ('roles/owner', 'user:two@example.org'), - ('roles/editor', 'user:two@example.org')]) diff --git a/tests/modules/organization/test_plan_firewall.py b/tests/modules/organization/test_plan_firewall.py deleted file mode 100644 index 5925e98b..00000000 --- a/tests/modules/organization/test_plan_firewall.py +++ /dev/null @@ -1,130 +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. - -_FACTORY = ''' -{ - cidr_file = "data/firewall-cidrs.yaml" - policy_name = "factory-1" - rules_file = "data/firewall-rules.yaml" -} -''' -_POLICIES = ''' -{ - policy1 = { - allow-ingress = { - description = "" - direction = "INGRESS" - action = "allow" - priority = 100 - ranges = ["10.0.0.0/8"] - ports = { - tcp = ["22"] - } - target_service_accounts = null - target_resources = null - logging = false - } - deny-egress = { - description = "" - direction = "EGRESS" - action = "deny" - priority = 200 - ranges = ["192.168.0.0/24"] - ports = { - tcp = ["443"] - } - target_service_accounts = null - target_resources = null - logging = false - } - } - policy2 = { - allow-ingress = { - description = "" - direction = "INGRESS" - action = "allow" - priority = 100 - ranges = ["10.0.0.0/8"] - ports = { - tcp = ["22"] - } - target_service_accounts = null - target_resources = null - logging = false - } - } - } -''' - - -def test_custom(plan_runner): - 'Test custom firewall policies.' - _, resources = plan_runner(firewall_policies=_POLICIES) - assert len(resources) == 5 - policies = [r for r in resources - if r['type'] == 'google_compute_firewall_policy'] - rules = [r for r in resources - if r['type'] == 'google_compute_firewall_policy_rule'] - assert set(r['index'] for r in policies) == set([ - 'policy1', 'policy2' - ]) - assert set(r['index'] for r in rules) == set([ - 'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress' - ]) - - -def test_factory(plan_runner): - 'Test firewall policy factory.' - _, resources = plan_runner(firewall_policy_factory=_FACTORY) - assert len(resources) == 3 - policies = [r for r in resources - if r['type'] == 'google_compute_firewall_policy'] - rules = [r for r in resources - if r['type'] == 'google_compute_firewall_policy_rule'] - assert set(r['index'] for r in policies) == set([ - 'factory-1' - ]) - assert set(r['index'] for r in rules) == set([ - 'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap' - ]) - - -def test_factory_name(plan_runner): - 'Test firewall policy factory default name.' - factory = _FACTORY.replace('"factory-1"', 'null') - _, resources = plan_runner(firewall_policy_factory=factory) - assert len(resources) == 3 - policies = [r for r in resources - if r['type'] == 'google_compute_firewall_policy'] - assert set(r['index'] for r in policies) == set([ - 'factory' - ]) - - -def test_combined(plan_runner): - 'Test combined rules.' - _, resources = plan_runner(firewall_policies=_POLICIES, - firewall_policy_factory=_FACTORY) - assert len(resources) == 8 - policies = [r for r in resources - if r['type'] == 'google_compute_firewall_policy'] - rules = [r for r in resources - if r['type'] == 'google_compute_firewall_policy_rule'] - assert set(r['index'] for r in policies) == set([ - 'factory-1', 'policy1', 'policy2' - ]) - assert set(r['index'] for r in rules) == set([ - 'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap', - 'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress' - ]) diff --git a/tests/modules/organization/test_plan_logging.py b/tests/modules/organization/test_plan_logging.py deleted file mode 100644 index 287a5a48..00000000 --- a/tests/modules/organization/test_plan_logging.py +++ /dev/null @@ -1,126 +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. - -from collections import Counter - - -def test_sinks(plan_runner): - "Test folder-level sinks." - tfvars = 'test.logging-sinks.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - assert len(resources) == 8 - - resource_types = Counter([r["type"] for r in resources]) - assert resource_types == { - "google_logging_organization_sink": 4, - "google_bigquery_dataset_iam_member": 1, - "google_project_iam_member": 1, - "google_pubsub_topic_iam_member": 1, - "google_storage_bucket_iam_member": 1, - } - - sinks = [ - r for r in resources if r["type"] == "google_logging_organization_sink" - ] - assert sorted([r["index"] for r in sinks]) == [ - "debug", - "info", - "notice", - "warning", - ] - values = [( - r["index"], - r["values"]["filter"], - r["values"]["destination"], - r["values"]["include_children"], - ) for r in sinks] - assert sorted(values) == [ - ( - "debug", - "severity=DEBUG", - "logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket", - False, - ), - ( - "info", - "severity=INFO", - "bigquery.googleapis.com/projects/myproject/datasets/mydataset", - True, - ), - ( - "notice", - "severity=NOTICE", - "pubsub.googleapis.com/projects/myproject/topics/mytopic", - False, - ), - ("warning", "severity=WARNING", "storage.googleapis.com/mybucket", True), - ] - - bindings = [r for r in resources if "member" in r["type"]] - values = [(r["index"], r["type"], r["values"]["role"]) for r in bindings] - assert sorted(values) == [ - ("debug", "google_project_iam_member", "roles/logging.bucketWriter"), - ("info", "google_bigquery_dataset_iam_member", - "roles/bigquery.dataEditor"), - ("notice", "google_pubsub_topic_iam_member", "roles/pubsub.publisher"), - ("warning", "google_storage_bucket_iam_member", - "roles/storage.objectCreator"), - ] - - exclusions = [(r["index"], r["values"]["exclusions"]) for r in sinks] - assert sorted(exclusions) == [ - ( - "debug", - [ - { - "description": None, - "disabled": False, - "filter": "logName:compute", - "name": "no-compute", - }, - { - "description": None, - "disabled": False, - "filter": "logName:container", - "name": "no-container", - }, - ], - ), - ("info", []), - ("notice", []), - ("warning", []), - ] - - -def test_exclusions(plan_runner): - "Test folder-level logging exclusions." - logging_exclusions = ("{" - 'exclusion1 = "resource.type=gce_instance", ' - 'exclusion2 = "severity=NOTICE", ' - "}") - _, resources = plan_runner(logging_exclusions=logging_exclusions) - assert len(resources) == 2 - exclusions = [ - r for r in resources - if r["type"] == "google_logging_organization_exclusion" - ] - assert sorted([r["index"] for r in exclusions]) == [ - "exclusion1", - "exclusion2", - ] - values = [(r["index"], r["values"]["filter"]) for r in exclusions] - assert sorted(values) == [ - ("exclusion1", "resource.type=gce_instance"), - ("exclusion2", "severity=NOTICE"), - ] diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 05550832..1e041dbc 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -14,46 +14,32 @@ import pathlib -from .validate_policies import validate_policy_boolean, validate_policy_list, validate_policy_custom_constraints +import pytest + +_params = ['boolean', 'list'] -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." - 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) - - -def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path): +@pytest.mark.parametrize('policy_type', _params) +def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): 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) + tfvars_to_yaml(f'org_policies_{policy_type}.tfvars', dest, 'org_policies') + tfvars_plan = plan_summary( + 'modules/organization', + tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) + yaml_plan = plan_summary('modules/organization', + tf_var_files=['common.tfvars'], + org_policies_data_path=f'{tmp_path}') + assert tfvars_plan.values == yaml_plan.values -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) - - -def test_factory_policy_custom_constraints(plan_runner, tfvars_to_yaml, tmp_path): +def test_custom_constraint_factory(plan_summary, 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) + tfvars_to_yaml(f'org_policies_custom_constraints.tfvars', dest, + 'org_policy_custom_constraints') + tfvars_plan = plan_summary( + 'modules/organization', + tf_var_files=['common.tfvars', f'org_policies_custom_constraints.tfvars']) + yaml_plan = plan_summary( + 'modules/organization', tf_var_files=['common.tfvars'], + org_policy_custom_constraints_data_path=f'{tmp_path}') + assert tfvars_plan.values == yaml_plan.values diff --git a/tests/modules/organization/test_plan_tags.py b/tests/modules/organization/test_plan_tags.py deleted file mode 100644 index bce2208a..00000000 --- a/tests/modules/organization/test_plan_tags.py +++ /dev/null @@ -1,52 +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_resource_tags(plan_runner): - 'Test resource tags.' - _, resources = plan_runner(tf_var_file='test.resource_tags.tfvars') - assert len(resources) == 10 - resource_values = {} - for r in resources: - resource_values.setdefault(r['type'], []).append(r['values']) - assert len(resource_values['google_tags_tag_key']) == 3 - assert len(resource_values['google_tags_tag_value']) == 3 - result = [ - r['role'] for r in resource_values['google_tags_tag_value_iam_binding'] - ] - expected = [ - 'roles/resourcemanager.tagAdmin', - 'roles/resourcemanager.tagViewer', - 'roles/resourcemanager.tagViewer' - ] - assert result == expected - - -def test_network_tags(plan_runner): - 'Test network tags.' - _, resources = plan_runner(tf_var_file='test.network_tags.tfvars') - assert len(resources) == 1 - resource_values = {} - for r in resources: - resource_values.setdefault(r['type'], []).append(r['values']) - google_tags_tag_key = resource_values['google_tags_tag_key'][0] - assert google_tags_tag_key['purpose'] == "GCE_FIREWALL" - assert google_tags_tag_key['purpose_data']['network'] == "foobar" - - -def test_bindings(plan_runner): - 'Test tag bindings.' - tag_bindings = '{foo = "tagValues/123456789012"}' - _, resources = plan_runner(tag_bindings=tag_bindings) - assert len(resources) == 1 diff --git a/tests/modules/organization/tftest.yaml b/tests/modules/organization/tftest.yaml new file mode 100644 index 00000000..c88e05c1 --- /dev/null +++ b/tests/modules/organization/tftest.yaml @@ -0,0 +1,35 @@ +# 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: modules/organization + +common_tfvars: + - common.tfvars + +tests: + audit_config: + iam: + iam_additive: + logging: + logging_exclusions: + org_policies_list: + org_policies_boolean: + org_policies_custom_constraints: + tags: + firewall_policies: + firewall_policies_factory: + firewall_policies_factory_combined: + tfvars: + - firewall_policies.tfvars + - firewall_policies_factory.tfvars diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index d4e4559d..8463761e 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -31,13 +31,14 @@ def test_policy_list(plan_runner): 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') + tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest, + 'org_policies') _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') validate_policy_boolean(resources) 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') + tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies') _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') validate_policy_list(resources) diff --git a/tests/requirements.txt b/tests/requirements.txt index 3eb583ab..a6f82d75 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ pytest>=6.2.5 PyYAML>=6.0 -tftest>=1.7.6 +tftest>=1.8.1 marko>=1.2.0 deepdiff>=5.7.0 python-hcl2>=3.0.5 diff --git a/tools/plan_summary.py b/tools/plan_summary.py new file mode 100755 index 00000000..def79adb --- /dev/null +++ b/tools/plan_summary.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +import sys +import yaml + +from pathlib import Path + +BASEDIR = Path(__file__).parents[1] +sys.path.append(str(BASEDIR / 'tests')) + +import fixtures + + +@click.command() +@click.argument('module', type=click.Path(), nargs=1) +@click.argument('tfvars', type=click.Path(exists=True), nargs=-1) +def main(module, tfvars): + module = BASEDIR / module + summary = fixtures.plan_summary(module, Path(), tfvars) + print(yaml.dump({'values': summary.values})) + print(yaml.dump({'counts': summary.counts})) + outputs = { + k: v.get('value', '__missing__') for k, v in summary.outputs.items() + } + print(yaml.dump({'outputs': outputs})) + + +if __name__ == '__main__': + main()