diff --git a/blueprints/data-solutions/shielded-folder/main.tf b/blueprints/data-solutions/shielded-folder/main.tf
index 5d1145cd1b..f4424a52f5 100644
--- a/blueprints/data-solutions/shielded-folder/main.tf
+++ b/blueprints/data-solutions/shielded-folder/main.tf
@@ -71,13 +71,15 @@ locals {
}
module "folder" {
- source = "../../../modules/folder"
- folder_create = var.folder_config.folder_create != null
- parent = try(var.folder_config.folder_create.parent, null)
- name = try(var.folder_config.folder_create.display_name, null)
- id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id
- group_iam = local.group_iam
- org_policies_data_path = var.data_dir != null ? "${var.data_dir}/org-policies" : null
+ source = "../../../modules/folder"
+ folder_create = var.folder_config.folder_create != null
+ parent = try(var.folder_config.folder_create.parent, null)
+ name = try(var.folder_config.folder_create.display_name, null)
+ id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id
+ group_iam = local.group_iam
+ factories_config = {
+ org_policies = var.data_dir != null ? "${var.data_dir}/org-policies" : null
+ }
logging_sinks = var.enable_features.log_sink ? {
for name, attrs in var.log_sinks : name => {
bq_partitioned_table = attrs.type == "bigquery"
diff --git a/fast/stages-multitenant/1-resman-tenant/README.md b/fast/stages-multitenant/1-resman-tenant/README.md
index c38f173f49..200e414067 100644
--- a/fast/stages-multitenant/1-resman-tenant/README.md
+++ b/fast/stages-multitenant/1-resman-tenant/README.md
@@ -155,21 +155,21 @@ Once the configuration is done just go through the usual `init/apply` cycle. On
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…})
| ✓ | | 0-bootstrap
|
| [billing_account](variables.tf#L52) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…})
| ✓ | | 0-bootstrap
|
-| [organization](variables.tf#L205) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
-| [prefix](variables.tf#L227) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
-| [root_node](variables.tf#L237) | Root folder node for the tenant, in folders/nnnnnn format. | string
| ✓ | | |
-| [short_name](variables.tf#L242) | Short name used to identify the tenant. | string
| ✓ | | |
-| [tags](variables.tf#L247) | Resource management tags. | object({…})
| ✓ | | |
+| [organization](variables.tf#L214) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
+| [prefix](variables.tf#L230) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
+| [root_node](variables.tf#L240) | Root folder node for the tenant, in folders/nnnnnn format. | string
| ✓ | | |
+| [short_name](variables.tf#L245) | Short name used to identify the tenant. | string
| ✓ | | |
+| [tags](variables.tf#L250) | Resource management tags. | object({…})
| ✓ | | |
| [cicd_repositories](variables.tf#L63) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…})
| | null
| |
| [custom_roles](variables.tf#L145) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 0-bootstrap
|
| [data_dir](variables.tf#L154) | Relative path for the folder storing configuration data. | string
| | "data"
| |
-| [fast_features](variables.tf#L160) | Selective control for top-level FAST features. | object({…})
| | {}
| 0-0-bootstrap
|
-| [groups](variables.tf#L174) | Group names to grant organization-level permissions. | object({…})
| | {}
| 0-bootstrap
|
-| [locations](variables.tf#L187) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| 0-bootstrap
|
-| [organization_policy_data_path](variables.tf#L215) | Path for the data folder used by the organization policies factory. | string
| | null
| |
-| [outputs_location](variables.tf#L221) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
-| [team_folders](variables.tf#L265) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
-| [test_skip_data_sources](variables.tf#L275) | Used when testing to bypass data sources. | bool
| | false
| |
+| [factories_config](variables.tf#L160) | Configuration for the organization policies factory. | object({…})
| | {}
| |
+| [fast_features](variables.tf#L169) | Selective control for top-level FAST features. | object({…})
| | {}
| 0-0-bootstrap
|
+| [groups](variables.tf#L183) | Group names to grant organization-level permissions. | object({…})
| | {}
| 0-bootstrap
|
+| [locations](variables.tf#L196) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
| 0-bootstrap
|
+| [outputs_location](variables.tf#L224) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
+| [team_folders](variables.tf#L268) | Team folders to be created. Format is described in a code comment. | map(object({…}))
| | null
| |
+| [test_skip_data_sources](variables.tf#L278) | Used when testing to bypass data sources. | bool
| | false
| |
## Outputs
diff --git a/fast/stages-multitenant/1-resman-tenant/root_node.tf b/fast/stages-multitenant/1-resman-tenant/root_node.tf
index 7b02a60923..6c920c9e21 100644
--- a/fast/stages-multitenant/1-resman-tenant/root_node.tf
+++ b/fast/stages-multitenant/1-resman-tenant/root_node.tf
@@ -26,6 +26,9 @@ module "root-folder" {
)
name = var.test_skip_data_sources ? "Test" : null
# end test attributes
+ factories_config = {
+ org_policy = var.factories_config.org_policy
+ }
iam_bindings_additive = {
sa_net_fw_policy_admin = {
member = local.automation_sas_iam.networking
@@ -40,5 +43,4 @@ module "root-folder" {
role = "roles/accesscontextmanager.policyAdmin"
}
}
- org_policies_data_path = var.organization_policy_data_path
}
diff --git a/fast/stages-multitenant/1-resman-tenant/variables.tf b/fast/stages-multitenant/1-resman-tenant/variables.tf
index bcda7fd629..d216a6e63a 100644
--- a/fast/stages-multitenant/1-resman-tenant/variables.tf
+++ b/fast/stages-multitenant/1-resman-tenant/variables.tf
@@ -157,6 +157,15 @@ variable "data_dir" {
default = "data"
}
+variable "factories_config" {
+ description = "Configuration for the organization policies factory."
+ type = object({
+ org_policy = optional(string, "data/org-policies")
+ })
+ nullable = false
+ default = {}
+}
+
variable "fast_features" {
# tfdoc:variable:source 0-0-bootstrap
description = "Selective control for top-level FAST features."
@@ -212,12 +221,6 @@ variable "organization" {
})
}
-variable "organization_policy_data_path" {
- description = "Path for the data folder used by the organization policies factory."
- type = string
- default = null
-}
-
variable "outputs_location" {
description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable."
type = string
diff --git a/fast/stages/0-bootstrap/README.md b/fast/stages/0-bootstrap/README.md
index 3ceec1eced..c10b2bf321 100644
--- a/fast/stages/0-bootstrap/README.md
+++ b/fast/stages/0-bootstrap/README.md
@@ -594,37 +594,36 @@ The `fast_features` variable consists of 4 toggles:
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to `false`. To disable handling of billing IAM roles set `no_iam` to `true`. | object({…})
| ✓ | | |
-| [organization](variables.tf#L248) | Organization details. | object({…})
| ✓ | | |
-| [prefix](variables.tf#L263) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | |
+| [organization](variables.tf#L235) | Organization details. | object({…})
| ✓ | | |
+| [prefix](variables.tf#L250) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | |
| [bootstrap_user](variables.tf#L27) | Email of the nominal user running this stage for the first time. | string
| | null
| |
| [cicd_repositories](variables.tf#L33) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…})
| | null
| |
-| [custom_role_names](variables.tf#L79) | Names of custom roles defined at the org level. | object({…})
| | {…}
| |
-| [custom_roles](variables.tf#L93) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string))
| | {}
| |
-| [factories_config](variables.tf#L100) | Configuration for the organization policies factory. | object({…})
| | {}
| |
-| [fast_features](variables.tf#L109) | Selective control for top-level FAST features. | object({…})
| | {}
| |
-| [federated_identity_providers](variables.tf#L122) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…}))
| | {}
| |
-| [group_iam](variables.tf#L142) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
| |
-| [groups](variables.tf#L149) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | map(string)
| | {…}
| |
-| [iam](variables.tf#L167) | Organization-level custom IAM settings in role => [principal] format. | map(list(string))
| | {}
| |
-| [iam_bindings_additive](variables.tf#L174) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
| |
-| [locations](variables.tf#L189) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {}
| |
-| [log_sinks](variables.tf#L203) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
| |
-| [org_policies_config](variables.tf#L232) | Organization policies customization. | object({…})
| | {}
| |
-| [outputs_location](variables.tf#L257) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
-| [project_parent_ids](variables.tf#L272) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…})
| | {…}
| |
+| [custom_roles](variables.tf#L79) | Map of role names => list of permissions to additionally create at the organization level. | map(list(string))
| | {}
| |
+| [factories_config](variables.tf#L86) | Configuration for the organization policies factory. | object({…})
| | {}
| |
+| [fast_features](variables.tf#L96) | Selective control for top-level FAST features. | object({…})
| | {}
| |
+| [federated_identity_providers](variables.tf#L109) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…}))
| | {}
| |
+| [group_iam](variables.tf#L129) | Organization-level authoritative IAM binding for groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
| |
+| [groups](variables.tf#L136) | Group names or emails to grant organization-level permissions. If just the name is provided, the default organization domain is assumed. | map(string)
| | {…}
| |
+| [iam](variables.tf#L154) | Organization-level custom IAM settings in role => [principal] format. | map(list(string))
| | {}
| |
+| [iam_bindings_additive](variables.tf#L161) | Organization-level custom additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
| |
+| [locations](variables.tf#L176) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {}
| |
+| [log_sinks](variables.tf#L190) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
| |
+| [org_policies_config](variables.tf#L219) | Organization policies customization. | object({…})
| | {}
| |
+| [outputs_location](variables.tf#L244) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string
| | null
| |
+| [project_parent_ids](variables.tf#L259) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…})
| | {…}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [automation](outputs.tf#L112) | Automation resources. | | |
-| [billing_dataset](outputs.tf#L117) | BigQuery dataset prepared for billing export. | | |
-| [cicd_repositories](outputs.tf#L122) | CI/CD repository configurations. | | |
-| [custom_roles](outputs.tf#L134) | Organization-level custom roles. | | |
-| [federated_identity](outputs.tf#L139) | Workload Identity Federation pool and providers. | | |
-| [outputs_bucket](outputs.tf#L149) | GCS bucket where generated output files are stored. | | |
-| [project_ids](outputs.tf#L154) | Projects created by this stage. | | |
-| [providers](outputs.tf#L164) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01
|
-| [service_accounts](outputs.tf#L171) | Automation service accounts created by this stage. | | |
-| [tfvars](outputs.tf#L180) | Terraform variable files for the following stages. | ✓ | |
+| [automation](outputs.tf#L102) | Automation resources. | | |
+| [billing_dataset](outputs.tf#L107) | BigQuery dataset prepared for billing export. | | |
+| [cicd_repositories](outputs.tf#L112) | CI/CD repository configurations. | | |
+| [custom_roles](outputs.tf#L124) | Organization-level custom roles. | | |
+| [federated_identity](outputs.tf#L129) | Workload Identity Federation pool and providers. | | |
+| [outputs_bucket](outputs.tf#L139) | GCS bucket where generated output files are stored. | | |
+| [project_ids](outputs.tf#L144) | Projects created by this stage. | | |
+| [providers](outputs.tf#L154) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01
|
+| [service_accounts](outputs.tf#L161) | Automation service accounts created by this stage. | | |
+| [tfvars](outputs.tf#L170) | Terraform variable files for the following stages. | ✓ | |
diff --git a/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml
new file mode 100644
index 0000000000..9ce8ac5c0e
--- /dev/null
+++ b/fast/stages/0-bootstrap/data/custom-roles/organization_iam_admin.yaml
@@ -0,0 +1,20 @@
+# Copyright 2023 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.
+
+# this is needed for use in additive IAM bindings, to avoid conflicts
+name: organizationIamAdmin
+includedPermissions:
+ - resourcemanager.organizations.get
+ - resourcemanager.organizations.getIamPolicy
+ - resourcemanager.organizations.setIamPolicy
diff --git a/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml
new file mode 100644
index 0000000000..a0eaf9b3b9
--- /dev/null
+++ b/fast/stages/0-bootstrap/data/custom-roles/service_project_network_admin.yaml
@@ -0,0 +1,31 @@
+# Copyright 2023 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.
+
+name: serviceProjectNetworkAdmin
+includedPermissions:
+ - compute.globalOperations.get
+ # compute.networks.updatePeering and compute.networks.get are
+ # used by automation service accounts who manage service
+ # projects where peering creation might be needed (e.g. GKE). If
+ # you remove them your network administrators should create
+ # peerings for service projects
+ - compute.networks.updatePeering
+ - compute.networks.get
+ - compute.organizations.disableXpnResource
+ - compute.organizations.enableXpnResource
+ - compute.projects.get
+ - compute.subnetworks.getIamPolicy
+ - compute.subnetworks.setIamPolicy
+ - dns.networks.bindPrivateDNSZone
+ - resourcemanager.projects.get
diff --git a/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml b/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml
new file mode 100644
index 0000000000..9d1e02822f
--- /dev/null
+++ b/fast/stages/0-bootstrap/data/custom-roles/tenant_network_admin.yaml
@@ -0,0 +1,17 @@
+# Copyright 2023 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.
+
+name: tenantNetworkAdmin
+includedPermissions:
+ - compute.globalOperations.get
diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf
index 12f6b44590..bd1e6c2852 100644
--- a/fast/stages/0-bootstrap/organization.tf
+++ b/fast/stages/0-bootstrap/organization.tf
@@ -95,7 +95,7 @@ module "organization" {
iam_bindings = {
organization_iam_admin_conditional = {
members = [module.automation-tf-resman-sa.iam_email]
- role = module.organization.custom_role_id[var.custom_role_names.organization_iam_admin]
+ role = module.organization.custom_role_id["organization_iam_admin"]
condition = {
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
@@ -106,7 +106,7 @@ module "organization" {
"roles/compute.xpnAdmin",
"roles/orgpolicy.policyAdmin",
"roles/resourcemanager.organizationViewer",
- module.organization.custom_role_id[var.custom_role_names.tenant_network_admin]
+ module.organization.custom_role_id["tenant_network_admin"]
],
local.billing_mode == "org" ? [
"roles/billing.admin",
@@ -120,34 +120,13 @@ module "organization" {
}
}
}
- custom_roles = merge(var.custom_roles, {
- # this is needed for use in additive IAM bindings, to avoid conflicts
- (var.custom_role_names.organization_iam_admin) = [
- "resourcemanager.organizations.get",
- "resourcemanager.organizations.getIamPolicy",
- "resourcemanager.organizations.setIamPolicy"
- ]
- (var.custom_role_names.service_project_network_admin) = [
- "compute.globalOperations.get",
- # compute.networks.updatePeering and compute.networks.get are
- # used by automation service accounts who manage service
- # projects where peering creation might be needed (e.g. GKE). If
- # you remove them your network administrators should create
- # peerings for service projects
- "compute.networks.updatePeering",
- "compute.networks.get",
- "compute.organizations.disableXpnResource",
- "compute.organizations.enableXpnResource",
- "compute.projects.get",
- "compute.subnetworks.getIamPolicy",
- "compute.subnetworks.setIamPolicy",
- "dns.networks.bindPrivateDNSZone",
- "resourcemanager.projects.get",
- ]
- (var.custom_role_names.tenant_network_admin) = [
- "compute.globalOperations.get",
- ]
- })
+ custom_roles = var.custom_roles
+ factories_config = {
+ custom_roles = var.factories_config.custom_roles
+ org_policies = (
+ var.bootstrap_user != null ? null : var.factories_config.org_policy
+ )
+ }
logging_sinks = {
for name, attrs in var.log_sinks : name => {
bq_partitioned_table = attrs.type == "bigquery"
@@ -156,11 +135,6 @@ module "organization" {
type = attrs.type
}
}
- org_policies_data_path = (
- var.bootstrap_user != null
- ? null
- : var.factories_config.org_policy_data_path
- )
org_policies = var.bootstrap_user != null ? {} : {
"iam.allowedPolicyMemberDomains" = {
rules = [
diff --git a/fast/stages/0-bootstrap/outputs.tf b/fast/stages/0-bootstrap/outputs.tf
index 58b8217938..4724a40918 100644
--- a/fast/stages/0-bootstrap/outputs.tf
+++ b/fast/stages/0-bootstrap/outputs.tf
@@ -38,16 +38,6 @@ locals {
}
)
}
- custom_roles = merge(
- {
- for k, v in var.custom_role_names :
- k => try(module.organization.custom_role_id[v], "")
- },
- {
- for k, v in var.custom_roles :
- k => try(module.organization.custom_role_id[k], "")
- }
- )
providers = {
"0-bootstrap" = templatefile(local._tpl_providers, {
backend_extra = null
@@ -82,7 +72,7 @@ locals {
project_id = module.automation-project.project_id
project_number = module.automation-project.number
}
- custom_roles = local.custom_roles
+ custom_roles = module.organization.custom_role_id
logging = {
project_id = module.log-export-project.project_id
project_number = module.log-export-project.number
@@ -133,7 +123,7 @@ output "cicd_repositories" {
output "custom_roles" {
description = "Organization-level custom roles."
- value = local.custom_roles
+ value = module.organization.custom_role_id
}
output "federated_identity" {
diff --git a/fast/stages/0-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf
index af171b3417..9980c11893 100644
--- a/fast/stages/0-bootstrap/variables.tf
+++ b/fast/stages/0-bootstrap/variables.tf
@@ -76,20 +76,6 @@ variable "cicd_repositories" {
}
}
-variable "custom_role_names" {
- description = "Names of custom roles defined at the org level."
- type = object({
- organization_iam_admin = string
- service_project_network_admin = string
- tenant_network_admin = string
- })
- default = {
- organization_iam_admin = "organizationIamAdmin"
- service_project_network_admin = "serviceProjectNetworkAdmin"
- tenant_network_admin = "tenantNetworkAdmin"
- }
-}
-
variable "custom_roles" {
description = "Map of role names => list of permissions to additionally create at the organization level."
type = map(list(string))
@@ -100,7 +86,8 @@ variable "custom_roles" {
variable "factories_config" {
description = "Configuration for the organization policies factory."
type = object({
- org_policy_data_path = optional(string, "data/org-policies")
+ custom_roles = optional(string, "data/custom-roles")
+ org_policy = optional(string, "data/org-policies")
})
nullable = false
default = {}
diff --git a/modules/folder/README.md b/modules/folder/README.md
index 059fe39955..fb45549514 100644
--- a/modules/folder/README.md
+++ b/modules/folder/README.md
@@ -122,10 +122,12 @@ The example below deploys a few organization policies split between two YAML fil
```hcl
module "folder" {
- source = "./fabric/modules/folder"
- parent = var.folder_id
- name = "Folder name"
- org_policies_data_path = "configs/org-policies/"
+ source = "./fabric/modules/folder"
+ parent = var.folder_id
+ name = "Folder name"
+ factories_config = {
+ org_policies = "configs/org-policies/"
+ }
}
# tftest modules=1 resources=8 files=boolean,list inventory=org-policies.yaml e2e
```
@@ -198,6 +200,7 @@ module "folder" {
}
# tftest modules=2 resources=3 e2e serial
```
+
## Log Sinks
```hcl
@@ -340,21 +343,21 @@ module "folder" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string))
| | {}
|
-| [firewall_policy](variables.tf#L24) | Hierarchical firewall policy to associate to this folder. | object({…})
| | null
|
-| [folder_create](variables.tf#L33) | Create folder. When set to false, uses id to reference an existing folder. | bool
| | true
|
-| [group_iam](variables.tf#L39) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
-| [iam](variables.tf#L46) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_bindings](variables.tf#L53) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [iam_bindings_additive](variables.tf#L68) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [id](variables.tf#L83) | Folder ID in case you use folder_create=false. | string
| | null
|
-| [logging_data_access](variables.tf#L89) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
-| [logging_exclusions](variables.tf#L104) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string)
| | {}
|
-| [logging_sinks](variables.tf#L111) | Logging sinks to create for the folder. | map(object({…}))
| | {}
|
-| [name](variables.tf#L142) | Folder name. | string
| | null
|
-| [org_policies](variables.tf#L148) | Organization policies applied to this folder keyed by policy name. | map(object({…}))
| | {}
|
-| [org_policies_data_path](variables.tf#L175) | Path containing org policies in YAML format. | string
| | null
|
-| [parent](variables.tf#L181) | Parent in folders/folder_id or organizations/org_id format. | string
| | null
|
-| [tag_bindings](variables.tf#L191) | Tag bindings for this folder, in key => tag value id format. | map(string)
| | null
|
+| [factories_config](variables.tf#L24) | Paths to data files and folders that enable factory functionality. | object({…})
| | {}
|
+| [firewall_policy](variables.tf#L33) | Hierarchical firewall policy to associate to this folder. | object({…})
| | null
|
+| [folder_create](variables.tf#L42) | Create folder. When set to false, uses id to reference an existing folder. | bool
| | true
|
+| [group_iam](variables.tf#L48) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
+| [iam](variables.tf#L55) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_bindings](variables.tf#L62) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L77) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [id](variables.tf#L92) | Folder ID in case you use folder_create=false. | string
| | null
|
+| [logging_data_access](variables.tf#L98) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
+| [logging_exclusions](variables.tf#L113) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string)
| | {}
|
+| [logging_sinks](variables.tf#L120) | Logging sinks to create for the folder. | map(object({…}))
| | {}
|
+| [name](variables.tf#L151) | Folder name. | string
| | null
|
+| [org_policies](variables.tf#L157) | Organization policies applied to this folder keyed by policy name. | map(object({…}))
| | {}
|
+| [parent](variables.tf#L184) | Parent in folders/folder_id or organizations/org_id format. | string
| | null
|
+| [tag_bindings](variables.tf#L194) | Tag bindings for this folder, in key => tag value id format. | map(string)
| | null
|
## Outputs
diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf
index 2bf79c4ab6..38b69871c9 100644
--- a/modules/folder/organization-policies.tf
+++ b/modules/folder/organization-policies.tf
@@ -18,10 +18,9 @@
locals {
_factory_data_raw = merge([
- for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
- yamldecode(file("${var.org_policies_data_path}/${f}"))
+ for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) :
+ yamldecode(file("${var.factories_config.org_policies}/${f}"))
]...)
-
# simulate applying defaults to data coming from yaml files
_factory_data = {
for k, v in local._factory_data_raw :
@@ -49,9 +48,7 @@ locals {
]
}
}
-
_org_policies = merge(local._factory_data, var.org_policies)
-
org_policies = {
for k, v in local._org_policies :
k => merge(v, {
diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf
index 91e8e11e81..be087379fe 100644
--- a/modules/folder/variables.tf
+++ b/modules/folder/variables.tf
@@ -21,6 +21,15 @@ variable "contacts" {
nullable = false
}
+variable "factories_config" {
+ description = "Paths to data files and folders that enable factory functionality."
+ type = object({
+ org_policies = optional(string)
+ })
+ nullable = false
+ default = {}
+}
+
variable "firewall_policy" {
description = "Hierarchical firewall policy to associate to this folder."
type = object({
@@ -172,12 +181,6 @@ variable "org_policies" {
nullable = false
}
-variable "org_policies_data_path" {
- description = "Path containing org policies in YAML format."
- type = string
- default = null
-}
-
variable "parent" {
description = "Parent in folders/folder_id or organizations/org_id format."
type = string
diff --git a/modules/organization/README.md b/modules/organization/README.md
index 1d8232cd38..90ab457450 100644
--- a/modules/organization/README.md
+++ b/modules/organization/README.md
@@ -102,7 +102,6 @@ module "org" {
}
]
}
-
"compute.trustedImageProjects" = {
rules = [{
allow = {
@@ -145,7 +144,6 @@ To manage organization policy custom constraints, the `orgpolicy.googleapis.com`
module "org" {
source = "./fabric/modules/organization"
organization_id = var.organization_id
-
org_policy_custom_constraints = {
"custom.gkeEnableAutoUpgrade" = {
resource_types = ["container.googleapis.com/NodePool"]
@@ -156,7 +154,6 @@ module "org" {
description = "All node pools must have node auto-upgrade enabled."
}
}
-
# not necessarily to enforce on the org level, policy may be applied on folder/project levels
org_policies = {
"custom.gkeEnableAutoUpgrade" = {
@@ -177,9 +174,11 @@ The example below deploys a few org policy custom constraints split between two
```hcl
module "org" {
- source = "./fabric/modules/organization"
- organization_id = var.organization_id
- org_policy_custom_constraints_data_path = "configs/custom-constraints"
+ source = "./fabric/modules/organization"
+ organization_id = var.organization_id
+ factories_config = {
+ org_policy_custom_constraints = "configs/custom-constraints"
+ }
org_policies = {
"custom.gkeEnableAutoUpgrade" = {
rules = [{ enforce = true }]
@@ -373,6 +372,41 @@ module "org" {
# tftest modules=1 resources=2 inventory=roles.yaml e2e serial
```
+Custom roles can also be specified via a factory in a similar way to organization policies and policy constraints. Each file is mapped to a custom role, where
+
+- the role name defaults to the file name but can be overridden via a `name` attribute in the yaml
+- role permissions are defined in an `includedPermissions` map
+
+Custom roles defined via the variable are merged with those coming from the factory, and override them in case of duplicate names.
+
+```hcl
+module "org" {
+ source = "./fabric/modules/organization"
+ organization_id = var.organization_id
+ factories_config = {
+ custom_roles = "data/custom_roles"
+ }
+}
+# tftest modules=1 resources=2 files=custom-role-1,custom-role-2 inventory=custom-roles.yaml
+```
+
+```yaml
+# tftest-file id=custom-role-1 path=data/custom_roles/test_1.yaml
+
+includedPermissions:
+ - compute.globalOperations.get
+```
+
+```yaml
+# tftest-file id=custom-role-2 path=data/custom_roles/test_2.yaml
+
+name: projectViewer
+includedPermissions:
+ - resourcemanager.projects.get
+ - resourcemanager.projects.getIamPolicy
+ - resourcemanager.projects.list
+```
+
## Tags
Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage.
@@ -453,24 +487,23 @@ module "org" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [organization_id](variables.tf#L212) | Organization id in organizations/nnnnnn format. | string
| ✓ | |
+| [organization_id](variables.tf#L211) | Organization id in organizations/nnnnnn format. | string
| ✓ | |
| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string))
| | {}
|
| [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string))
| | {}
|
-| [firewall_policy](variables.tf#L31) | Hierarchical firewall policies to associate to the organization. | object({…})
| | null
|
-| [group_iam](variables.tf#L40) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
-| [iam](variables.tf#L47) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_bindings](variables.tf#L54) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [iam_bindings_additive](variables.tf#L69) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [logging_data_access](variables.tf#L84) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
-| [logging_exclusions](variables.tf#L99) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string)
| | {}
|
-| [logging_sinks](variables.tf#L106) | Logging sinks to create for the organization. | map(object({…}))
| | {}
|
-| [network_tags](variables.tf#L137) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | {}
|
-| [org_policies](variables.tf#L159) | Organization policies applied to this organization keyed by policy name. | map(object({…}))
| | {}
|
-| [org_policies_data_path](variables.tf#L186) | Path containing org policies in YAML format. | string
| | null
|
-| [org_policy_custom_constraints](variables.tf#L192) | Organization policy custom constraints keyed by constraint name. | map(object({…}))
| | {}
|
-| [org_policy_custom_constraints_data_path](variables.tf#L206) | Path containing org policy custom constraints in YAML format. | string
| | null
|
-| [tag_bindings](variables.tf#L221) | Tag bindings for this organization, in key => tag value id format. | map(string)
| | null
|
-| [tags](variables.tf#L227) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | {}
|
+| [factories_config](variables.tf#L31) | Paths to data files and folders that enable factory functionality. | object({…})
| | {}
|
+| [firewall_policy](variables.tf#L42) | Hierarchical firewall policies to associate to the organization. | object({…})
| | null
|
+| [group_iam](variables.tf#L51) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
+| [iam](variables.tf#L58) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_bindings](variables.tf#L65) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L80) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [logging_data_access](variables.tf#L95) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
+| [logging_exclusions](variables.tf#L110) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string)
| | {}
|
+| [logging_sinks](variables.tf#L117) | Logging sinks to create for the organization. | map(object({…}))
| | {}
|
+| [network_tags](variables.tf#L148) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | {}
|
+| [org_policies](variables.tf#L170) | Organization policies applied to this organization keyed by policy name. | map(object({…}))
| | {}
|
+| [org_policy_custom_constraints](variables.tf#L197) | Organization policy custom constraints keyed by constraint name. | map(object({…}))
| | {}
|
+| [tag_bindings](variables.tf#L220) | Tag bindings for this organization, in key => tag value id format. | map(string)
| | null
|
+| [tags](variables.tf#L226) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | {}
|
## Outputs
@@ -478,12 +511,12 @@ module "org" {
|---|---|:---:|
| [custom_constraint_ids](outputs.tf#L17) | Map of CUSTOM_CONSTRAINTS => ID in the organization. | |
| [custom_role_id](outputs.tf#L22) | Map of custom role IDs created in the organization. | |
-| [custom_roles](outputs.tf#L35) | Map of custom roles resources created in the organization. | |
-| [id](outputs.tf#L40) | Fully qualified organization id. | |
-| [network_tag_keys](outputs.tf#L57) | Tag key resources. | |
-| [network_tag_values](outputs.tf#L66) | Tag value resources. | |
-| [organization_id](outputs.tf#L76) | Organization id dependent on module resources. | |
-| [sink_writer_identities](outputs.tf#L93) | Writer identities created for each sink. | |
-| [tag_keys](outputs.tf#L101) | Tag key resources. | |
-| [tag_values](outputs.tf#L110) | Tag value resources. | |
+| [custom_roles](outputs.tf#L32) | Map of custom roles resources created in the organization. | |
+| [id](outputs.tf#L37) | Fully qualified organization id. | |
+| [network_tag_keys](outputs.tf#L54) | Tag key resources. | |
+| [network_tag_values](outputs.tf#L63) | Tag value resources. | |
+| [organization_id](outputs.tf#L73) | Organization id dependent on module resources. | |
+| [sink_writer_identities](outputs.tf#L90) | Writer identities created for each sink. | |
+| [tag_keys](outputs.tf#L98) | Tag key resources. | |
+| [tag_values](outputs.tf#L107) | Tag value resources. | |
diff --git a/modules/organization/iam.tf b/modules/organization/iam.tf
index 16a0fa00f9..e0726467f5 100644
--- a/modules/organization/iam.tf
+++ b/modules/organization/iam.tf
@@ -17,12 +17,32 @@
# tfdoc:file:description IAM bindings, roles and audit logging resources.
locals {
+ _custom_roles = {
+ for f in try(fileset(var.factories_config.custom_roles, "*.yaml"), []) :
+ replace(f, ".yaml", "") => yamldecode(
+ file("${var.factories_config.custom_roles}/${f}")
+ )
+ }
_group_iam_roles = distinct(flatten(values(var.group_iam)))
_group_iam = {
for r in local._group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
+ custom_roles = merge(
+ {
+ for k, v in local._custom_roles : k => {
+ name = lookup(v, "name", k)
+ permissions = v["includedPermissions"]
+ }
+ },
+ {
+ for k, v in var.custom_roles : k => {
+ name = k
+ permissions = v
+ }
+ }
+ )
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@@ -32,13 +52,27 @@ locals {
}
}
+# we use a different key for custom roles to allow referring to the role alias
+# in Terraform, while still being able to define unique role names
+
+check "custom_roles" {
+ assert {
+ condition = (
+ length(local.custom_roles) == length({
+ for k, v in local.custom_roles : v.name => null
+ })
+ )
+ error_message = "Duplicate role name in custom roles."
+ }
+}
+
resource "google_organization_iam_custom_role" "roles" {
- for_each = var.custom_roles
+ for_each = local.custom_roles
org_id = local.organization_id_numeric
- role_id = each.key
- title = "Custom role ${each.key}"
+ role_id = each.value.name
+ title = "Custom role ${each.value.name}"
description = "Terraform-managed."
- permissions = each.value
+ permissions = each.value.permissions
}
resource "google_organization_iam_binding" "authoritative" {
diff --git a/modules/organization/org-policy-custom-constraints.tf b/modules/organization/org-policy-custom-constraints.tf
index 6a8cf5e6d4..cae888d1c6 100644
--- a/modules/organization/org-policy-custom-constraints.tf
+++ b/modules/organization/org-policy-custom-constraints.tf
@@ -16,11 +16,9 @@
locals {
_custom_constraints_factory_data_raw = merge([
- for f in try(fileset(var.org_policy_custom_constraints_data_path, "*.yaml"), []) :
- yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}"))
+ for f in try(fileset(var.factories_config.org_policy_custom_constraints, "*.yaml"), []) :
+ yamldecode(file("${var.factories_config.org_policy_custom_constraints}/${f}"))
]...)
-
-
_custom_constraints_factory_data = {
for k, v in local._custom_constraints_factory_data_raw :
k => {
@@ -32,9 +30,10 @@ locals {
resource_types = v.resource_types
}
}
-
- _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints)
-
+ _custom_constraints = merge(
+ local._custom_constraints_factory_data,
+ var.org_policy_custom_constraints
+ )
custom_constraints = {
for k, v in local._custom_constraints :
k => merge(v, {
diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf
index 8d867f668c..2faf2e97e1 100644
--- a/modules/organization/organization-policies.tf
+++ b/modules/organization/organization-policies.tf
@@ -18,10 +18,9 @@
locals {
_factory_data_raw = merge([
- for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
- yamldecode(file("${var.org_policies_data_path}/${f}"))
+ for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) :
+ yamldecode(file("${var.factories_config.org_policies}/${f}"))
]...)
-
# simulate applying defaults to data coming from yaml files
_factory_data = {
for k, v in local._factory_data_raw :
@@ -49,9 +48,7 @@ locals {
]
}
}
-
_org_policies = merge(local._factory_data, var.org_policies)
-
org_policies = {
for k, v in local._org_policies :
k => merge(v, {
diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf
index 12c133e87b..b010b29976 100644
--- a/modules/organization/outputs.tf
+++ b/modules/organization/outputs.tf
@@ -22,14 +22,11 @@ output "custom_constraint_ids" {
output "custom_role_id" {
description = "Map of custom role IDs created in the organization."
value = {
- for role_id, role in google_organization_iam_custom_role.roles :
+ for k, v in google_organization_iam_custom_role.roles :
# build the string manually so that role IDs can be used as map
# keys (useful for folder/organization/project-level iam bindings)
- (role_id) => "${var.organization_id}/roles/${role_id}"
+ (k) => "${var.organization_id}/roles/${local.custom_roles[k].name}"
}
- depends_on = [
- google_organization_iam_custom_role.roles
- ]
}
output "custom_roles" {
diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf
index f664deaef1..f11900dba7 100644
--- a/modules/organization/variables.tf
+++ b/modules/organization/variables.tf
@@ -28,6 +28,17 @@ variable "custom_roles" {
nullable = false
}
+variable "factories_config" {
+ description = "Paths to data files and folders that enable factory functionality."
+ type = object({
+ custom_roles = optional(string)
+ org_policies = optional(string)
+ org_policy_custom_constraints = optional(string)
+ })
+ nullable = false
+ default = {}
+}
+
variable "firewall_policy" {
description = "Hierarchical firewall policies to associate to the organization."
type = object({
@@ -183,12 +194,6 @@ variable "org_policies" {
nullable = false
}
-variable "org_policies_data_path" {
- description = "Path containing org policies in YAML format."
- type = string
- default = null
-}
-
variable "org_policy_custom_constraints" {
description = "Organization policy custom constraints keyed by constraint name."
type = map(object({
@@ -203,12 +208,6 @@ variable "org_policy_custom_constraints" {
nullable = false
}
-variable "org_policy_custom_constraints_data_path" {
- description = "Path containing org policy custom constraints in YAML format."
- type = string
- default = null
-}
-
variable "organization_id" {
description = "Organization id in organizations/nnnnnn format."
type = string
diff --git a/modules/project/README.md b/modules/project/README.md
index bc5f3e3927..fb08566244 100644
--- a/modules/project/README.md
+++ b/modules/project/README.md
@@ -421,12 +421,14 @@ The example below deploys a few organization policies split between two YAML fil
```hcl
module "project" {
- source = "./fabric/modules/project"
- billing_account = var.billing_account_id
- name = "project"
- parent = var.folder_id
- prefix = var.prefix
- org_policies_data_path = "configs/org-policies/"
+ source = "./fabric/modules/project"
+ billing_account = var.billing_account_id
+ name = "project"
+ parent = var.folder_id
+ prefix = var.prefix
+ factories_config = {
+ org_policies = "configs/org-policies/"
+ }
}
# tftest modules=1 resources=8 files=boolean,list inventory=org-policies.yaml e2e
```
@@ -898,7 +900,7 @@ module "bucket" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L186) | Project name and id suffix. | string
| ✓ | |
+| [name](variables.tf#L196) | Project name and id suffix. | string
| ✓ | |
| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool
| | false
|
| [billing_account](variables.tf#L23) | Billing account id. | string
| | null
|
| [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string)
| | {}
|
@@ -906,41 +908,41 @@ module "bucket" {
| [custom_roles](variables.tf#L43) | Map of role name => list of permissions to create in this project. | map(list(string))
| | {}
|
| [default_service_account](variables.tf#L50) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string
| | "keep"
|
| [descriptive_name](variables.tf#L63) | Name of the project name. Used for project name instead of `name` variable. | string
| | null
|
-| [group_iam](variables.tf#L69) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
-| [iam](variables.tf#L76) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_bindings](variables.tf#L83) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [iam_bindings_additive](variables.tf#L98) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [labels](variables.tf#L113) | Resource labels. | map(string)
| | {}
|
-| [lien_reason](variables.tf#L120) | If non-empty, creates a project lien with this description. | string
| | null
|
-| [logging_data_access](variables.tf#L126) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
-| [logging_exclusions](variables.tf#L141) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string)
| | {}
|
-| [logging_sinks](variables.tf#L148) | Logging sinks to create for this project. | map(object({…}))
| | {}
|
-| [metric_scopes](variables.tf#L179) | List of projects that will act as metric scopes for this project. | list(string)
| | []
|
-| [org_policies](variables.tf#L191) | Organization policies applied to this project keyed by policy name. | map(object({…}))
| | {}
|
-| [org_policies_data_path](variables.tf#L218) | Path containing org policies in YAML format. | string
| | null
|
-| [parent](variables.tf#L224) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
-| [prefix](variables.tf#L234) | Optional prefix used to generate project id and name. | string
| | null
|
-| [project_create](variables.tf#L244) | Create project. When set to false, uses a data source to reference existing project. | bool
| | true
|
-| [service_config](variables.tf#L250) | Configure service API activation. | object({…})
| | {…}
|
-| [service_encryption_key_ids](variables.tf#L262) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string))
| | {}
|
-| [service_perimeter_bridges](variables.tf#L269) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string)
| | null
|
-| [service_perimeter_standard](variables.tf#L276) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string
| | null
|
-| [services](variables.tf#L282) | Service APIs to enable. | list(string)
| | []
|
-| [shared_vpc_host_config](variables.tf#L288) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…})
| | null
|
-| [shared_vpc_service_config](variables.tf#L297) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…})
| | {…}
|
-| [skip_delete](variables.tf#L320) | Allows the underlying resources to be destroyed without destroying the project itself. | bool
| | false
|
-| [tag_bindings](variables.tf#L326) | Tag bindings for this project, in key => tag value id format. | map(string)
| | null
|
+| [factories_config](variables.tf#L69) | Paths to data files and folders that enable factory functionality. | object({…})
| | {}
|
+| [group_iam](variables.tf#L79) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
+| [iam](variables.tf#L86) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_bindings](variables.tf#L93) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L108) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [labels](variables.tf#L123) | Resource labels. | map(string)
| | {}
|
+| [lien_reason](variables.tf#L130) | If non-empty, creates a project lien with this description. | string
| | null
|
+| [logging_data_access](variables.tf#L136) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string)))
| | {}
|
+| [logging_exclusions](variables.tf#L151) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string)
| | {}
|
+| [logging_sinks](variables.tf#L158) | Logging sinks to create for this project. | map(object({…}))
| | {}
|
+| [metric_scopes](variables.tf#L189) | List of projects that will act as metric scopes for this project. | list(string)
| | []
|
+| [org_policies](variables.tf#L201) | Organization policies applied to this project keyed by policy name. | map(object({…}))
| | {}
|
+| [parent](variables.tf#L228) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+| [prefix](variables.tf#L238) | Optional prefix used to generate project id and name. | string
| | null
|
+| [project_create](variables.tf#L248) | Create project. When set to false, uses a data source to reference existing project. | bool
| | true
|
+| [service_config](variables.tf#L254) | Configure service API activation. | object({…})
| | {…}
|
+| [service_encryption_key_ids](variables.tf#L266) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string))
| | {}
|
+| [service_perimeter_bridges](variables.tf#L273) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string)
| | null
|
+| [service_perimeter_standard](variables.tf#L280) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string
| | null
|
+| [services](variables.tf#L286) | Service APIs to enable. | list(string)
| | []
|
+| [shared_vpc_host_config](variables.tf#L292) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…})
| | null
|
+| [shared_vpc_service_config](variables.tf#L301) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…})
| | {…}
|
+| [skip_delete](variables.tf#L324) | Allows the underlying resources to be destroyed without destroying the project itself. | bool
| | false
|
+| [tag_bindings](variables.tf#L330) | Tag bindings for this project, in key => tag value id format. | map(string)
| | null
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
-| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | |
-| [id](outputs.tf#L25) | Project id. | |
-| [name](outputs.tf#L44) | Project name. | |
-| [number](outputs.tf#L56) | Project number. | |
-| [project_id](outputs.tf#L75) | Project id. | |
-| [service_accounts](outputs.tf#L94) | Product robot service accounts in project. | |
-| [services](outputs.tf#L110) | Service APIs to enabled in the project. | |
-| [sink_writer_identities](outputs.tf#L119) | Writer identities created for each sink. | |
+| [custom_role_ids](outputs.tf#L17) | Map of custom role IDs created in the project. | |
+| [id](outputs.tf#L27) | Project id. | |
+| [name](outputs.tf#L46) | Project name. | |
+| [number](outputs.tf#L58) | Project number. | |
+| [project_id](outputs.tf#L77) | Project id. | |
+| [service_accounts](outputs.tf#L96) | Product robot service accounts in project. | |
+| [services](outputs.tf#L112) | Service APIs to enabled in the project. | |
+| [sink_writer_identities](outputs.tf#L121) | Writer identities created for each sink. | |
diff --git a/modules/project/iam.tf b/modules/project/iam.tf
index 0f00f2861a..dfd14473cd 100644
--- a/modules/project/iam.tf
+++ b/modules/project/iam.tf
@@ -20,12 +20,32 @@
# - external users need to have accepted the invitation email to join
locals {
+ _custom_roles = {
+ for f in try(fileset(var.factories_config.custom_roles, "*.yaml"), []) :
+ replace(f, ".yaml", "") => yamldecode(
+ file("${var.factories_config.custom_roles}/${f}")
+ )
+ }
_group_iam_roles = distinct(flatten(values(var.group_iam)))
_group_iam = {
for r in local._group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
+ custom_roles = merge(
+ {
+ for k, v in local._custom_roles : k => {
+ name = lookup(v, "name", k)
+ permissions = v["includedPermissions"]
+ }
+ },
+ {
+ for k, v in var.custom_roles : k => {
+ name = k
+ permissions = v
+ }
+ }
+ )
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
@@ -35,13 +55,27 @@ locals {
}
}
+# we use a different key for custom roles to allow referring to the role alias
+# in Terraform, while still being able to define unique role names
+
+check "custom_roles" {
+ assert {
+ condition = (
+ length(local.custom_roles) == length({
+ for k, v in local.custom_roles : v.name => null
+ })
+ )
+ error_message = "Duplicate role name in custom roles."
+ }
+}
+
resource "google_project_iam_custom_role" "roles" {
- for_each = var.custom_roles
+ for_each = local.custom_roles
project = local.project.project_id
- role_id = each.key
- title = "Custom role ${each.key}"
+ role_id = each.value.name
+ title = "Custom role ${each.value.name}"
description = "Terraform-managed."
- permissions = each.value
+ permissions = each.value.permissions
}
resource "google_project_iam_binding" "authoritative" {
diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf
index 37e6f2531f..32ebf66fcf 100644
--- a/modules/project/organization-policies.tf
+++ b/modules/project/organization-policies.tf
@@ -18,10 +18,9 @@
locals {
_factory_data_raw = merge([
- for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
- yamldecode(file("${var.org_policies_data_path}/${f}"))
+ for f in try(fileset(var.factories_config.org_policies, "*.yaml"), []) :
+ yamldecode(file("${var.factories_config.org_policies}/${f}"))
]...)
-
# simulate applying defaults to data coming from yaml files
_factory_data = {
for k, v in local._factory_data_raw :
@@ -49,9 +48,7 @@ locals {
]
}
}
-
_org_policies = merge(local._factory_data, var.org_policies)
-
org_policies = {
for k, v in local._org_policies :
k => merge(v, {
diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf
index ae7bbc6e90..332a956e59 100644
--- a/modules/project/outputs.tf
+++ b/modules/project/outputs.tf
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-output "custom_roles" {
- description = "Ids of the created custom roles."
+output "custom_role_ids" {
+ description = "Map of custom role IDs created in the project."
value = {
- for name, role in google_project_iam_custom_role.roles :
- name => role.id
+ for k, v in google_project_iam_custom_role.roles :
+ # build the string manually so that role IDs can be used as map
+ # keys (useful for folder/organization/project-level iam bindings)
+ (k) => "projects/${local.prefix}${var.name}/roles/${local.custom_roles[k].name}"
}
}
diff --git a/modules/project/variables.tf b/modules/project/variables.tf
index 1bf26a32f4..7ca6003606 100644
--- a/modules/project/variables.tf
+++ b/modules/project/variables.tf
@@ -66,6 +66,16 @@ variable "descriptive_name" {
default = null
}
+variable "factories_config" {
+ description = "Paths to data files and folders that enable factory functionality."
+ type = object({
+ custom_roles = optional(string)
+ org_policies = optional(string)
+ })
+ nullable = false
+ default = {}
+}
+
variable "group_iam" {
description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
type = map(list(string))
@@ -215,12 +225,6 @@ variable "org_policies" {
nullable = false
}
-variable "org_policies_data_path" {
- description = "Path containing org policies in YAML format."
- type = string
- default = null
-}
-
variable "parent" {
description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format."
type = string
diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py
index 7e52704e69..48d4257ade 100644
--- a/tests/modules/folder/test_plan_org_policies.py
+++ b/tests/modules/folder/test_plan_org_policies.py
@@ -25,5 +25,5 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type):
'modules/folder',
tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars'])
yaml_plan = plan_summary('modules/folder', tf_var_files=['common.tfvars'],
- org_policies_data_path=f'{tmp_path}')
+ factories_config=f'{{org_policies="{tmp_path}"}}')
assert tfvars_plan.values == yaml_plan.values
diff --git a/tests/modules/organization/examples/custom-roles.yaml b/tests/modules/organization/examples/custom-roles.yaml
new file mode 100644
index 0000000000..e087878098
--- /dev/null
+++ b/tests/modules/organization/examples/custom-roles.yaml
@@ -0,0 +1,40 @@
+# Copyright 2023 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.org.google_organization_iam_custom_role.roles["test_2"]:
+ description: Terraform-managed.
+ org_id: '1122334455'
+ permissions:
+ - resourcemanager.projects.get
+ - resourcemanager.projects.getIamPolicy
+ - resourcemanager.projects.list
+ role_id: projectViewer
+ stage: GA
+ title: Custom role projectViewer
+ module.org.google_organization_iam_custom_role.roles["test_1"]:
+ description: Terraform-managed.
+ org_id: '1122334455'
+ permissions:
+ - compute.globalOperations.get
+ role_id: test_1
+ stage: GA
+ title: Custom role test_1
+
+counts:
+ google_organization_iam_custom_role: 2
+ modules: 1
+ resources: 2
+
+outputs: {}
diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py
index 9a5d4a458b..1e4474402b 100644
--- a/tests/modules/organization/test_plan_org_policies.py
+++ b/tests/modules/organization/test_plan_org_policies.py
@@ -26,7 +26,7 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type):
tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars'])
yaml_plan = plan_summary('modules/organization',
tf_var_files=['common.tfvars'],
- org_policies_data_path=f'{tmp_path}')
+ factories_config=f'{{org_policies="{tmp_path}"}}')
assert tfvars_plan.values == yaml_plan.values
@@ -39,5 +39,5 @@ def test_custom_constraint_factory(plan_summary, tfvars_to_yaml, tmp_path):
tf_var_files=['common.tfvars', f'org_policies_custom_constraints.tfvars'])
yaml_plan = plan_summary(
'modules/organization', tf_var_files=['common.tfvars'],
- org_policy_custom_constraints_data_path=f'{tmp_path}')
+ factories_config=f'{{org_policy_custom_constraints="{tmp_path}"}}')
assert tfvars_plan.values == yaml_plan.values
diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py
index 4d92b46f5c..632b45e726 100644
--- a/tests/modules/organization/test_plan_org_policies_modules.py
+++ b/tests/modules/organization/test_plan_org_policies_modules.py
@@ -34,7 +34,7 @@ def test_policy_implementation():
'@@ -17 +17 @@\n',
'-# tfdoc:file:description Project-level organization policies.\n',
'+# tfdoc:file:description Folder-level organization policies.\n',
- '@@ -58,2 +58,2 @@\n',
+ '@@ -55,2 +55,2 @@\n',
'- name = "projects/${local.project.project_id}/policies/${k}"\n',
'- parent = "projects/${local.project.project_id}"\n',
'+ name = "${local.folder.name}/policies/${k}"\n',
@@ -49,12 +49,12 @@ def test_policy_implementation():
'@@ -17 +17 @@\n',
'-# tfdoc:file:description Folder-level organization policies.\n',
'+# tfdoc:file:description Organization-level organization policies.\n',
- '@@ -58,2 +58,2 @@\n',
+ '@@ -55,2 +55,2 @@\n',
'- name = "${local.folder.name}/policies/${k}"\n',
'- parent = local.folder.name\n',
'+ name = "${var.organization_id}/policies/${k}"\n',
'+ parent = var.organization_id\n',
- '@@ -116,0 +117,9 @@\n',
+ '@@ -113,0 +114,9 @@\n',
'+ depends_on = [\n',
'+ google_organization_iam_binding.authoritative,\n',
'+ google_organization_iam_binding.bindings,\n',
diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py
index 30354aeaa9..a85bf9c5f6 100644
--- a/tests/modules/project/test_plan_org_policies.py
+++ b/tests/modules/project/test_plan_org_policies.py
@@ -25,5 +25,5 @@ def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type):
'modules/project',
tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars'])
yaml_plan = plan_summary('modules/project', tf_var_files=['common.tfvars'],
- org_policies_data_path=f'{tmp_path}')
+ factories_config=f'{{org_policies="{tmp_path}"}}')
assert tfvars_plan.values == yaml_plan.values