Merge branch 'master' into lcaggioni/fast-data-platform

This commit is contained in:
lcaggio 2022-02-16 09:56:42 +01:00 committed by GitHub
commit 8e8378fb28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 73 additions and 140 deletions

View File

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- **incompatible change** the variable for service identities IAM has changed in the project factory
- the `net-vpc` and `project` modules now use the beta provider for shared VPC-related resources
- add `data-catalog-policy-tag` module
- **incompatible change** the `psn_ranges` variable has been renamed to `psa_ranges` in the `net-vpc` module and its type changed from `list(string)` to `map(string)`
## [13.0.0] - 2022-01-27

View File

@ -39,13 +39,13 @@ locals {
shared_vpc_project = try(var.network_config.host_project, null)
# this is needed so that for_each only uses static values
shared_vpc_role_members = {
load-robot-df = module.load-project.service_accounts.robots.dataflow
load-robot-df = "serviceAccount:${module.load-project.service_accounts.robots.dataflow}"
load-sa-df-worker = module.load-sa-df-0.iam_email
orch-cloudservices = module.orch-project.service_accounts.cloud_services
orch-robot-cs = module.orch-project.service_accounts.robots.composer
orch-robot-df = module.orch-project.service_accounts.robots.dataflow
orch-robot-gke = module.orch-project.service_accounts.robots.container-engine
transf-robot-df = module.transf-project.service_accounts.robots.dataflow
orch-cloudservices = "serviceAccount:${module.orch-project.service_accounts.cloud_services}"
orch-robot-cs = "serviceAccount:${module.orch-project.service_accounts.robots.composer}"
orch-robot-df = "serviceAccount:${module.orch-project.service_accounts.robots.dataflow}"
orch-robot-gke = "serviceAccount:${module.orch-project.service_accounts.robots.container-engine}"
transf-robot-df = "serviceAccount:${module.transf-project.service_accounts.robots.dataflow}"
transf-sa-df-worker = module.transf-sa-df-0.iam_email
}
# reassemble in a format suitable for for_each

View File

@ -77,7 +77,7 @@ module "branch-teams-team-dev-folder" {
for_each = coalesce(var.team_folders, {})
parent = module.branch-teams-team-folder[each.key].id
# naming: environment descriptive name
name = "${module.branch-teams-team-folder[each.key].name} - Development"
name = "Development"
# environment-wide human permissions on the whole teams environment
group_iam = {}
iam = {
@ -127,7 +127,7 @@ module "branch-teams-team-prod-folder" {
for_each = coalesce(var.team_folders, {})
parent = module.branch-teams-team-folder[each.key].id
# naming: environment descriptive name
name = "${module.branch-teams-team-folder[each.key].name} - Production"
name = "Production"
# environment-wide human permissions on the whole teams environment
group_iam = {}
iam = {

View File

@ -353,8 +353,8 @@ Don't forget to add a peering zone in the landing project and point it to the ne
| [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | <code>google_monitoring_dashboard</code> |
| [nva.tf](./nva.tf) | None | <code>compute-mig</code> · <code>compute-vm</code> · <code>net-ilb</code> | |
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>local_file</code> |
| [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | <code>net-address</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>net-vpc-peering</code> · <code>project</code> | |
| [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | <code>net-address</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>net-vpc-peering</code> · <code>project</code> | |
| [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>net-vpc-peering</code> · <code>project</code> | |
| [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>net-vpc-peering</code> · <code>project</code> | |
| [test-resources.tf](./test-resources.tf) | temporary instances for testing | <code>compute-vm</code> | |
| [variables.tf](./variables.tf) | Module variables. | | |
| [vpn-onprem.tf](./vpn-onprem.tf) | VPN between landing and onprem. | <code>net-vpn-ha</code> | |

View File

@ -50,6 +50,7 @@ module "dev-spoke-vpc" {
mtu = 1500
data_folder = "${var.data_dir}/subnets/dev"
delete_default_routes_on_create = true
psa_ranges = var.psa_ranges.dev
subnets_l7ilb = local.l7ilb_subnets.dev
# Set explicit routes for googleapis; send everything else to NVAs
routes = {
@ -110,17 +111,6 @@ module "dev-spoke-firewall" {
cidr_template_file = "${var.data_dir}/cidrs.yaml"
}
module "dev-spoke-psa-addresses" {
source = "../../../modules/net-address"
project_id = module.dev-spoke-project.project_id
psa_addresses = { for r, v in var.psa_ranges.dev : r => {
address = cidrhost(v, 0)
network = module.dev-spoke-vpc.self_link
prefix_length = split("/", v)[1]
}
}
}
module "peering-dev" {
source = "../../../modules/net-vpc-peering"
prefix = "dev-peering-0"

View File

@ -50,6 +50,7 @@ module "prod-spoke-vpc" {
mtu = 1500
data_folder = "${var.data_dir}/subnets/prod"
delete_default_routes_on_create = true
psa_ranges = var.psa_ranges.prod
subnets_l7ilb = local.l7ilb_subnets.prod
# Set explicit routes for googleapis; send everything else to NVAs
routes = {
@ -110,17 +111,6 @@ module "prod-spoke-firewall" {
cidr_template_file = "${var.data_dir}/cidrs.yaml"
}
module "prod-spoke-psa-addresses" {
source = "../../../modules/net-address"
project_id = module.prod-spoke-project.project_id
psa_addresses = { for r, v in var.psa_ranges.prod : r => {
address = cidrhost(v, 0)
network = module.prod-spoke-vpc.self_link
prefix_length = split("/", v)[1]
}
}
}
module "peering-prod" {
source = "../../../modules/net-vpc-peering"
prefix = "prod-peering-0"

View File

@ -296,8 +296,8 @@ DNS configurations are centralised in the `dns.tf` file. Spokes delegate DNS res
| [main.tf](./main.tf) | Networking folder and hierarchical policy. | <code>folder</code> | |
| [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | <code>google_monitoring_dashboard</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>local_file</code> |
| [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | <code>net-address</code> · <code>net-cloudnat</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>project</code> | <code>google_project_iam_binding</code> |
| [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | <code>net-address</code> · <code>net-cloudnat</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>project</code> | <code>google_project_iam_binding</code> |
| [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | <code>net-cloudnat</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>project</code> | <code>google_project_iam_binding</code> |
| [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | <code>net-cloudnat</code> · <code>net-vpc</code> · <code>net-vpc-firewall</code> · <code>project</code> | <code>google_project_iam_binding</code> |
| [test-resources.tf](./test-resources.tf) | temporary instances for testing | <code>compute-vm</code> | |
| [variables.tf](./variables.tf) | Module variables. | | |
| [vpn-onprem.tf](./vpn-onprem.tf) | VPN between landing and onprem. | <code>net-vpn-ha</code> | |

View File

@ -54,6 +54,7 @@ module "dev-spoke-vpc" {
name = "dev-spoke-0"
mtu = 1500
data_folder = "${var.data_dir}/subnets/dev"
psa_ranges = var.psa_ranges.dev
subnets_l7ilb = local.l7ilb_subnets.dev
# set explicit routes for googleapis in case the default route is deleted
routes = {
@ -98,17 +99,6 @@ module "dev-spoke-cloudnat" {
logging_filter = "ERRORS_ONLY"
}
module "dev-spoke-psa-addresses" {
source = "../../../modules/net-address"
project_id = module.dev-spoke-project.project_id
psa_addresses = { for r, v in var.psa_ranges.dev : r => {
address = cidrhost(v, 0)
network = module.dev-spoke-vpc.self_link
prefix_length = split("/", v)[1]
}
}
}
# Create delegated grants for stage3 service accounts
resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" {
project = module.dev-spoke-project.project_id

View File

@ -54,6 +54,7 @@ module "prod-spoke-vpc" {
name = "prod-spoke-0"
mtu = 1500
data_folder = "${var.data_dir}/subnets/prod"
psa_ranges = var.psa_ranges.prod
subnets_l7ilb = local.l7ilb_subnets.prod
# set explicit routes for googleapis in case the default route is deleted
routes = {
@ -98,17 +99,6 @@ module "prod-spoke-cloudnat" {
logging_filter = "ERRORS_ONLY"
}
module "prod-spoke-psa-addresses" {
source = "../../../modules/net-address"
project_id = module.prod-spoke-project.project_id
psa_addresses = { for r, v in var.psa_ranges.prod : r => {
address = cidrhost(v, 0)
network = module.prod-spoke-vpc.self_link
prefix_length = split("/", v)[1]
}
}
}
# Create delegated grants for stage3 service accounts
resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" {
project = module.prod-spoke-project.project_id

View File

@ -3,4 +3,4 @@
The Project Factory (PF) builds on top of your foundations to create and set up projects (and related resources) to be used for your workloads.
It is organized in folders representing environments (e.g. "dev", "prod"), each implemented by a stand-alone terraform [resource factory](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c).
This directory contains a single project factory ([`prod/`](./prod/)) as an example - to implement multiple environments (e.g. "prod" and "dev") you'll need to copy the `prod` folder into one folder per environment, then customize each one following the instructions found in [`prod/README.md`](./prod/README.md).
This directory contains a single project factory ([`dev/`](./dev/)) as an example - to implement multiple environments (e.g. "prod" and "dev") you'll need to copy the `dev` folder into one folder per environment, then customize each one following the instructions found in [`dev/README.md`](./dev/README.md).

View File

@ -49,7 +49,7 @@ It's of course possible to run this stage in isolation, by making sure the archi
- `"roles/compute.viewer"`
- `"roles/dns.admin"`
- If networking is used (e.g., for VMs, GKE Clusters or AppEngine flex), VPC Host projects and their subnets should exist when creating projects
- If per-environment DNS sub-zones are required, one "root" zone per environment should exist when creating projects (e.g., prod.gcp.example.com.)
- If per-environment DNS sub-zones are required, one "root" zone per environment should exist when creating projects (e.g., dev.gcp.example.com.)
### Providers configuration
@ -57,8 +57,8 @@ If you're running this on top of Fast, you should run the following commands to
```bash
# Variable `outputs_location` is set to `../../../config` in stage 01-resman
$ cd fabric-fast/stages/03-project-factory/prod
ln -s ../../../config/03-project-factory-prod/providers.tf
$ cd fabric-fast/stages/03-project-factory/dev
ln -s ../../../config/03-project-factory-dev/providers.tf
```
### Variable configuration
@ -74,16 +74,16 @@ If you configured a valid path for `outputs_location` in the bootstrap and netwo
```bash
# Variable `outputs_location` is set to `../../../config` in stages 01-bootstrap and the 02-networking stage in use
ln -s ../../../config/03-project-factory-prod/terraform-bootstrap.auto.tfvars.json
ln -s ../../../config/03-project-factory-prod/terraform-networking.auto.tfvars.json
ln -s ../../../config/03-project-factory-dev/terraform-bootstrap.auto.tfvars.json
ln -s ../../../config/03-project-factory-dev/terraform-networking.auto.tfvars.json
```
If you're not using Fast, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning.
Besides the values above, a project factory takes 2 additional inputs:
- `data/defaults.yaml`, manually configured by adapting the [`prod/data/defaults.yaml.sample`](./prod/data/defaults.yaml.sample), which defines per-environment default values e.g., for billing alerts and labels.
- `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`prod/data/projects/project.yaml.sample`](./prod/data/projects/project.yaml.sample) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-prod-lab0.yaml` will create project `fast-prod-lab0`.
- `data/defaults.yaml`, manually configured by adapting the [`data/defaults.yaml`](./data/defaults.yaml), which defines per-environment default values e.g., for billing alerts and labels.
- `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`data/projects/project.yaml`](./data/projects/project.yaml) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-dev-lab0.yaml` will create project `fast-dev-lab0`.
Once the configuration is complete, run the project factory by running

View File

@ -15,7 +15,7 @@ essential_contacts: ["team-contacts@example.com"]
# [opt] Labels set for all projects
labels:
environment: prod
environment: dev
department: accounting
application: example-app
foo: bar

View File

@ -44,7 +44,7 @@ kms_service_agents:
# [opt] Labels for the project - merged with the ones defined in defaults
labels:
environment: prod
environment: dev
# [opt] Org policy overrides defined at project level
org_policies:
@ -56,7 +56,7 @@ org_policies:
status: true
suggested_value: null
values:
- projects/fast-prod-iac-core-0
- projects/fast-dev-iac-core-0
# [opt] Service account to create for the project and their roles on the project
# in name => [roles] format
@ -90,11 +90,11 @@ vpc:
enable_security_admin: true
# Host project the project will be service project of
host_project: fast-prod-net-spoke-0
host_project: fast-dev-net-spoke-0
# [opt] Subnets in the host project where principals will be granted networkUser
# in region/subnet-name => [principals]
subnets_iam:
europe-west1/prod-default-ew1:
europe-west1/dev-default-ew1:
- user:foobar@example.com
- serviceAccount:service-account1

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

View File

@ -25,7 +25,7 @@ module "vpc" {
source = "./modules/net-vpc"
project_id = module.project.project_id
name = "my-network"
psn_ranges = ["10.60.0.0/16"]
psa_ranges = {cloudsql-ew1-0="10.60.0.0/16"}
}
module "db" {

View File

@ -61,7 +61,7 @@ module "folder" {
policy_name = null
rules_file = "data/rules.yaml"
}
firewall_policy_attachments = {
firewall_policy_association = {
factory-policy = module.folder.firewall_policy_id["factory"]
}
}

View File

@ -138,7 +138,7 @@ module "vpc" {
secondary_ip_range = null
}
]
psn_ranges = ["10.10.0.0/16"]
psa_ranges = {range-a = "10.10.0.0/16"}
}
# tftest modules=1 resources=4
```
@ -171,8 +171,8 @@ module "vpc" {
```
### Subnet Factory
The `net-vpc` module includes a subnet factory (see [Resource Factories](../../examples/factories/)) for the massive creation of subnets leveraging one configuration file per subnet.
The `net-vpc` module includes a subnet factory (see [Resource Factories](../../examples/factories/)) for the massive creation of subnets leveraging one configuration file per subnet.
```hcl
module "vpc" {
@ -220,7 +220,7 @@ flow_logs: # enable, set to empty map to use defaults
| [mtu](variables.tf#L80) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 and the maximum value is 1500 bytes. | <code></code> | | <code>null</code> |
| [peering_config](variables.tf#L90) | VPC peering configuration. | <code title="object&#40;&#123;&#10; peer_vpc_self_link &#61; string&#10; export_routes &#61; bool&#10; import_routes &#61; bool&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [peering_create_remote_end](variables.tf#L100) | Skip creation of peering on the remote end when using peering_config. | <code>bool</code> | | <code>true</code> |
| [psn_ranges](variables.tf#L111) | CIDR ranges used for Google services that support Private Service Networking. | <code>list&#40;string&#41;</code> | | <code>null</code> |
| [psa_ranges](variables.tf#L111) | CIDR ranges used for Google services that support Private Service Networking. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [routes](variables.tf#L124) | Network routes, keyed by name. | <code title="map&#40;object&#40;&#123;&#10; dest_range &#61; string&#10; priority &#61; number&#10; tags &#61; list&#40;string&#41;&#10; next_hop_type &#61; string &#35; gateway, instance, ip, vpn_tunnel, ilb&#10; next_hop &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [routing_mode](variables.tf#L136) | The network routing mode (default 'GLOBAL'). | <code>string</code> | | <code>&#34;GLOBAL&#34;</code> |
| [shared_vpc_host](variables.tf#L146) | Enable shared VPC for this project. | <code>bool</code> | | <code>false</code> |

View File

@ -78,11 +78,11 @@ locals {
? null
: element(reverse(split("/", var.peering_config.peer_vpc_self_link)), 0)
)
psn_ranges = {
for r in(var.psn_ranges == null ? [] : var.psn_ranges) : r => {
address = split("/", r)[0]
name = replace(split("/", r)[0], ".", "-")
prefix_length = split("/", r)[1]
psa_ranges = {
for k, v in coalesce(var.psa_ranges, {}) : k => {
address = split("/", v)[0]
name = k
prefix_length = split("/", v)[1]
}
}
routes = {
@ -328,10 +328,10 @@ resource "google_dns_policy" "default" {
}
}
resource "google_compute_global_address" "psn_ranges" {
for_each = local.psn_ranges
resource "google_compute_global_address" "psa_ranges" {
for_each = local.psa_ranges
project = var.project_id
name = "${var.name}-psn-${each.value.name}"
name = "${var.name}-psa-${each.key}"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
address = each.value.address
@ -339,11 +339,11 @@ resource "google_compute_global_address" "psn_ranges" {
network = local.network.id
}
resource "google_service_networking_connection" "psn_connection" {
for_each = toset(local.psn_ranges == {} ? [] : [""])
resource "google_service_networking_connection" "psa_connection" {
for_each = toset(local.psa_ranges == {} ? [] : [""])
network = local.network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [
for k, v in google_compute_global_address.psn_ranges : v.name
for k, v in google_compute_global_address.psa_ranges : v.name
]
}

View File

@ -27,7 +27,7 @@ output "name" {
google_compute_network_peering.remote,
google_compute_shared_vpc_host_project.shared_vpc_host,
google_compute_shared_vpc_service_project.service_projects,
google_service_networking_connection.psn_connection
google_service_networking_connection.psa_connection
]
}
@ -39,7 +39,7 @@ output "network" {
google_compute_network_peering.remote,
google_compute_shared_vpc_host_project.shared_vpc_host,
google_compute_shared_vpc_service_project.service_projects,
google_service_networking_connection.psn_connection
google_service_networking_connection.psa_connection
]
}
@ -52,7 +52,7 @@ output "project_id" {
google_compute_network_peering.remote,
google_compute_shared_vpc_host_project.shared_vpc_host,
google_compute_shared_vpc_service_project.service_projects,
google_service_networking_connection.psn_connection
google_service_networking_connection.psa_connection
]
}
@ -64,7 +64,7 @@ output "self_link" {
google_compute_network_peering.remote,
google_compute_shared_vpc_host_project.shared_vpc_host,
google_compute_shared_vpc_service_project.service_projects,
google_service_networking_connection.psn_connection
google_service_networking_connection.psa_connection
]
}

View File

@ -108,16 +108,16 @@ variable "project_id" {
type = string
}
variable "psn_ranges" {
variable "psa_ranges" {
description = "CIDR ranges used for Google services that support Private Service Networking."
type = list(string)
type = map(string)
default = null
validation {
condition = alltrue([
for r in(var.psn_ranges == null ? [] : var.psn_ranges) :
can(cidrnetmask(r))
for k, v in(var.psa_ranges == null ? {} : var.psa_ranges) :
can(cidrnetmask(v))
])
error_message = "Specify a valid RFC1918 CIDR range for Private Service Networking."
error_message = "Specify valid RFC1918 CIDR ranges for Private Service Networking."
}
}

View File

@ -14,44 +14,15 @@
* limitations under the License.
*/
# tfdoc:file:description Project factory.
locals {
_defaults = yamldecode(file(var.defaults_file))
_defaults_net = {
billing_account_id = var.billing_account_id
environment_dns_zone = var.environment_dns_zone
shared_vpc_self_link = var.shared_vpc_self_link
vpc_host_project = var.vpc_host_project
}
defaults = merge(local._defaults, local._defaults_net)
projects = {
for f in fileset("${var.data_dir}", "**/*.yaml") :
trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
}
}
module "projects" {
#TODO(sruffilli): Pin to release
source = "../../../../../examples/factories/project-factory"
for_each = local.projects
defaults = local.defaults
project_id = each.key
billing_account_id = try(each.value.billing_account_id, null)
billing_alert = try(each.value.billing_alert, null)
dns_zones = try(each.value.dns_zones, [])
essential_contacts = try(each.value.essential_contacts, [])
folder_id = each.value.folder_id
group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
kms_service_agents = try(each.value.kms, {})
labels = try(each.value.labels, {})
org_policies = try(each.value.org_policies, null)
service_accounts = try(each.value.service_accounts, {})
services = try(each.value.services, [])
services_iam = try(each.value.services_iam, {})
vpc = try(each.value.vpc, null)
source = "../../../../../fast/stages/03-project-factory/dev"
data_dir = "./data/projects/"
defaults_file = "./data/defaults.yaml"
prefix = "test"
billing_account_id = "12345-67890A-BCDEF0"
environment_dns_zone = "dev"
shared_vpc_self_link = "fake_link"
vpc_host_project = "host_project"
}

View File

@ -30,6 +30,6 @@ module "test" {
subnet_flow_logs = var.subnet_flow_logs
subnet_private_access = var.subnet_private_access
auto_create_subnetworks = var.auto_create_subnetworks
psn_ranges = var.psn_ranges
psa_ranges = var.psa_ranges
data_folder = var.data_folder
}

View File

@ -61,8 +61,8 @@ variable "peering_config" {
default = null
}
variable "psn_ranges" {
type = list(string)
variable "psa_ranges" {
type = map(string)
default = null
}

View File

@ -16,20 +16,21 @@ import tftest
def test_single_range(plan_runner):
"Test single PSN range."
_, resources = plan_runner(psn_ranges='["172.16.100.0/24"]')
"Test single PSA range."
_, resources = plan_runner(psa_ranges='{foobar="172.16.100.0/24"}')
assert len(resources) == 3
def test_multi_range(plan_runner):
"Test multiple PSN ranges."
_, resources = plan_runner(psn_ranges='["172.16.100.0/24", "172.16.101.0/24"]')
"Test multiple PSA ranges."
psa_ranges = '{foobar="172.16.100.0/24", frobniz="172.16.101.0/24"}'
_, resources = plan_runner(psa_ranges=psa_ranges)
assert len(resources) == 4
def test_validation(plan_runner):
"Test PSN variable validation."
"Test PSA variable validation."
try:
plan_runner(psn_ranges='["foobar"]')
plan_runner(psa_ranges='{foobar="foobar"}')
except tftest.TerraformTestError as e:
assert 'Invalid value for variable' in e.args[0]