diff --git a/.ci/cloudbuild.lint.yaml b/.ci/cloudbuild.lint.yaml
index 4a178185..745cc433 100644
--- a/.ci/cloudbuild.lint.yaml
+++ b/.ci/cloudbuild.lint.yaml
@@ -43,11 +43,13 @@ steps:
args:
[
"/workspace/tools/check_documentation.py",
- "modules",
"cloud-operations",
"data-solutions",
"data-solutions/data-platform-foundations",
+ "factories",
+ "factories/firewall-vpc-rules"
"foundations",
+ "modules",
"networking",
]
diff --git a/.ci/cloudbuild.test.environments.yaml b/.ci/cloudbuild.test.environments.yaml
index 9c5b2a6f..180f33de 100644
--- a/.ci/cloudbuild.test.environments.yaml
+++ b/.ci/cloudbuild.test.environments.yaml
@@ -32,6 +32,7 @@ steps:
- -vv
- tests/cloud_operations
- tests/data_solutions
+ - tests/factories
- tests/foundations
- tests/networking
env:
diff --git a/factories/README.md b/factories/README.md
new file mode 100644
index 00000000..b102f0b4
--- /dev/null
+++ b/factories/README.md
@@ -0,0 +1,40 @@
+# Resource Factories
+
+This set of modules creates specialized resource factories, made of two distinct components:
+
+- a module, which implements the factory logic in Terraform syntax, and
+- a set of directories, which hold the configuration for the factory in YAML syntax.
+
+## Available modules
+
+- [Hierarchical Firewall policies](./firewall-hierarchical-policies)
+- [VPC Firewall rules](./firewall-vpc-rules)
+- [Subnets](./subnets)
+
+## Example implementation
+
+See [Example environments](./example-environments)
+
+## Using the modules
+
+Each module is specialised and comes with a `README.md` file which describes the module interface, as well as the directory structure each module requires.
+
+## Rationale
+This approach is based on modules implementing the factory logic using Terraform code and a set of directories having a well-defined, semantic structure, holding the configuration for the resources in YaML syntax.
+
+Resource factories are designed to:
+
+- accelerate and rationalize the repetitive creation of common resources, such as firewall rules and subnets.
+- enable teams without Terraform specific knowledge to build IaC leveraging human-friendly and machine-parseable YAML files
+- make it simple to implement specific requirements and best practices (e.g. "always enable PGA for GCP subnets", or "only allow using regions `europe-west1` and `europe-west3`")
+- codify and centralise business logics and policies (e.g. labels and naming conventions)
+
+Terraform natively supports YaML, JSON and CSV parsing - however we've decided to embrace YaML for the following reasons:
+
+- YaML is easier to parse for a human, and allows for comments and nested, complex structures
+- JSON and CSV can't include comments, which can be used to document configurations, but are often useful to bridge from other systems in automated pipelines
+- JSON is more verbose (reads: longer) and harder to parse for a human
+- CSV isn't often expressive enough (e.g. doesn't allow for nested structures)
+
+If needed, converting factories to consume JSON instead is a matter of switching from `yamldecode()` to `jsondecode()` in the right place on each module.
+
diff --git a/factories/example-environments/README.md b/factories/example-environments/README.md
new file mode 100644
index 00000000..a90219ab
--- /dev/null
+++ b/factories/example-environments/README.md
@@ -0,0 +1,42 @@
+# Resource Factories
+
+The example in this folder are derived from actual production use cases, and show how to use a factory module and how you could structure your codebase for multiple environments.
+
+
+## Resource Factories usage - Managing subnets
+
+
+At the top level of this directory, besides the `README.md` your're reading now, you'll find
+
+- `dev/`, a directory which holds all configurations for the *development* environment
+- `prod/`, a directory which holds all configurations for the *production* environment
+- `main.tf`, a simple terraform file which consumes the [`subnets`](../subnets/) module
+
+
+Each environment directory structure is meant to mimic your GCP resources structure
+
+```
+.
+├── dev # Environment
+│ ├── project-dev-a # Project id
+│ │ └── vpc-alpha # VPC name
+│ │ ├── subnet-alpha-a.yaml # Subnet name (one file per subnet)
+│ │ └── subnet-alpha-b.yaml
+│ └── project-dev-b
+│ ├── vpc-beta
+│ │ └── subnet-beta-a.yaml
+│ └── vpc-gamma
+│ └── subnet-gamma-a.yaml
+├── prod
+│ └── project-prod-a
+│ └── vpc-alpha
+│ ├── subnet-alpha-a.yaml
+│ └── subnet-alpha-b.yaml
+├── main.tf
+└── README.md
+```
+
+Since this resource factory only creates subnets, projects and VPCs are expected to exist.
+
+In this example, a single `main.tf` file (hence a single state) drives the creation of both the `dev` and the `prod` environment. Another option you might want to consider, in line with the CI/CD pipeline or processes you have in place, might be to move the `main.tf` to the each environment directory, so that states (and pipelines) can be separated.
+
diff --git a/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-a.yaml b/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-a.yaml
new file mode 100644
index 00000000..61844e76
--- /dev/null
+++ b/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-a.yaml
@@ -0,0 +1,6 @@
+region: europe-west3
+ip_cidr_range: 10.0.0.0/24
+description: Sample Subnet in project project-dev-a, vpc-alpha
+secondary_ip_ranges:
+ secondary-range-a: 192.168.0.0/24
+ secondary-range-b: 192.168.1.0/24
diff --git a/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-b.yaml b/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-b.yaml
new file mode 100644
index 00000000..64e5ba7b
--- /dev/null
+++ b/factories/example-environments/dev/project-dev-a/vpc-alpha/subnet-alpha-b.yaml
@@ -0,0 +1,4 @@
+region: europe-west3
+ip_cidr_range: 10.0.1.0/24
+description: Sample Subnet in project project-dev-a, vpc-alpha
+private_ip_google_access: false
diff --git a/factories/example-environments/dev/project-dev-b/vpc-beta/subnet-beta-a.yaml b/factories/example-environments/dev/project-dev-b/vpc-beta/subnet-beta-a.yaml
new file mode 100644
index 00000000..276b5063
--- /dev/null
+++ b/factories/example-environments/dev/project-dev-b/vpc-beta/subnet-beta-a.yaml
@@ -0,0 +1,5 @@
+region: europe-west4
+ip_cidr_range: 10.0.2.0/24
+description: Sample Subnet in project project-dev-b, vpc-beta
+iam_users: ["sruffilli@google.com"]
+iam_groups: []
diff --git a/factories/example-environments/dev/project-dev-b/vpc-gamma/subnet-gamma-a.yaml b/factories/example-environments/dev/project-dev-b/vpc-gamma/subnet-gamma-a.yaml
new file mode 100644
index 00000000..2cf95c16
--- /dev/null
+++ b/factories/example-environments/dev/project-dev-b/vpc-gamma/subnet-gamma-a.yaml
@@ -0,0 +1,3 @@
+region: europe-west4
+ip_cidr_range: 10.0.3.0/24
+description: Sample Subnet in project project-dev-b, vpc-gamma
diff --git a/factories/example-environments/main.tf b/factories/example-environments/main.tf
new file mode 100644
index 00000000..08906178
--- /dev/null
+++ b/factories/example-environments/main.tf
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2021 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 "subnets-dev" {
+ source = "../subnets"
+ config_folder = "dev"
+}
+
+module "subnets-prod" {
+ source = "../subnets"
+ config_folder = "prod"
+}
diff --git a/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-a.yaml b/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-a.yaml
new file mode 100644
index 00000000..7d9d70e0
--- /dev/null
+++ b/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-a.yaml
@@ -0,0 +1,6 @@
+region: europe-west3
+ip_cidr_range: 10.0.0.0/24
+description: Sample Subnet in project project-prod-a, vpc-alpha
+secondary_ip_ranges:
+ secondary-range-a: 192.168.0.0/24
+ secondary-range-b: 192.168.1.0/24
diff --git a/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-b.yaml b/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-b.yaml
new file mode 100644
index 00000000..216fd01e
--- /dev/null
+++ b/factories/example-environments/prod/project-prod-a/vpc-alpha/subnet-alpha-b.yaml
@@ -0,0 +1,4 @@
+region: europe-west3
+ip_cidr_range: 10.0.1.0/24
+description: Sample Subnet in project project-prod-a, vpc-alpha
+private_ip_google_access: false
diff --git a/factories/firewall-hierarchical-policies/README.md b/factories/firewall-hierarchical-policies/README.md
new file mode 100644
index 00000000..b3adbb19
--- /dev/null
+++ b/factories/firewall-hierarchical-policies/README.md
@@ -0,0 +1,161 @@
+# Google Cloud Resource Factories - Hierarchical Firewall Policies
+
+This module implements a resource factory which allows the creation and management of [hierarchical firewall policies](https://cloud.google.com/vpc/docs/firewall-policies) through properly formatted `yaml` files.
+
+`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which allows for simple grouping of policies by Organization ID.
+
+This module also allows for a definition of variable templates, allowing for the definition and centralization of common CIDRs or Service Account lists, which enables re-using them across different policies.
+
+## Example
+
+### Terraform code
+
+```hcl
+module "hierarchical" {
+ source = "./modules/resource-factories/hierarchical-firewall"
+ config_folder = "firewall/hierarchical"
+ templates_folder = "firewall/templates"
+}
+# tftest:skip
+```
+
+### Configuration Structure
+
+The naming convention for the `config_folder` folder requires
+
+- the first directory layer to be named after the organization ID we're creating the policies for
+- each file to be either named either `$folder_id-$description.yaml` (e.g. `1234567890-sharedinfra.yaml`) for policies applying to regular folders or `org.yaml` for the root folder.
+
+Organizations and folders should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
+
+The optional `templates_folder` folder can have two files.
+
+- `cidrs.yaml` - a YAML map defining lists of CIDRs
+- `service_accounts.yaml` - a YAML map definint lists of Service Accounts
+
+Examples for both files are shown in the following section.
+
+```bash
+└── firewall
+ ├── hierarchical
+ │ ├── 31415926535
+ │ │ ├── 1234567890-sharedinfra.yaml # Maps to folders/1234567890
+ │ │ └── org.yaml # Maps to organizations/31415926535
+ │ └── 27182818284
+ │ └── 1234567891-sharedinfra.yaml # Maps to folders/1234567891
+ └── templates
+ ├── cidrs.yaml
+ └── service_accounts.yaml
+```
+
+### Hierarchical firewall policies format and structure
+
+The following syntax applies both for `$folder_id-$description.yaml` and for `org.yaml` files, with the former applying at the `$folder_id` level and the latter at the Organization level.
+
+Each file can contain an arbitrary number of policies.
+
+```yaml
+# Policy name
+allow-icmp:
+ # Description
+ description: Sample policy
+ # Direction {INGRESS, EGRESS}
+ direction: INGRESS
+ # Action {allow, deny}
+ action: allow
+ # Priority (must be unique on a node)
+ priority: 1000
+ # List of CIDRs this rule applies to
+ source_ranges:
+ - 0.0.0.0/0
+ # List of ports this rule applies to (empty array means all ports)
+ ports:
+ tcp: []
+ udp: []
+ icmp: []
+ # List of VPCs this rule applies to - a null value implies all VPCs
+ target_resources: null
+ # Opt - List of target Service Accounts this rule applies to
+ target_service_accounts:
+ - example-service-account@foobar.iam.gserviceaccount.com
+ # Opt - Whether to enable logs - defaults to false
+ enable_logging: true
+```
+
+A sample configuration file might look like the following one:
+
+```yaml
+allow-icmp:
+ description: Enable ICMP for all hosts
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - 0.0.0.0/0
+ ports:
+ icmp: []
+ target_resources: null
+ enable_logging: false
+
+allow-ssh-from-onprem:
+ description: Enable SSH for on prem hosts
+ direction: INGRESS
+ action: allow
+ priority: 1001
+ source_ranges:
+ - $onprem
+ ports:
+ tcp: ["22"]
+ target_resources: null
+ enable_logging: false
+
+allow-443-from-clients:
+ description: Enable HTTPS for web clients
+ direction: INGRESS
+ action: allow
+ priority: 1001
+ source_ranges:
+ - $web_clients
+ ports:
+ tcp: ["443"]
+ target_resources: null
+ target_service_accounts:
+ - $web_frontends
+ enable_logging: false
+```
+
+with `firewall/templates/cidrs.yaml` defined as follows:
+
+```yaml
+onprem:
+ - 10.0.0.0/8
+ - 192.168.0.0/16
+
+web_clients:
+ - 172.16.0.0/16
+ - 10.0.10.0/24
+ - 10.0.250.0/24
+```
+
+and `firewall/templates/service_accounts.yaml`:
+
+```yaml
+web_frontends:
+ - web-frontends@project-wf1.iam.gserviceaccount.com
+ - web-frontends@project-wf2.iam.gserviceaccount.com
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| config_folder | Relative path of the folder containing the hierarchical firewall configuration | string
| ✓ | |
+| templates_folder | Relative path of the folder containing the cidr/service account templates | string
| ✓ | |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| hierarchical-firewall-rules | Generated Hierarchical Firewall Rules | |
+
\ No newline at end of file
diff --git a/factories/firewall-hierarchical-policies/main.tf b/factories/firewall-hierarchical-policies/main.tf
new file mode 100644
index 00000000..ec3a6431
--- /dev/null
+++ b/factories/firewall-hierarchical-policies/main.tf
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ cidrs = try({ for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
+ name => cidrs
+ }, {})
+
+ service_accounts = try({ for name, service_accounts in yamldecode(file("${var.templates_folder}/service_accounts.yaml")) :
+ name => service_accounts
+ }, {})
+
+ org_paths = {
+ for file in fileset(var.config_folder, "**/*.yaml") :
+ file => split("/", file)[1] == "org.yaml"
+ ? "organizations/${split("/", file)[0]}"
+ : "folders/${split("-", split("/", file)[1])[0]}"
+ }
+
+ rules = flatten([
+ for file in fileset(var.config_folder, "**/*.yaml") : [
+ for key, ruleset in yamldecode(file("${var.config_folder}/${file}")) :
+ merge(ruleset, {
+ parent_id = local.org_paths[file]
+ name = "${replace(local.org_paths[file], "/", "-")}-${key}"
+ source_ranges = try(ruleset.source_ranges, null) == null ? null : flatten(
+ [for cidr in ruleset.source_ranges :
+ can(regex("^\\$", cidr))
+ ? local.cidrs[trimprefix(cidr, "$")]
+ : [cidr]
+ ])
+ destination_ranges = try(ruleset.destination_ranges, null) == null ? null : flatten(
+ [for cidr in ruleset.destination_ranges :
+ can(regex("^\\$", cidr))
+ ? local.cidrs[trimprefix(cidr, "$")]
+ : [cidr]
+ ])
+ target_service_accounts = try(ruleset.target_service_accounts, null) == null ? null : flatten(
+ [for service_account in ruleset.target_service_accounts :
+ can(regex("^\\$", service_account))
+ ? local.service_accounts[trimprefix(service_account, "$")]
+ : [service_account]
+ ])
+ })
+ ]
+ ])
+}
+
+resource "google_compute_organization_security_policy" "default" {
+ provider = google-beta
+ for_each = { for rule in local.rules : rule.parent_id => rule.name... }
+ display_name = replace("hierarchical-fw-policy-${each.key}", "/", "-")
+ parent = each.key
+}
+
+resource "google_compute_organization_security_policy_rule" "default" {
+ provider = google-beta
+ for_each = { for rule in local.rules : "${rule.parent_id}-${rule.name}" => rule }
+ policy_id = google_compute_organization_security_policy.default[each.value.parent_id].id
+ action = each.value.action
+ direction = each.value.direction
+ priority = each.value.priority
+ target_resources = each.value.target_resources
+ target_service_accounts = each.value.target_service_accounts
+ enable_logging = try(each.value.enable_logging, false)
+ # preview = each.value.preview
+ match {
+ config {
+ src_ip_ranges = each.value.source_ranges
+ dynamic "layer4_config" {
+ for_each = each.value.ports
+ iterator = port
+ content {
+ ip_protocol = port.key
+ ports = port.value
+ }
+ }
+ }
+ }
+}
+
+resource "google_compute_organization_security_policy_association" "default" {
+ provider = google-beta
+ for_each = { for rule in local.rules : rule.parent_id => rule.name... }
+ name = replace("hierarchical-fw-policy-${each.key}", "/", "-")
+ attachment_id = google_compute_organization_security_policy.default[each.key].parent
+ policy_id = google_compute_organization_security_policy.default[each.key].id
+}
diff --git a/factories/firewall-hierarchical-policies/outputs.tf b/factories/firewall-hierarchical-policies/outputs.tf
new file mode 100644
index 00000000..25062aaf
--- /dev/null
+++ b/factories/firewall-hierarchical-policies/outputs.tf
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+output "hierarchical-firewall-rules" {
+ description = "Generated Hierarchical Firewall Rules"
+ value = {
+ for k, v in google_compute_organization_security_policy_rule.default :
+ k => {
+ parent_id = split("-", k)[0]
+ id = v.id
+ description = v.match[0].description
+ }
+ }
+}
diff --git a/factories/firewall-hierarchical-policies/variables.tf b/factories/firewall-hierarchical-policies/variables.tf
new file mode 100644
index 00000000..4b686f8d
--- /dev/null
+++ b/factories/firewall-hierarchical-policies/variables.tf
@@ -0,0 +1,9 @@
+variable "config_folder" {
+ description = "Relative path of the folder containing the hierarchical firewall configuration"
+ type = string
+}
+
+variable "templates_folder" {
+ description = "Relative path of the folder containing the cidr/service account templates"
+ type = string
+}
diff --git a/factories/firewall-vpc-rules/README.md b/factories/firewall-vpc-rules/README.md
new file mode 100644
index 00000000..7d33b561
--- /dev/null
+++ b/factories/firewall-vpc-rules/README.md
@@ -0,0 +1,6 @@
+# Google Cloud VPC Firewall Factories
+
+This collection of modules implement two different metodologies for the creation of VPC firewall rules, both based on leveraging well-defined `yaml` configuration files.
+
+- The [flat module](flat/) delegates the definition of all firewall rules metadata (project, network amongst other) to the individual `yaml` configuration. This module allows for the maximum flexibility, and a custom logical grouping of resources which can be trasversal to the traditional resources hierarchy, and could be useful in scenarios where network is not managed centrally by a single team.
+- The [nested module](nested/) requires and enforces a semantical folder structure that carries some of the rules metadata (project and network), and leaves the rest to each `yaml` configuration. This solution allows for the definition of a resource hierarchy that is aligned with the organisational resource structure.
\ No newline at end of file
diff --git a/modules/net-vpc-firewall-yaml/README.md b/factories/firewall-vpc-rules/flat/README.md
similarity index 95%
rename from modules/net-vpc-firewall-yaml/README.md
rename to factories/firewall-vpc-rules/flat/README.md
index 2d4ab90e..c13c5f86 100644
--- a/modules/net-vpc-firewall-yaml/README.md
+++ b/factories/firewall-vpc-rules/flat/README.md
@@ -1,10 +1,10 @@
-# Google Cloud VPC Firewall - Yaml
+# Google Cloud VPC Firewall Factory - Flat hierarchy
This module allows creation and management of different types of firewall rules by defining them in well formatted `yaml` files.
Yaml abstraction for FW rules can simplify users onboarding and also makes rules definition simpler and clearer comparing to HCL.
-Nested folder structure for yaml configurations is supported, which allows better and structured code management for multiple teams and environments.
+Nested folder structure for yaml configurations is optionally supported, which allows better and structured code management for multiple teams and environments.
## Example
diff --git a/modules/net-vpc-firewall-yaml/main.tf b/factories/firewall-vpc-rules/flat/main.tf
similarity index 100%
rename from modules/net-vpc-firewall-yaml/main.tf
rename to factories/firewall-vpc-rules/flat/main.tf
diff --git a/modules/net-vpc-firewall-yaml/outputs.tf b/factories/firewall-vpc-rules/flat/outputs.tf
similarity index 100%
rename from modules/net-vpc-firewall-yaml/outputs.tf
rename to factories/firewall-vpc-rules/flat/outputs.tf
diff --git a/modules/net-vpc-firewall-yaml/variables.tf b/factories/firewall-vpc-rules/flat/variables.tf
similarity index 100%
rename from modules/net-vpc-firewall-yaml/variables.tf
rename to factories/firewall-vpc-rules/flat/variables.tf
diff --git a/modules/net-vpc-firewall-yaml/versions.tf b/factories/firewall-vpc-rules/flat/versions.tf
similarity index 100%
rename from modules/net-vpc-firewall-yaml/versions.tf
rename to factories/firewall-vpc-rules/flat/versions.tf
diff --git a/factories/firewall-vpc-rules/nested/README.md b/factories/firewall-vpc-rules/nested/README.md
new file mode 100644
index 00000000..47fba3b1
--- /dev/null
+++ b/factories/firewall-vpc-rules/nested/README.md
@@ -0,0 +1,140 @@
+# Google Cloud VPC Firewall Factory - Nested hierarchy
+
+This module implements a resource factory which allows the creation and management of [VPC firewall rules](https://cloud.google.com/vpc/docs/firewalls) through properly formatted `yaml` files.
+
+`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which represents and forces the resource hierarchy a firewall rule belongs to (Project > VPC > Firewall Rule).
+
+This module also allows for a definition of variable templates, allowing for the definition and centralization of common CIDRs or Service Account lists, which enables re-using them across different policies.
+
+## Example
+
+### Terraform code
+
+```hcl
+module "vpc-firewall" {
+ source = "../../cloud-foundation-fabric/modules/resource-factories/vpc-firewall"
+ config_folder = "firewall/vpc"
+ templates_folder = "firewall/templates"
+}
+
+# tftest:skip
+```
+
+### Configuration Structure
+
+The naming convention for the `config_folder` folder **requires**
+
+- the first directory layer to be named after the project ID which contains the VPC we're creating the firewall rules for
+- the second directory layer to be named after the VPC we're creating the firewall rules for
+- `yaml` files contained in the "VPC" directory can be arbitrarily named, to allow for an easier logical grouping.
+
+Projects and VPCs should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
+
+The optional `templates_folder` folder can have two files.
+
+- `cidrs.yaml` - a YAML map defining lists of CIDRs
+- `service_accounts.yaml` - a YAML map definint lists of Service Accounts
+
+```bash
+└── firewall
+ ├── vpc
+ │ ├── project-resource-factory-dev
+ │ │ └── vpc-resource-factory-dev-one
+ │ │ │ ├── frontend.yaml
+ │ │ │ └── backend.yaml
+ │ │ └── vpc-resource-factory-dev-two
+ │ │ ├── foo.yaml
+ │ │ └── bar.yaml
+ │ └── project-resource-factory-prod
+ │ │ └── vpc-resource-factory-prod-alpha
+ │ │ ├── lorem.yaml
+ │ │ └── ipsum.yaml
+ └── templates
+ ├── cidrs.yaml
+ └── service_accounts.yaml
+```
+
+### Rule definition format and structure
+
+Firewall rules configuration should be placed in a set of yaml files in a folder/s. Firewall rule entry structure is following:
+
+```yaml
+rule-name: # descriptive name, naming convention is adjusted by the module
+ description: "Allow icmp" # rule description
+ action: allow # `allow` or `deny`
+ direction: INGRESS # EGRESS or INGRESS
+ ports:
+ icmp: [] # {tcp, udp, icmp, all}: [ports], use [] for any port
+ priority: 1000 # rule priority value, default value is 1000
+ source_ranges: # list of source ranges
+ - 0.0.0.0/0
+ destination_ranges: # list of destination ranges
+ - 0.0.0.0/0
+ source_tags: ['some-tag'] # list of source tags
+ source_service_accounts: # list of source service accounts
+ - myapp@myproject-id.iam.gserviceaccount.com
+ target_tags: ['some-tag'] # list of target tags
+ target_service_accounts: # list of target service accounts
+ - myapp@myproject-id.iam.gserviceaccount.com
+ enable_logging: true # `false` or `true`, logging is enabled when `true`
+```
+
+A sample configuration file might look like the following one:
+
+```yaml
+allow-healthchecks:
+ description: "Allow traffic from healthcheck"
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - $healthcheck
+ ports:
+ tcp: ["80"]
+ enable_logging: false
+
+allow-http:
+ description: "Allow traffic to LB backend"
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - 0.0.0.0/0
+ target_service_accounts:
+ - $web_frontends
+ ports:
+ tcp: ["80", "443"]
+ enable_logging: false
+
+```
+
+with `firewall/templates/cidrs.yaml` defined as follows:
+
+```yaml
+healthcheck:
+ - 35.191.0.0/16
+ - 130.211.0.0/22
+```
+
+and `firewall/templates/service_accounts.yaml`:
+
+```yaml
+web_frontends:
+ - web-frontends@project-wf1.iam.gserviceaccount.com
+ - web-frontends@project-wf2.iam.gserviceaccount.com
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| config_folder | Relative path of the folder containing the hierarchical firewall configuration | string
| ✓ | |
+| templates_folder | Relative path of the folder containing the cidr/service account templates | string
| ✓ | |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| vpc-firewall-rules | Generated VPC Firewall Rules | |
+
\ No newline at end of file
diff --git a/factories/firewall-vpc-rules/nested/main.tf b/factories/firewall-vpc-rules/nested/main.tf
new file mode 100644
index 00000000..1c966eda
--- /dev/null
+++ b/factories/firewall-vpc-rules/nested/main.tf
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+locals {
+ cidrs = try({ for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
+ name => cidrs
+ }, {})
+
+ service_accounts = try({ for name, service_accounts in yamldecode(file("${var.templates_folder}/service_accounts.yaml")) :
+ name => service_accounts
+ }, {})
+ rules = flatten([
+ for file in fileset(var.config_folder, "**/*.yaml") : [
+ for key, ruleset in yamldecode(file("${var.config_folder}/${file}")) :
+ merge(ruleset, {
+ project_id = split("/", file)[0]
+ network = split("/", file)[1]
+ name = "${key}-${split("/", file)[1]}"
+
+ source_ranges = try(ruleset.source_ranges, null) == null ? null : flatten(
+ [for cidr in ruleset.source_ranges :
+ can(regex("^\\$", cidr))
+ ? local.cidrs[trimprefix(cidr, "$")]
+ : [cidr]
+ ])
+ destination_ranges = try(ruleset.destination_ranges, null) == null ? null : flatten(
+ [for cidr in ruleset.destination_ranges :
+ can(regex("^\\$", cidr))
+ ? local.cidrs[trimprefix(cidr, "$")]
+ : [cidr]
+ ])
+ source_service_accounts = try(ruleset.source_service_accounts, null) == null ? null : flatten(
+ [for service_account in ruleset.source_service_accounts :
+ can(regex("^\\$", service_account))
+ ? local.service_accounts[trimprefix(service_account, "$")]
+ : [service_account]
+ ])
+ target_service_accounts = try(ruleset.target_service_accounts, null) == null ? null : flatten(
+ [for service_account in ruleset.target_service_accounts :
+ can(regex("^\\$", service_account))
+ ? local.service_accounts[trimprefix(service_account, "$")]
+ : [service_account]
+ ])
+ })
+ ]
+ ])
+
+ rules_allow = [for item in local.rules : item if item.action == "allow"]
+ rules_deny = [for item in local.rules : item if item.action == "deny"]
+
+}
+
+
+resource "google_compute_firewall" "rules-allow" {
+ for_each = { for rule in local.rules_allow : "${rule.network}-${rule.name}" => rule }
+ project = each.value.project_id
+ name = each.value.name
+ description = each.value.description
+ network = each.value.network
+ direction = each.value.direction
+ priority = each.value.priority
+
+ source_ranges = try(each.value.source_ranges, each.value.direction == "INGRESS" ? [] : null)
+ source_tags = try(each.value.source_tags, null)
+ source_service_accounts = try(each.value.source_service_accounts, null)
+
+ destination_ranges = try(each.value.destination_ranges, each.value.direction == "EGRESS" ? [] : null)
+ target_tags = try(each.value.target_tags, null)
+ target_service_accounts = try(each.value.target_service_accounts, null)
+
+ dynamic "allow" {
+ for_each = { for proto, ports in try(each.value.ports, []) :
+ "${proto}-${join("-", ports)}" => {
+ ports = [for port in ports : tostring(port)]
+ protocol = proto
+ }
+ }
+ content {
+ protocol = allow.value.protocol
+ ports = allow.value.ports
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = (each.value.enable_logging == null) || (each.value.enable_logging == false) ? [] : [""]
+ content {
+ metadata = "INCLUDE_ALL_METADATA"
+ }
+ }
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+
+
+resource "google_compute_firewall" "rules-deny" {
+ for_each = { for rule in local.rules_deny : "${rule.network}-${rule.name}" => rule }
+ project = each.value.project_id
+ name = each.value.name
+ description = each.value.description
+ network = each.value.network
+ direction = each.value.direction
+ priority = each.value.priority
+
+ source_ranges = try(each.value.source_ranges, each.value.direction == "INGRESS" ? [] : null)
+ source_tags = try(each.value.source_tags, null)
+ source_service_accounts = try(each.value.source_service_accounts, null)
+
+ destination_ranges = try(each.value.destination_ranges, each.value.direction == "EGRESS" ? [] : null)
+ target_tags = try(each.value.target_tags, null)
+ target_service_accounts = try(each.value.target_service_accounts, null)
+
+ dynamic "deny" {
+ for_each = { for proto, ports in try(each.value.ports, []) :
+ "${proto}-${join("-", ports)}" => {
+ ports = [for port in ports : tostring(port)]
+ protocol = proto
+ }
+ }
+ content {
+ protocol = deny.value.protocol
+ ports = deny.value.ports
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = (each.value.enable_logging == null) || (each.value.enable_logging == false) ? [] : [""]
+ content {
+ metadata = "INCLUDE_ALL_METADATA"
+ }
+ }
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
diff --git a/factories/firewall-vpc-rules/nested/outputs.tf b/factories/firewall-vpc-rules/nested/outputs.tf
new file mode 100644
index 00000000..041a8b8e
--- /dev/null
+++ b/factories/firewall-vpc-rules/nested/outputs.tf
@@ -0,0 +1,4 @@
+output "vpc-firewall-rules" {
+ description = "Generated VPC Firewall Rules"
+ value = merge(google_compute_firewall.rules-allow, google_compute_firewall.rules-deny)
+}
diff --git a/factories/firewall-vpc-rules/nested/variables.tf b/factories/firewall-vpc-rules/nested/variables.tf
new file mode 100644
index 00000000..4b686f8d
--- /dev/null
+++ b/factories/firewall-vpc-rules/nested/variables.tf
@@ -0,0 +1,9 @@
+variable "config_folder" {
+ description = "Relative path of the folder containing the hierarchical firewall configuration"
+ type = string
+}
+
+variable "templates_folder" {
+ description = "Relative path of the folder containing the cidr/service account templates"
+ type = string
+}
diff --git a/factories/subnets/README.md b/factories/subnets/README.md
new file mode 100644
index 00000000..07cec90a
--- /dev/null
+++ b/factories/subnets/README.md
@@ -0,0 +1,68 @@
+# Google Cloud Resource Factories - VPC Subnets
+
+This module implements a resource factory which allows the creation and management of subnets through properly formatted `yaml` files.
+
+`yaml` configurations are stored on a well-defined folder structure, whose entry point can be customized, and which allows for simple grouping of subnets by Project > VPC.
+
+## Example
+
+### Terraform code
+
+```hcl
+module "subnets" {
+ source = "./modules/resource-factories/subnets"
+ config_folder = "subnets"
+}
+# tftest:skip
+```
+
+### Configuration Structure
+
+The directory structure implies the project and the VPC each subnet belongs to.
+Per the structure below, a subnet named `subnet-a` (after filename `subnet-a.yaml`) will be created on VPC `vpc-alpha-one` which belongs to project `project-alpha`.
+
+Projects and VPCs should exist prior to running this module, or set as an explicit dependency to this module, leveraging `depends_on`.
+
+```bash
+└── subnets
+ ├── project-alpha
+ │ ├── vpc-alpha-one
+ │ │ ├── subnet-a.yaml
+ │ │ └── subnet-b.yaml
+ │ └── vpc-alpha-two
+ │ └── subnet-c.yaml
+ └── project-bravo
+ └── vpc-bravo-one
+ └── subnet-d.yaml
+```
+
+### Subnet definition format and structure
+
+```yaml
+region: europe-west1 # Region where the subnet will be creted
+description: Sample description # Description
+ip_cidr_range: 10.0.0.0/24 # Primary IP range for the subnet
+private_ip_google_access: false # Opt- Enables PGA. Defaults to true
+iam_users: ["foobar@example.com"] # Opt- Users to grant compute/networkUser to
+iam_groups: ["lorem@example.com"] # Opt- Groups to grant compute/networkUser to
+iam_service_accounts: ["foobar@project-id.iam.gserviceaccount.com"]
+ # Opt- SAs to grant compute/networkUser to
+secondary_ip_ranges: # Opt- List of secondary IP ranges
+ - name: secondary-range-a # Name for the secondary range
+ ip_cidr_range: 192.168.0.0/24 # IP range for the secondary range
+
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| config_folder | Relative path of the folder containing the subnet configuration | string
| ✓ | |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| subnet | Generated subnets | |
+
diff --git a/factories/subnets/main.tf b/factories/subnets/main.tf
new file mode 100644
index 00000000..0bed8407
--- /dev/null
+++ b/factories/subnets/main.tf
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ _data = {
+ for f in fileset(var.config_folder, "**/*.yaml") :
+ trimsuffix(split("/", f)[2], ".yaml") => merge(
+ yamldecode(file("${var.config_folder}/${f}")),
+ {
+ project_id = split("/", f)[0]
+ network = split("/", f)[1]
+ }
+ )
+ }
+
+ data = {
+ for k, v in local._data : k => merge(v,
+ {
+ network_users : concat(
+ formatlist("group:%s", try(v.iam_groups, [])),
+ formatlist("user:%s", try(v.iam_users, [])),
+ formatlist("serviceAccount:%s", try(v.iam_service_accounts, []))
+ )
+ }
+ )
+ }
+}
+
+resource "google_compute_subnetwork" "default" {
+ for_each = local.data
+ project = each.value.project_id
+ network = each.value.network
+ name = each.key
+ region = each.value.region
+ description = each.value.description
+ ip_cidr_range = each.value.ip_cidr_range
+ private_ip_google_access = try(each.value.private_ip_google_access, true)
+
+ dynamic "secondary_ip_range" {
+ for_each = try(each.value.secondary_ip_ranges, [])
+ iterator = secondary_range
+ content {
+ range_name = secondary_range.key
+ ip_cidr_range = secondary_range.value
+ }
+ }
+}
+
+
+resource "google_compute_subnetwork_iam_binding" "default" {
+ for_each = {
+ for k, v in local.data : k => v if length(v.network_users) > 0
+ }
+ project = each.value.project_id
+ subnetwork = google_compute_subnetwork.default[each.key].name
+ region = each.value.region
+ role = "roles/compute.networkUser"
+ members = each.value.network_users
+}
diff --git a/factories/subnets/outputs.tf b/factories/subnets/outputs.tf
new file mode 100644
index 00000000..e3032c10
--- /dev/null
+++ b/factories/subnets/outputs.tf
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+output "subnet" {
+ description = "Generated subnets"
+ value = {
+ for k, v in google_compute_subnetwork.default :
+ k => {
+ network = v.network
+ project = v.project
+ range = v.ip_cidr_range
+ region = v.region
+ }
+ }
+}
diff --git a/factories/subnets/variables.tf b/factories/subnets/variables.tf
new file mode 100644
index 00000000..fbea28bb
--- /dev/null
+++ b/factories/subnets/variables.tf
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "config_folder" {
+ description = "Relative path of the folder containing the subnet configuration"
+ type = string
+}
diff --git a/tests/modules/net_vpc_firewall_yaml/__init__.py b/tests/factories/__init__.py
similarity index 100%
rename from tests/modules/net_vpc_firewall_yaml/__init__.py
rename to tests/factories/__init__.py
diff --git a/tests/factories/firewall_hierarchical_policies/__init__.py b/tests/factories/firewall_hierarchical_policies/__init__.py
new file mode 100644
index 00000000..d46dbae5
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/0987654321-foobar.yaml b/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/0987654321-foobar.yaml
new file mode 100644
index 00000000..539694a5
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/0987654321-foobar.yaml
@@ -0,0 +1,11 @@
+allow-ssh-from-onprem:
+ description: Enable SSH for onprem ranges
+ direction: INGRESS
+ action: allow
+ priority: 1001
+ source_ranges:
+ - $example
+ ports:
+ tcp: ["22"]
+ target_resources: null
+ enable_logging: false
diff --git a/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/org.yaml b/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/org.yaml
new file mode 100644
index 00000000..2183fc05
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/fixture/conf/rules/1234567890/org.yaml
@@ -0,0 +1,11 @@
+allow-icmp:
+ description: Enable ICMP for all hosts
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - 0.0.0.0/0
+ ports:
+ icmp: []
+ target_resources: null
+ enable_logging: false
diff --git a/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/cidrs.yaml b/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/cidrs.yaml
new file mode 100644
index 00000000..4f4e158f
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/cidrs.yaml
@@ -0,0 +1,8 @@
+example:
+ - 10.0.0.0/24
+ - 10.0.10.0/24
+ - 192.168.1.1/32
+
+healthcheck:
+ - 35.191.0.0/16
+ - 130.211.0.0/22
\ No newline at end of file
diff --git a/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/service_accounts.yaml b/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/service_accounts.yaml
new file mode 100644
index 00000000..697be919
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/fixture/conf/templates/service_accounts.yaml
@@ -0,0 +1,2 @@
+example:
+ - example-service-account@resource-factory-playground.iam.gserviceaccount.com
diff --git a/tests/factories/firewall_hierarchical_policies/fixture/main.tf b/tests/factories/firewall_hierarchical_policies/fixture/main.tf
new file mode 100644
index 00000000..9e171641
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/fixture/main.tf
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2021 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 "hierarchical-firewall-rules" {
+ source = "../../../../factories/firewall-hierarchical-policies/"
+ config_folder = "conf/rules"
+ templates_folder = "conf/templates"
+}
diff --git a/tests/factories/firewall_hierarchical_policies/test_plan.py b/tests/factories/firewall_hierarchical_policies/test_plan.py
new file mode 100644
index 00000000..c66519bc
--- /dev/null
+++ b/tests/factories/firewall_hierarchical_policies/test_plan.py
@@ -0,0 +1,43 @@
+# Copyright 2021 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 os
+import pytest
+
+
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
+
+
+def test_firewall(plan_runner):
+ "Test hierarchical firewall rules from conf/rules"
+ _, resources = plan_runner(FIXTURES_DIR)
+ assert len(resources) == 6
+ assert set(r["type"] for r in resources) == set([
+ "google_compute_organization_security_policy_rule", "google_compute_organization_security_policy_association", "google_compute_organization_security_policy"
+ ])
+ rule_ssh = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_rule" and r["values"]["priority"]==1001]
+ rule_icmp = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_rule" and r["values"]["priority"]==1000]
+ association_org = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_association" and r["values"]["attachment_id"]=="organizations/1234567890"]
+ association_folder = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy_association" and r["values"]["attachment_id"]=="folders/0987654321"]
+ policies_org = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy" and r["values"]["parent"]=="organizations/1234567890"]
+ policies_folder = [r["values"] for r in resources if r["type"]== "google_compute_organization_security_policy" and r["values"]["parent"]=="folders/0987654321"]
+
+ assert set(rule_ssh[0]["match"][0]["config"][0]["src_ip_ranges"])==set(["10.0.0.0/24", "10.0.10.0/24", "192.168.1.1/32"])
+ assert rule_icmp[0]["match"][0]["config"][0]["layer4_config"][0]["ip_protocol"]=="icmp"
+ assert association_org[0]["name"]=="hierarchical-fw-policy-organizations-1234567890"
+ assert association_folder[0]["name"]=="hierarchical-fw-policy-folders-0987654321"
+ assert policies_org[0]["display_name"]=="hierarchical-fw-policy-organizations-1234567890"
+ assert policies_folder[0]["display_name"]=="hierarchical-fw-policy-folders-0987654321"
+
\ No newline at end of file
diff --git a/tests/factories/subnets/__init__.py b/tests/factories/subnets/__init__.py
new file mode 100644
index 00000000..d46dbae5
--- /dev/null
+++ b/tests/factories/subnets/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-a.yaml b/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-a.yaml
new file mode 100644
index 00000000..39a26145
--- /dev/null
+++ b/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-a.yaml
@@ -0,0 +1,6 @@
+region: europe-west1
+ip_cidr_range: 10.0.0.0/24
+description: Sample Subnet in project project-a, vpc-a
+secondary_ip_ranges:
+ secondary-range-a: 192.168.0.0/24
+ secondary-range-b: 192.168.1.0/24
diff --git a/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-b.yaml b/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-b.yaml
new file mode 100644
index 00000000..212bf5ea
--- /dev/null
+++ b/tests/factories/subnets/fixture/conf/project-a/vpc-a/subnet-b.yaml
@@ -0,0 +1,4 @@
+region: europe-west3
+ip_cidr_range: 10.0.1.0/24
+description: Sample Subnet in project project-a, vpc-a
+private_ip_google_access: false
diff --git a/tests/factories/subnets/fixture/conf/project-a/vpc-b/subnet-one.yaml b/tests/factories/subnets/fixture/conf/project-a/vpc-b/subnet-one.yaml
new file mode 100644
index 00000000..321557a3
--- /dev/null
+++ b/tests/factories/subnets/fixture/conf/project-a/vpc-b/subnet-one.yaml
@@ -0,0 +1,5 @@
+region: europe-west4
+ip_cidr_range: 10.0.2.0/24
+description: Sample Subnet in project project-a, vpc-b
+iam_users: ["sruffilli@google.com"]
+iam_groups: []
diff --git a/tests/factories/subnets/fixture/conf/project-b/vpc-x/subnet-alpha.yaml b/tests/factories/subnets/fixture/conf/project-b/vpc-x/subnet-alpha.yaml
new file mode 100644
index 00000000..c451b0f8
--- /dev/null
+++ b/tests/factories/subnets/fixture/conf/project-b/vpc-x/subnet-alpha.yaml
@@ -0,0 +1,5 @@
+region: europe-west4
+ip_cidr_range: 172.16.0.0/24
+description: Sample Subnet in project project-b, vpc-x
+iam_users: ["sruffilli@google.com"]
+iam_groups: []
diff --git a/tests/factories/subnets/fixture/main.tf b/tests/factories/subnets/fixture/main.tf
new file mode 100644
index 00000000..815bac66
--- /dev/null
+++ b/tests/factories/subnets/fixture/main.tf
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2021 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 "subnets" {
+ source = "../../../../factories/subnets"
+ config_folder = "conf"
+}
\ No newline at end of file
diff --git a/tests/factories/subnets/test_plan.py b/tests/factories/subnets/test_plan.py
new file mode 100644
index 00000000..abd5ad25
--- /dev/null
+++ b/tests/factories/subnets/test_plan.py
@@ -0,0 +1,64 @@
+# Copyright 2021 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 os
+import pytest
+
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
+
+
+def test_firewall(plan_runner):
+ "Test hierarchical firewall rules from conf/rules"
+ _, resources = plan_runner(FIXTURES_DIR)
+ assert len(resources) == 6
+ assert set(r["type"] for r in resources) == set(
+ ["google_compute_subnetwork", "google_compute_subnetwork_iam_binding"])
+ subnets = [
+ r["values"] for r in resources
+ if r["type"] == "google_compute_subnetwork"
+ ]
+ iam_bindings = [
+ r["values"] for r in resources
+ if r["type"] == "google_compute_subnetwork_iam_binding"
+ ]
+
+ subnet_a_a = [
+ s for s in subnets
+ if s["project"] == "project-a" and s["network"] == "vpc-a" and s["name"] == "subnet-a"
+ ][0]
+ assert subnet_a_a["ip_cidr_range"] == "10.0.0.0/24"
+ assert subnet_a_a["private_ip_google_access"] == True
+ assert subnet_a_a["region"] == "europe-west1"
+ assert subnet_a_a["secondary_ip_range"] == [{
+ "ip_cidr_range":
+ "192.168.0.0/24",
+ "range_name":
+ "secondary-range-a"
+ }, {
+ "ip_cidr_range":
+ "192.168.1.0/24",
+ "range_name":
+ "secondary-range-b"
+ }]
+
+ subnet_a_b = [
+ s for s in subnets
+ if s["project"] == "project-a" and s["network"] == "vpc-a" and s["name"] == "subnet-b"
+ ][0]
+ assert subnet_a_b["private_ip_google_access"] == False
+
+ iam_binding_b_alpha = [b for b in iam_bindings if b["project"]=="project-b"][0]
+ assert set(iam_binding_b_alpha["members"])==set(["user:sruffilli@google.com"])
+ assert iam_binding_b_alpha["role"]=="roles/compute.networkUser"
+ assert iam_binding_b_alpha["subnetwork"]=="subnet-alpha"
\ No newline at end of file
diff --git a/tests/factories/vpc_firewall/__init__.py b/tests/factories/vpc_firewall/__init__.py
new file mode 100644
index 00000000..d46dbae5
--- /dev/null
+++ b/tests/factories/vpc_firewall/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/tests/factories/vpc_firewall/flat/__init__.py b/tests/factories/vpc_firewall/flat/__init__.py
new file mode 100644
index 00000000..d46dbae5
--- /dev/null
+++ b/tests/factories/vpc_firewall/flat/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/tests/modules/net_vpc_firewall_yaml/fixture/main.tf b/tests/factories/vpc_firewall/flat/fixture/main.tf
similarity index 91%
rename from tests/modules/net_vpc_firewall_yaml/fixture/main.tf
rename to tests/factories/vpc_firewall/flat/fixture/main.tf
index 4dcc9b7c..2fca3a87 100644
--- a/tests/modules/net_vpc_firewall_yaml/fixture/main.tf
+++ b/tests/factories/vpc_firewall/flat/fixture/main.tf
@@ -15,7 +15,7 @@
*/
module "firewall" {
- source = "../../../../modules/net-vpc-firewall-yaml"
+ source = "../../../../../factories/firewall-vpc-rules/flat"
project_id = "my-project"
network = "my-network"
config_directories = [
diff --git a/tests/modules/net_vpc_firewall_yaml/fixture/rules/common.yaml b/tests/factories/vpc_firewall/flat/fixture/rules/common.yaml
similarity index 100%
rename from tests/modules/net_vpc_firewall_yaml/fixture/rules/common.yaml
rename to tests/factories/vpc_firewall/flat/fixture/rules/common.yaml
diff --git a/tests/modules/net_vpc_firewall_yaml/fixture/variables.tf b/tests/factories/vpc_firewall/flat/fixture/variables.tf
similarity index 100%
rename from tests/modules/net_vpc_firewall_yaml/fixture/variables.tf
rename to tests/factories/vpc_firewall/flat/fixture/variables.tf
diff --git a/tests/modules/net_vpc_firewall_yaml/test_plan.py b/tests/factories/vpc_firewall/flat/test_plan.py
similarity index 100%
rename from tests/modules/net_vpc_firewall_yaml/test_plan.py
rename to tests/factories/vpc_firewall/flat/test_plan.py
diff --git a/tests/factories/vpc_firewall/nested/__init__.py b/tests/factories/vpc_firewall/nested/__init__.py
new file mode 100644
index 00000000..d46dbae5
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/tests/factories/vpc_firewall/nested/fixture/conf/rules/resource-factory-playground/vpc-a/ingress-loadbalancers.yaml b/tests/factories/vpc_firewall/nested/fixture/conf/rules/resource-factory-playground/vpc-a/ingress-loadbalancers.yaml
new file mode 100644
index 00000000..c71b9066
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/fixture/conf/rules/resource-factory-playground/vpc-a/ingress-loadbalancers.yaml
@@ -0,0 +1,23 @@
+allow-healthchecks:
+ description: "Allow traffic from healthcheck"
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - $healthcheck
+ ports:
+ tcp: ["80"]
+ enable_logging: false
+
+allow-http:
+ description: "Allow traffic to LB backend"
+ direction: INGRESS
+ action: allow
+ priority: 1000
+ source_ranges:
+ - 0.0.0.0/0
+ target_service_accounts:
+ - example-service-account@resource-factory-playground.iam.gserviceaccount.com
+ ports:
+ tcp: ["80", "443"]
+ enable_logging: true
diff --git a/tests/factories/vpc_firewall/nested/fixture/conf/templates/cidrs.yaml b/tests/factories/vpc_firewall/nested/fixture/conf/templates/cidrs.yaml
new file mode 100644
index 00000000..4f4e158f
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/fixture/conf/templates/cidrs.yaml
@@ -0,0 +1,8 @@
+example:
+ - 10.0.0.0/24
+ - 10.0.10.0/24
+ - 192.168.1.1/32
+
+healthcheck:
+ - 35.191.0.0/16
+ - 130.211.0.0/22
\ No newline at end of file
diff --git a/tests/factories/vpc_firewall/nested/fixture/conf/templates/service_accounts.yaml b/tests/factories/vpc_firewall/nested/fixture/conf/templates/service_accounts.yaml
new file mode 100644
index 00000000..b4dd9237
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/fixture/conf/templates/service_accounts.yaml
@@ -0,0 +1,2 @@
+couchbase:
+ - example-service-account@resource-factory-playground.iam.gserviceaccount.com
diff --git a/tests/factories/vpc_firewall/nested/fixture/main.tf b/tests/factories/vpc_firewall/nested/fixture/main.tf
new file mode 100644
index 00000000..d50ebb79
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/fixture/main.tf
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2021 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 "vpc-firewall-rules" {
+ source = "../../../../../factories/firewall-vpc-rules/nested"
+ config_folder = "conf/rules"
+ templates_folder = "conf/templates"
+}
diff --git a/tests/factories/vpc_firewall/nested/test_plan.py b/tests/factories/vpc_firewall/nested/test_plan.py
new file mode 100644
index 00000000..c3cce2c2
--- /dev/null
+++ b/tests/factories/vpc_firewall/nested/test_plan.py
@@ -0,0 +1,45 @@
+# Copyright 2021 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 os
+import pytest
+
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixture")
+
+
+def test_firewall(plan_runner):
+ "Test hierarchical firewall rules from conf/rules"
+ _, resources = plan_runner(FIXTURES_DIR)
+ assert len(resources) == 2
+
+ assert set(r["type"]
+ for r in resources) == set(["google_compute_firewall"])
+
+ rule_hc = [
+ r["values"] for r in resources
+ if r["values"]["name"] == "allow-healthchecks-vpc-a"
+ ][0]
+ rule_be = [
+ r["values"] for r in resources
+ if r["values"]["description"] == "Allow traffic to LB backend"
+ ][0]
+
+ assert set(rule_hc["source_ranges"]) == set(
+ ["130.211.0.0/22", "35.191.0.0/16"])
+ assert rule_hc["direction"] == "INGRESS"
+ assert rule_hc["network"] == "vpc-a"
+ assert rule_hc["priority"] == 1000
+ assert rule_hc["project"] == "resource-factory-playground"
+ assert rule_hc["allow"][0] == {'ports': ['80'], 'protocol': 'tcp'}
+ assert rule_be["log_config"][0] == {'metadata': 'INCLUDE_ALL_METADATA'}