From e2abd772f2618c7e1d6e8609e746803cb8b74774 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Sat, 29 Jan 2022 19:35:33 +0100 Subject: [PATCH] Update resman modules (#475) * Make logging sinks in different resources use the same API * Split resman modules in multiple files. Add nullables where applicable --- modules/folder/README.md | 43 +++-- ...firewall-policy.tf => firewal_policies.tf} | 0 modules/folder/iam.tf | 40 ++++ modules/folder/logging.tf | 91 ++++++++++ modules/folder/main.tf | 171 ------------------ modules/folder/organization_policies.tf | 90 +++++++++ modules/folder/variables.tf | 22 ++- modules/folder/versions.tf | 2 +- modules/organization/README.md | 4 +- ...irewall-policy.tf => firewall_policies.tf} | 0 ...org-policy.tf => organization_policies.tf} | 2 +- modules/project/README.md | 62 +++---- modules/project/logging.tf | 19 +- modules/project/main.tf | 2 +- ...n-policies.tf => organization_policies.tf} | 0 modules/project/variables.tf | 29 ++- modules/project/versions.tf | 2 +- modules/project/{vpc-sc.tf => vpc_sc.tf} | 0 tests/modules/folder/test_plan_logging.py | 2 +- tests/modules/project/test_plan_logging.py | 2 +- 20 files changed, 340 insertions(+), 243 deletions(-) rename modules/folder/{firewall-policy.tf => firewal_policies.tf} (100%) create mode 100644 modules/folder/iam.tf create mode 100644 modules/folder/logging.tf create mode 100644 modules/folder/organization_policies.tf rename modules/organization/{firewall-policy.tf => firewall_policies.tf} (100%) rename modules/organization/{org-policy.tf => organization_policies.tf} (97%) rename modules/project/{organization-policies.tf => organization_policies.tf} (100%) rename modules/project/{vpc-sc.tf => vpc_sc.tf} (100%) diff --git a/modules/folder/README.md b/modules/folder/README.md index b85ba2f69..efb207e1d 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -140,7 +140,7 @@ module "folder-sink" { name = "my-folder" logging_sinks = { warnings = { - type = "gcs" + type = "storage" destination = module.gcs.name filter = "severity=WARNING" include_children = true @@ -215,26 +215,41 @@ module "folder2" { } # tftest modules=2 resources=6 ``` + + +## Files + +| name | description | resources | +|---|---|---| +| [firewal_policies.tf](./firewal_policies.tf) | None | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding | +| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | +| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | +| [organization_policies.tf](./organization_policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + ## Variables | 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_policies](variables.tf#L23) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | -| [firewall_policy_association](variables.tf#L39) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | -| [firewall_policy_factory](variables.tf#L45) | Configuration for the firewall policy factory. | object({…}) | | null | -| [folder_create](variables.tf#L55) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | -| [group_iam](variables.tf#L61) | 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#L67) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [id](variables.tf#L73) | Folder ID in case you use folder_create=false | string | | null | -| [logging_exclusions](variables.tf#L79) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L85) | Logging sinks to create for this folder. | map(object({…})) | | {} | -| [name](variables.tf#L98) | Folder name. | string | | null | -| [parent](variables.tf#L104) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [policy_boolean](variables.tf#L114) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L120) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [firewall_policies](variables.tf#L24) | Hierarchical firewall policies created in this folder. | map(map(object({…}))) | | {} | +| [firewall_policy_association](variables.tf#L41) | The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else. | map(string) | | {} | +| [firewall_policy_factory](variables.tf#L48) | Configuration for the firewall policy factory. | object({…}) | | null | +| [folder_create](variables.tf#L58) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [group_iam](variables.tf#L64) | 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#L71) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [id](variables.tf#L78) | Folder ID in case you use folder_create=false | string | | null | +| [logging_exclusions](variables.tf#L84) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L91) | Logging sinks to create for this folder. | map(object({…})) | | {} | +| [name](variables.tf#L112) | Folder name. | string | | null | +| [parent](variables.tf#L118) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [policy_boolean](variables.tf#L128) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L135) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | ## Outputs diff --git a/modules/folder/firewall-policy.tf b/modules/folder/firewal_policies.tf similarity index 100% rename from modules/folder/firewall-policy.tf rename to modules/folder/firewal_policies.tf diff --git a/modules/folder/iam.tf b/modules/folder/iam.tf new file mode 100644 index 000000000..52886badb --- /dev/null +++ b/modules/folder/iam.tf @@ -0,0 +1,40 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description IAM bindings, roles and audit logging resources. + +locals { + 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 + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : + role => concat( + try(var.iam[role], []), + try(local.group_iam[role], []) + ) + } +} + +resource "google_folder_iam_binding" "authoritative" { + for_each = local.iam + folder = local.folder.name + role = each.key + members = each.value +} diff --git a/modules/folder/logging.tf b/modules/folder/logging.tf new file mode 100644 index 000000000..04df65103 --- /dev/null +++ b/modules/folder/logging.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_folder_sink" "sink" { + for_each = var.logging_sinks + name = each.key + #description = "${each.key} (Terraform-managed)" + folder = local.folder.name + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + include_children = each.value.include_children + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value + } + } + + depends_on = [ + google_folder_iam_binding.authoritative + ] +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_folder_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_folder_sink.sink[each.key].writer_identity + # TODO(jccb): use a condition to limit writer-identity only to this + # bucket +} + +resource "google_logging_folder_exclusion" "logging-exclusion" { + for_each = var.logging_exclusions + name = each.key + folder = local.folder.name + description = "${each.key} (Terraform-managed)" + filter = each.value +} diff --git a/modules/folder/main.tf b/modules/folder/main.tf index 028a87ea6..5d285d2d2 100644 --- a/modules/folder/main.tf +++ b/modules/folder/main.tf @@ -15,34 +15,6 @@ */ locals { - 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 - ] - } - iam = { - for role in distinct(concat(keys(var.iam), keys(local.group_iam))) : - role => concat( - try(var.iam[role], []), - try(local.group_iam[role], []) - ) - } - logging_sinks = coalesce(var.logging_sinks, {}) - sink_type_destination = { - gcs = "storage.googleapis.com" - bigquery = "bigquery.googleapis.com" - pubsub = "pubsub.googleapis.com" - logging = "logging.googleapis.com" - } - sink_bindings = { - for type in ["gcs", "bigquery", "pubsub", "logging"] : - type => { - for name, sink in local.logging_sinks : - name => sink - if sink.type == type - } - } folder = ( var.folder_create ? try(google_folder.folder.0, null) @@ -61,149 +33,6 @@ resource "google_folder" "folder" { parent = var.parent } -resource "google_folder_iam_binding" "authoritative" { - for_each = local.iam - folder = local.folder.name - role = each.key - members = each.value -} - -resource "google_folder_organization_policy" "boolean" { - for_each = var.policy_boolean - folder = local.folder.name - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } - } -} - -resource "google_folder_organization_policy" "list" { - for_each = var.policy_list - folder = local.folder.name - constraint = each.key - - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - } - } - - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true - } - } -} - -resource "google_logging_folder_sink" "sink" { - for_each = local.logging_sinks - name = each.key - #description = "${each.key} (Terraform-managed)" - folder = local.folder.name - destination = "${local.sink_type_destination[each.value.type]}/${each.value.destination}" - filter = each.value.filter - include_children = each.value.include_children - - dynamic "exclusions" { - for_each = each.value.exclusions - iterator = exclusion - content { - name = exclusion.key - filter = exclusion.value - } - } - - depends_on = [ - google_folder_iam_binding.authoritative - ] -} - -resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { - for_each = local.sink_bindings["gcs"] - bucket = each.value.destination - role = "roles/storage.objectCreator" - member = google_logging_folder_sink.sink[each.key].writer_identity -} - -resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { - for_each = local.sink_bindings["bigquery"] - project = split("/", each.value.destination)[1] - dataset_id = split("/", each.value.destination)[3] - role = "roles/bigquery.dataEditor" - member = google_logging_folder_sink.sink[each.key].writer_identity -} - -resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { - for_each = local.sink_bindings["pubsub"] - project = split("/", each.value.destination)[1] - topic = split("/", each.value.destination)[3] - role = "roles/pubsub.publisher" - member = google_logging_folder_sink.sink[each.key].writer_identity -} - -resource "google_project_iam_member" "bucket-sinks-binding" { - for_each = local.sink_bindings["logging"] - project = split("/", each.value.destination)[1] - role = "roles/logging.bucketWriter" - member = google_logging_folder_sink.sink[each.key].writer_identity - # TODO(jccb): use a condition to limit writer-identity only to this - # bucket -} - -resource "google_logging_folder_exclusion" "logging-exclusion" { - for_each = coalesce(var.logging_exclusions, {}) - name = each.key - folder = local.folder.name - description = "${each.key} (Terraform-managed)" - filter = each.value -} - resource "google_essential_contacts_contact" "contact" { provider = google-beta for_each = var.contacts diff --git a/modules/folder/organization_policies.tf b/modules/folder/organization_policies.tf new file mode 100644 index 000000000..177a3d804 --- /dev/null +++ b/modules/folder/organization_policies.tf @@ -0,0 +1,90 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Folder-level organization policies. + +resource "google_folder_organization_policy" "boolean" { + for_each = var.policy_boolean + folder = local.folder.name + constraint = each.key + + dynamic "boolean_policy" { + for_each = each.value == null ? [] : [each.value] + iterator = policy + content { + enforced = policy.value + } + } + + dynamic "restore_policy" { + for_each = each.value == null ? [""] : [] + content { + default = true + } + } +} + +resource "google_folder_organization_policy" "list" { + for_each = var.policy_list + folder = local.folder.name + constraint = each.key + + dynamic "list_policy" { + for_each = each.value.status == null ? [] : [each.value] + iterator = policy + content { + inherit_from_parent = policy.value.inherit_from_parent + suggested_value = policy.value.suggested_value + dynamic "allow" { + for_each = policy.value.status ? [""] : [] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + dynamic "deny" { + for_each = policy.value.status ? [] : [""] + content { + values = ( + try(length(policy.value.values) > 0, false) + ? policy.value.values + : null + ) + all = ( + try(length(policy.value.values) > 0, false) + ? null + : true + ) + } + } + } + } + + dynamic "restore_policy" { + for_each = each.value.status == null ? [true] : [] + content { + default = true + } + } +} diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 9fc22b52b..563831cb6 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -18,6 +18,7 @@ variable "contacts" { description = "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" type = map(list(string)) default = {} + nullable = false } variable "firewall_policies" { @@ -33,13 +34,15 @@ variable "firewall_policies" { target_resources = list(string) target_service_accounts = list(string) }))) - default = {} + default = {} + nullable = false } variable "firewall_policy_association" { description = "The hierarchical firewall policy to associate to this folder. Must be either a key in the `firewall_policies` map or the id of a policy defined somewhere else." type = map(string) default = {} + nullable = false } variable "firewall_policy_factory" { @@ -62,12 +65,14 @@ 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)) default = {} + nullable = false } variable "iam" { description = "IAM bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) default = {} + nullable = false } variable "id" { @@ -80,6 +85,7 @@ variable "logging_exclusions" { description = "Logging exclusions for this folder in the form {NAME -> FILTER}." type = map(string) default = {} + nullable = false } variable "logging_sinks" { @@ -92,7 +98,15 @@ variable "logging_sinks" { # TODO exclusions also support description and disabled exclusions = map(string) })) - default = {} + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false } variable "name" { @@ -115,6 +129,7 @@ variable "policy_boolean" { description = "Map of boolean org policies and enforcement value, set value to null for policy restore." type = map(bool) default = {} + nullable = false } variable "policy_list" { @@ -125,5 +140,6 @@ variable "policy_list" { status = bool values = list(string) })) - default = {} + default = {} + nullable = false } diff --git a/modules/folder/versions.tf b/modules/folder/versions.tf index 290412687..e72a78007 100644 --- a/modules/folder/versions.tf +++ b/modules/folder/versions.tf @@ -13,7 +13,7 @@ # limitations under the License. terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.1.0" required_providers { google = { source = "hashicorp/google" diff --git a/modules/organization/README.md b/modules/organization/README.md index 4cda102be..8edc529d7 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -243,11 +243,11 @@ module "org" { | name | description | resources | |---|---|---| -| [firewall-policy.tf](./firewall-policy.tf) | Hierarchical firewall policies. | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | +| [firewall_policies.tf](./firewall_policies.tf) | Hierarchical firewall policies. | google_compute_firewall_policy · google_compute_firewall_policy_association · google_compute_firewall_policy_rule | | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_audit_config · google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member · google_organization_iam_policy | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact | -| [org-policy.tf](./org-policy.tf) | Organization policies. | google_organization_policy | +| [organization_policies.tf](./organization_policies.tf) | Organization-level organization policies. | google_organization_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | diff --git a/modules/organization/firewall-policy.tf b/modules/organization/firewall_policies.tf similarity index 100% rename from modules/organization/firewall-policy.tf rename to modules/organization/firewall_policies.tf diff --git a/modules/organization/org-policy.tf b/modules/organization/organization_policies.tf similarity index 97% rename from modules/organization/org-policy.tf rename to modules/organization/organization_policies.tf index c00783791..f23a98b48 100644 --- a/modules/organization/org-policy.tf +++ b/modules/organization/organization_policies.tf @@ -14,7 +14,7 @@ * limitations under the License. */ -# tfdoc:file:description Organization policies. +# tfdoc:file:description Organization-level organization policies. resource "google_organization_policy" "boolean" { for_each = var.policy_boolean diff --git a/modules/project/README.md b/modules/project/README.md index b021fdcb7..170160325 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -108,7 +108,7 @@ module "project-host" { parent = "folders/1234567890" logging_sinks = { warnings = { - type = "gcs" + type = "storage" destination = module.gcs.name filter = "severity=WARNING" iam = false @@ -184,49 +184,49 @@ module "project" { | [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | -| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [organization_policies.tf](./organization_policies.tf) | Project-level organization policies. | google_project_organization_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [service_accounts.tf](./service_accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_service_identity | | [shared_vpc.tf](./shared_vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | -| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | +| [vpc_sc.tf](./vpc_sc.tf) | VPC-SC project-level perimeter configuration. | google_access_context_manager_service_perimeter_resource | ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L109) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L125) | 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 | | [contacts](variables.tf#L29) | 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#L35) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [descriptive_name](variables.tf#L41) | Name of the project name. Used for project name instead of `name` variable | string | | null | -| [group_iam](variables.tf#L47) | 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#L53) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_additive](variables.tf#L59) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_additive_members](variables.tf#L65) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | -| [labels](variables.tf#L71) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L77) | If non-empty, creates a project lien with this description. | string | | "" | -| [logging_exclusions](variables.tf#L83) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L89) | Logging sinks to create for this project. | map(object({…})) | | {} | -| [metric_scopes](variables.tf#L103) | List of projects that will act as metric scopes for this project. | list(string) | | null | -| [oslogin](variables.tf#L114) | Enable OS Login. | bool | | false | -| [oslogin_admins](variables.tf#L120) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | -| [oslogin_users](variables.tf#L126) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | -| [parent](variables.tf#L132) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [policy_boolean](variables.tf#L142) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L148) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [prefix](variables.tf#L159) | Prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L165) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L171) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L183) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L190) | 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#L197) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L203) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L209) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | {…} | -| [shared_vpc_service_config](variables.tf#L221) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L233) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable | string | | null | +| [group_iam](variables.tf#L49) | 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#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string)) | | {} | +| [labels](variables.tf#L76) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string | | "" | +| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [oslogin](variables.tf#L130) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | +| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | +| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L192) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L211) | 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#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L224) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | {…} | +| [shared_vpc_service_config](variables.tf#L243) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L256) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | ## Outputs diff --git a/modules/project/logging.tf b/modules/project/logging.tf index 5a22a63ec..53bc2f030 100644 --- a/modules/project/logging.tf +++ b/modules/project/logging.tf @@ -17,28 +17,21 @@ # tfdoc:file:description Log sinks and supporting resources. locals { - logging_sinks = coalesce(var.logging_sinks, {}) sink_bindings = { - for type in ["gcs", "bigquery", "pubsub", "logging"] : + for type in ["bigquery", "pubsub", "logging", "storage"] : type => { - for name, sink in local.logging_sinks : + for name, sink in var.logging_sinks : name => sink if sink.iam && sink.type == type } } - sink_type_destination = { - gcs = "storage.googleapis.com" - bigquery = "bigquery.googleapis.com" - pubsub = "pubsub.googleapis.com" - logging = "logging.googleapis.com" - } } resource "google_logging_project_sink" "sink" { - for_each = local.logging_sinks + for_each = var.logging_sinks name = each.key #description = "${each.key} (Terraform-managed)" project = local.project.project_id - destination = "${local.sink_type_destination[each.value.type]}/${each.value.destination}" + destination = "${each.value.type}.googleapis.com/${each.value.destination}" filter = each.value.filter unique_writer_identity = each.value.unique_writer @@ -58,7 +51,7 @@ resource "google_logging_project_sink" "sink" { } resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { - for_each = local.sink_bindings["gcs"] + for_each = local.sink_bindings["storage"] bucket = each.value.destination role = "roles/storage.objectCreator" member = google_logging_project_sink.sink[each.key].writer_identity @@ -90,7 +83,7 @@ resource "google_project_iam_member" "bucket-sinks-binding" { } resource "google_logging_project_exclusion" "logging-exclusion" { - for_each = coalesce(var.logging_exclusions, {}) + for_each = var.logging_exclusions name = each.key project = local.project.project_id description = "${each.key} (Terraform-managed)" diff --git a/modules/project/main.tf b/modules/project/main.tf index 89af9501f..9f0dff497 100644 --- a/modules/project/main.tf +++ b/modules/project/main.tf @@ -89,7 +89,7 @@ resource "google_essential_contacts_contact" "contact" { resource "google_monitoring_monitored_project" "primary" { provider = google-beta - for_each = toset(coalesce(var.metric_scopes, [])) + for_each = toset(var.metric_scopes) metrics_scope = each.value name = local.project.project_id } diff --git a/modules/project/organization-policies.tf b/modules/project/organization_policies.tf similarity index 100% rename from modules/project/organization-policies.tf rename to modules/project/organization_policies.tf diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 4b581fa53..00f20f949 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -30,12 +30,14 @@ variable "contacts" { description = "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" type = map(list(string)) default = {} + nullable = false } variable "custom_roles" { description = "Map of role name => list of permissions to create in this project." type = map(list(string)) default = {} + nullable = false } variable "descriptive_name" { @@ -48,18 +50,21 @@ 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)) default = {} + nullable = false } variable "iam" { description = "IAM bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) default = {} + nullable = false } variable "iam_additive" { description = "IAM additive bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) default = {} + nullable = false } variable "iam_additive_members" { @@ -72,6 +77,7 @@ variable "labels" { description = "Resource labels." type = map(string) default = {} + nullable = false } variable "lien_reason" { @@ -84,6 +90,7 @@ variable "logging_exclusions" { description = "Logging exclusions for this project in the form {NAME -> FILTER}." type = map(string) default = {} + nullable = false } variable "logging_sinks" { @@ -97,13 +104,22 @@ variable "logging_sinks" { # TODO exclusions also support description and disabled exclusions = map(string) })) - default = {} + validation { + condition = alltrue([ + for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + default = {} + nullable = false } variable "metric_scopes" { description = "List of projects that will act as metric scopes for this project." type = list(string) - default = null + default = [] + nullable = false } variable "name" { @@ -121,12 +137,15 @@ variable "oslogin_admins" { description = "List of IAM-style identities that will be granted roles necessary for OS Login administrators." type = list(string) default = [] + nullable = false + } variable "oslogin_users" { description = "List of IAM-style identities that will be granted roles necessary for OS Login users." type = list(string) default = [] + nullable = false } variable "parent" { @@ -143,6 +162,7 @@ variable "policy_boolean" { description = "Map of boolean org policies and enforcement value, set value to null for policy restore." type = map(bool) default = {} + nullable = false } variable "policy_list" { @@ -153,7 +173,8 @@ variable "policy_list" { status = bool values = list(string) })) - default = {} + default = {} + nullable = false } variable "prefix" { @@ -216,6 +237,7 @@ variable "shared_vpc_host_config" { enabled = false service_projects = [] } + nullable = false } variable "shared_vpc_service_config" { @@ -228,6 +250,7 @@ variable "shared_vpc_service_config" { attach = false host_project = "" } + nullable = false } variable "skip_delete" { diff --git a/modules/project/versions.tf b/modules/project/versions.tf index 290412687..e72a78007 100644 --- a/modules/project/versions.tf +++ b/modules/project/versions.tf @@ -13,7 +13,7 @@ # limitations under the License. terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.1.0" required_providers { google = { source = "hashicorp/google" diff --git a/modules/project/vpc-sc.tf b/modules/project/vpc_sc.tf similarity index 100% rename from modules/project/vpc-sc.tf rename to modules/project/vpc_sc.tf diff --git a/tests/modules/folder/test_plan_logging.py b/tests/modules/folder/test_plan_logging.py index 9e323f57a..9f3da533d 100644 --- a/tests/modules/folder/test_plan_logging.py +++ b/tests/modules/folder/test_plan_logging.py @@ -18,7 +18,7 @@ def test_sinks(plan_runner): "Test folder-level sinks." logging_sinks = """ { warning = { - type = "gcs" + type = "storage" destination = "mybucket" filter = "severity=WARNING" iam = true diff --git a/tests/modules/project/test_plan_logging.py b/tests/modules/project/test_plan_logging.py index 4c0a4c325..2b847f99a 100644 --- a/tests/modules/project/test_plan_logging.py +++ b/tests/modules/project/test_plan_logging.py @@ -19,7 +19,7 @@ def test_sinks(plan_runner): "Test folder-level sinks." logging_sinks = """ { warning = { - type = "gcs" + type = "storage" destination = "mybucket" filter = "severity=WARNING" iam = true