diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md
index ee0d63a3..2bdeebe9 100644
--- a/modules/vpc-sc/README.md
+++ b/modules/vpc-sc/README.md
@@ -6,6 +6,23 @@ Given the complexity of the underlying resources, the module intentionally mimic
If you are using [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) with Terraform and run into permissions issues, make sure to check out the recommended provider configuration in the [VPC SC resources documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_access_level).
+
+- [Examples](#examples)
+ - [Access policy](#access-policy)
+ - [Scoped policy](#scoped-policy)
+ - [Access policy IAM](#access-policy-iam)
+ - [Access levels](#access-levels)
+ - [Service perimeters](#service-perimeters)
+ - [Bridge type](#bridge-type)
+ - [Regular type](#regular-type)
+- [Factories](#factories)
+- [Notes](#notes)
+- [TODO](#todo)
+- [Files](#files)
+- [Variables](#variables)
+- [Outputs](#outputs)
+
+
## Examples
### Access policy
@@ -194,6 +211,83 @@ module "test" {
# tftest modules=1 resources=3 inventory=regular.yaml
```
+## Factories
+
+This module implements support for three distinct factories, used to create and manage access levels, egress policies and ingress policies via YAML files. The YAML files syntax is a 1:1 match for the corresponding variables, and the factory data is merged at runtime with any data set in variables, which take precedence in case of key overlaps.
+
+This is an example that uses all three factories. Note that the factory configuration points to folders, where each file represents one resource.
+
+```hcl
+module "test" {
+ source = "./fabric/modules/vpc-sc"
+ access_policy = "12345678"
+ factories_config = {
+ access_levels = "data/access-levels"
+ egress_policies = "data/egress-policies"
+ ingress_policies = "data/ingress-policies"
+ }
+ service_perimeters_regular = {
+ r1 = {
+ status = {
+ access_levels = ["geo-it", "identity-user1"]
+ resources = ["projects/11111", "projects/111111"]
+ restricted_services = ["storage.googleapis.com"]
+ egress_policies = ["gcs-sa-foo"]
+ ingress_policies = ["sa-tf-test"]
+ vpc_accessible_services = {
+ allowed_services = ["storage.googleapis.com"]
+ enable_restriction = true
+ }
+ }
+ }
+ }
+}
+# tftest modules=1 resources=3 files=a1,a2,e1,i1 inventory=factory.yaml
+```
+
+```yaml
+conditions:
+ - members:
+ - user:user1@example.com
+# tftest-file id=a1 path=data/access-levels/identity-user1.yaml
+```
+
+```yaml
+conditions:
+ - regions:
+ - IT
+# tftest-file id=a2 path=data/access-levels/geo-it.yaml
+```
+
+```yaml
+from:
+ identities:
+ - serviceAccount:foo@myproject.iam.gserviceaccount.com
+to:
+ operations:
+ - method_selectors:
+ - "*"
+ service_name: storage.googleapis.com
+ resources:
+ - projects/123456789
+
+# tftest-file id=e1 path=data/egress-policies/gcs-sa-foo.yaml
+```
+
+```yaml
+from:
+ access_levels:
+ - "*"
+ identities:
+ - serviceAccount:test-tf@myproject.iam.gserviceaccount.com
+to:
+ operations:
+ - service_name: "*"
+ resources:
+ - "*"
+# tftest-file id=i1 path=data/ingress-policies/sa-tf-test.yaml
+```
+
## Notes
- To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again.
@@ -209,6 +303,7 @@ module "test" {
| name | description | resources |
|---|---|---|
| [access-levels.tf](./access-levels.tf) | Access level resources. | google_access_context_manager_access_level
|
+| [factory.tf](./factory.tf) | None | |
| [iam.tf](./iam.tf) | IAM bindings | google_access_context_manager_access_policy_iam_binding
ยท google_access_context_manager_access_policy_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | google_access_context_manager_access_policy
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
@@ -225,12 +320,13 @@ module "test" {
| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…}))
| | {}
|
| [access_policy_create](variables.tf#L61) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format. | object({…})
| | null
|
| [egress_policies](variables.tf#L71) | Egress policy definitions that can be referenced in perimeters. | map(object({…}))
| | {}
|
-| [iam](variables.tf#L102) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_bindings](variables.tf#L108) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [iam_bindings_additive](variables.tf#L123) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [ingress_policies](variables.tf#L138) | Ingress policy definitions that can be referenced in perimeters. | map(object({…}))
| | {}
|
-| [service_perimeters_bridge](variables.tf#L170) | Bridge service perimeters. | map(object({…}))
| | {}
|
-| [service_perimeters_regular](variables.tf#L180) | Regular service perimeters. | map(object({…}))
| | {}
|
+| [factories_config](variables.tf#L102) | Paths to folders that enable factory functionality. | object({…})
| | {}
|
+| [iam](variables.tf#L113) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_bindings](variables.tf#L119) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L134) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [ingress_policies](variables.tf#L149) | Ingress policy definitions that can be referenced in perimeters. | map(object({…}))
| | {}
|
+| [service_perimeters_bridge](variables.tf#L181) | Bridge service perimeters. | map(object({…}))
| | {}
|
+| [service_perimeters_regular](variables.tf#L191) | Regular service perimeters. | map(object({…}))
| | {}
|
## Outputs
diff --git a/modules/vpc-sc/access-levels.tf b/modules/vpc-sc/access-levels.tf
index 1eb85343..c00288e1 100644
--- a/modules/vpc-sc/access-levels.tf
+++ b/modules/vpc-sc/access-levels.tf
@@ -21,7 +21,7 @@
# google_access_context_manager_access_levels resource
resource "google_access_context_manager_access_level" "basic" {
- for_each = var.access_levels
+ for_each = merge(local.data.access_levels, var.access_levels)
parent = "accessPolicies/${local.access_policy}"
name = "accessPolicies/${local.access_policy}/accessLevels/${each.key}"
title = each.key
diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf
new file mode 100644
index 00000000..eca5867a
--- /dev/null
+++ b/modules/vpc-sc/factory.tf
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ _data = {
+ for k, v in local._data_paths : k => {
+ for f in try(fileset(v, "**/*.yaml"), []) :
+ trimsuffix(f, ".yaml") => yamldecode(file("${v}/${f}"))
+ }
+ }
+ _data_paths = {
+ for k in ["access_levels", "egress_policies", "ingress_policies"] : k => (
+ var.factories_config[k] == null
+ ? null
+ : pathexpand(var.factories_config[k])
+ )
+ }
+ data = {
+ access_levels = {
+ for k, v in local._data.access_levels : k => {
+ combining_function = try(v.combining_function, null)
+ description = try(v.description, null)
+ conditions = [
+ for c in try(v.conditions, []) : merge({
+ device_policy = null
+ ip_subnetworks = []
+ members = []
+ negate = null
+ regions = []
+ required_access_levels = []
+ }, c)
+ ]
+ }
+ }
+ egress_policies = {
+ for k, v in local._data.egress_policies : k => {
+ from = merge({
+ identity_type = null
+ identities = null
+ }, try(v.from, {}))
+ to = {
+ operations = [
+ for o in try(v.to.operations, []) : merge({
+ method_selectors = []
+ permission_selectors = []
+ service_name = null
+ }, o)
+ ]
+ resources = try(v.to.resources, [])
+ resource_type_external = try(v.to.resource_type_external, false)
+ }
+ }
+ }
+ ingress_policies = {
+ for k, v in local._data.ingress_policies : k => {
+ from = merge({
+ access_levels = []
+ identity_type = null
+ identities = null
+ resources = []
+ }, try(v.from, {}))
+ to = {
+ operations = [
+ for o in try(v.operations, []) : merge({
+ method_selectors = []
+ permission_selectors = []
+ service_name = null
+ }, o)
+ ]
+ resources = try(v.to.resources, [])
+ }
+ }
+ }
+ }
+ # TODO: add checks that emulate the variable validations
+}
diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf
index 6742a1c2..a646df3c 100644
--- a/modules/vpc-sc/service-perimeters-regular.tf
+++ b/modules/vpc-sc/service-perimeters-regular.tf
@@ -20,6 +20,11 @@
# service perimeters are needed, switch to the
# google_access_context_manager_service_perimeters resource
+locals {
+ egress_policies = merge(local.data.egress_policies, var.egress_policies)
+ ingress_policies = merge(local.data.ingress_policies, var.ingress_policies)
+}
+
resource "google_access_context_manager_service_perimeter" "regular" {
for_each = var.service_perimeters_regular
parent = "accessPolicies/${local.access_policy}"
@@ -43,8 +48,8 @@ resource "google_access_context_manager_service_perimeter" "regular" {
dynamic "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)
+ k => lookup(local.egress_policies, k, null)
+ if contains(keys(local.egress_policies), k)
}
iterator = policy
content {
@@ -86,8 +91,8 @@ resource "google_access_context_manager_service_perimeter" "regular" {
dynamic "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)
+ k => lookup(local.ingress_policies, k, null)
+ if contains(keys(local.ingress_policies), k)
}
iterator = policy
content {
@@ -167,8 +172,8 @@ resource "google_access_context_manager_service_perimeter" "regular" {
dynamic "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)
+ k => lookup(local.egress_policies, k, null)
+ if contains(keys(local.egress_policies), k)
}
iterator = policy
content {
@@ -210,8 +215,8 @@ resource "google_access_context_manager_service_perimeter" "regular" {
dynamic "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)
+ k => lookup(local.ingress_policies, k, null)
+ if contains(keys(local.ingress_policies), k)
}
iterator = policy
content {
diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf
index e852ed1c..c1c16063 100644
--- a/modules/vpc-sc/variables.tf
+++ b/modules/vpc-sc/variables.tf
@@ -99,6 +99,17 @@ variable "egress_policies" {
}
}
+variable "factories_config" {
+ description = "Paths to folders that enable factory functionality."
+ type = object({
+ access_levels = optional(string)
+ egress_policies = optional(string)
+ ingress_policies = optional(string)
+ })
+ nullable = false
+ default = {}
+}
+
variable "iam" {
description = "IAM bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml
new file mode 100644
index 00000000..475c4d1e
--- /dev/null
+++ b/tests/modules/vpc_sc/examples/factory.yaml
@@ -0,0 +1,106 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+values:
+ module.test.google_access_context_manager_access_level.basic["geo-it"]:
+ basic:
+ - combining_function: AND
+ conditions:
+ - device_policy: []
+ ip_subnetworks: []
+ members: []
+ negate: null
+ regions:
+ - IT
+ required_access_levels: []
+ vpc_network_sources: []
+ custom: []
+ description: null
+ name: accessPolicies/12345678/accessLevels/geo-it
+ parent: accessPolicies/12345678
+ timeouts: null
+ title: geo-it
+ module.test.google_access_context_manager_access_level.basic["identity-user1"]:
+ basic:
+ - combining_function: AND
+ conditions:
+ - device_policy: []
+ ip_subnetworks: []
+ members:
+ - user:user1@example.com
+ negate: null
+ regions: []
+ required_access_levels: []
+ vpc_network_sources: []
+ custom: []
+ description: null
+ name: accessPolicies/12345678/accessLevels/identity-user1
+ parent: accessPolicies/12345678
+ timeouts: null
+ title: identity-user1
+ module.test.google_access_context_manager_service_perimeter.regular["r1"]:
+ description: null
+ name: accessPolicies/12345678/servicePerimeters/r1
+ parent: accessPolicies/12345678
+ perimeter_type: PERIMETER_TYPE_REGULAR
+ spec: []
+ status:
+ - egress_policies:
+ - egress_from:
+ - identities:
+ - serviceAccount:foo@myproject.iam.gserviceaccount.com
+ identity_type: null
+ source_restriction: null
+ sources: []
+ egress_to:
+ - external_resources: null
+ operations:
+ - method_selectors:
+ - method: '*'
+ permission: null
+ service_name: storage.googleapis.com
+ resources:
+ - projects/123456789
+ ingress_policies:
+ - ingress_from:
+ - identities:
+ - serviceAccount:test-tf@myproject.iam.gserviceaccount.com
+ identity_type: null
+ sources:
+ - access_level: '*'
+ resource: null
+ ingress_to:
+ - operations: []
+ resources:
+ - '*'
+ resources:
+ - projects/11111
+ - projects/111111
+ restricted_services:
+ - storage.googleapis.com
+ vpc_accessible_services:
+ - allowed_services:
+ - storage.googleapis.com
+ enable_restriction: true
+ timeouts: null
+ title: r1
+ use_explicit_dry_run_spec: false
+
+counts:
+ google_access_context_manager_access_level: 2
+ google_access_context_manager_service_perimeter: 1
+ modules: 1
+ resources: 3
+
+outputs: {}