diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf
index e4082f69c3..e396364e89 100644
--- a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf
+++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf
@@ -55,10 +55,12 @@ module "vpc" {
}
module "pubsub" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = var.name
- subscriptions = { "${var.name}-default" = null }
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = var.name
+ subscriptions = {
+ "${var.name}-default" = {}
+ }
iam = {
"roles/pubsub.publisher" = [
"serviceAccount:${module.project.service_accounts.robots.cloudasset}"
diff --git a/blueprints/cloud-operations/quota-monitoring/main.tf b/blueprints/cloud-operations/quota-monitoring/main.tf
index f644c8fb9e..a49891c00c 100644
--- a/blueprints/cloud-operations/quota-monitoring/main.tf
+++ b/blueprints/cloud-operations/quota-monitoring/main.tf
@@ -39,7 +39,7 @@ module "pubsub" {
project_id = module.project.project_id
name = var.name
subscriptions = {
- "${var.name}-default" = null
+ "${var.name}-default" = {}
}
# the Cloud Scheduler robot service account already has pubsub.topics.publish
# at the project level via roles/cloudscheduler.serviceAgent
diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
index c10c0b6b0f..6460384ea7 100644
--- a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
+++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
@@ -63,7 +63,7 @@ module "pubsub" {
project_id = module.project.project_id
name = var.name
subscriptions = {
- "${var.name}-default" = null
+ "${var.name}-default" = {}
}
# the Cloud Scheduler robot service account already has pubsub.topics.publish
# at the project level via roles/cloudscheduler.serviceAgent
@@ -74,7 +74,7 @@ module "pubsub_file" {
project_id = module.project.project_id
name = var.name_cffile
subscriptions = {
- "${var.name_cffile}-default" = null
+ "${var.name_cffile}-default" = {}
}
# the Cloud Scheduler robot service account already has pubsub.topics.publish
# at the project level via roles/cloudscheduler.serviceAgent
diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf
index 27fbe99b3e..fb446e718a 100644
--- a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf
+++ b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf
@@ -1,4 +1,4 @@
-# Copyright 2022 Google LLC
+# 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.
@@ -106,7 +106,10 @@ module "kms" {
name = "${var.prefix}-${var.region}",
location = var.region
}
- keys = { key-gce = null, key-gcs = null }
+ keys = {
+ key-gce = {}
+ key-gcs = {}
+ }
}
###############################################################################
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
index 5e61663087..722016b7cc 100644
--- a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
@@ -1,4 +1,4 @@
-# Copyright 2022 Google LLC
+# 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.
@@ -21,26 +21,27 @@ module "kms" {
location = var.region
}
keys = {
- key-df = null
- key-gcs = null
- key-bq = null
- }
- key_iam = {
+ key-df = {
+ iam = {
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
+ "serviceAccount:${module.project.service_accounts.robots.dataflow}",
+ "serviceAccount:${module.project.service_accounts.robots.compute}",
+ ]
+ }
+ }
key-gcs = {
- "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
- "serviceAccount:${module.project.service_accounts.robots.storage}"
- ]
- },
+ iam = {
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
+ "serviceAccount:${module.project.service_accounts.robots.storage}"
+ ]
+ }
+ }
key-bq = {
- "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
- "serviceAccount:${module.project.service_accounts.robots.bq}"
- ]
- },
- key-df = {
- "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
- "serviceAccount:${module.project.service_accounts.robots.dataflow}",
- "serviceAccount:${module.project.service_accounts.robots.compute}",
- ]
+ iam = {
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
+ "serviceAccount:${module.project.service_accounts.robots.bq}"
+ ]
+ }
}
}
}
diff --git a/blueprints/data-solutions/shielded-folder/README.md b/blueprints/data-solutions/shielded-folder/README.md
index ed177d27be..72a6b69f04 100644
--- a/blueprints/data-solutions/shielded-folder/README.md
+++ b/blueprints/data-solutions/shielded-folder/README.md
@@ -159,18 +159,18 @@ terraform apply
|---|---|:---:|:---:|:---:|
| [access_policy_config](variables.tf#L17) | Provide 'access_policy_create' values if a folder scoped Access Policy creation is needed, uses existing 'policy_name' otherwise. Parent is in 'organizations/123456' format. Policy will be created scoped to the folder. | object({…})
| ✓ | |
| [folder_config](variables.tf#L49) | Provide 'folder_create' values if folder creation is needed, uses existing 'folder_id' otherwise. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| ✓ | |
-| [organization](variables.tf#L129) | Organization details. | object({…})
| ✓ | |
-| [prefix](variables.tf#L137) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [project_config](variables.tf#L142) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| ✓ | |
+| [organization](variables.tf#L148) | Organization details. | object({…})
| ✓ | |
+| [prefix](variables.tf#L156) | Prefix used for resources that need unique names. | string
| ✓ | |
+| [project_config](variables.tf#L161) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| ✓ | |
| [data_dir](variables.tf#L29) | Relative path for the folder storing configuration data. | string
| | "data"
|
| [enable_features](variables.tf#L35) | Flag to enable features on the solution. | object({…})
| | {…}
|
| [groups](variables.tf#L65) | User groups. | object({…})
| | {}
|
-| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | map(object({…}))
| | {}
|
-| [log_locations](variables.tf#L87) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {…}
|
-| [log_sinks](variables.tf#L104) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
|
-| [vpc_sc_access_levels](variables.tf#L162) | VPC SC access level definitions. | map(object({…}))
| | {}
|
-| [vpc_sc_egress_policies](variables.tf#L191) | VPC SC egress policy definitions. | map(object({…}))
| | {}
|
-| [vpc_sc_ingress_policies](variables.tf#L211) | VPC SC ingress policy definitions. | map(object({…}))
| | {}
|
+| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | map(object({…}))
| | {}
|
+| [log_locations](variables.tf#L111) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…})
| | {}
|
+| [log_sinks](variables.tf#L123) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
|
+| [vpc_sc_access_levels](variables.tf#L181) | VPC SC access level definitions. | map(object({…}))
| | {}
|
+| [vpc_sc_egress_policies](variables.tf#L210) | VPC SC egress policy definitions. | map(object({…}))
| | {}
|
+| [vpc_sc_ingress_policies](variables.tf#L230) | VPC SC ingress policy definitions. | map(object({…}))
| | {}
|
## Outputs
diff --git a/blueprints/data-solutions/shielded-folder/kms.tf b/blueprints/data-solutions/shielded-folder/kms.tf
index 9953d458a7..4a634fcc78 100644
--- a/blueprints/data-solutions/shielded-folder/kms.tf
+++ b/blueprints/data-solutions/shielded-folder/kms.tf
@@ -17,12 +17,17 @@
# tfdoc:file:description Security project, Cloud KMS and Secret Manager resources.
locals {
+ # list of locations with keys
kms_locations = distinct(flatten([
for k, v in var.kms_keys : v.locations
]))
+ # map { location -> { key_name -> key_details } }
kms_locations_keys = {
- for loc in local.kms_locations : loc => {
- for k, v in var.kms_keys : k => v if contains(v.locations, loc)
+ for loc in local.kms_locations :
+ loc => {
+ for k, v in var.kms_keys :
+ k => v
+ if contains(v.locations, loc)
}
}
kms_log_locations = distinct(flatten([
@@ -30,17 +35,14 @@ locals {
]))
kms_log_sink_keys = {
"storage" = {
- labels = {}
locations = [var.log_locations.storage]
rotation_period = "7776000s"
}
"bq" = {
- labels = {}
locations = [var.log_locations.bq]
rotation_period = "7776000s"
}
"pubsub" = {
- labels = {}
locations = [var.log_locations.pubsub]
rotation_period = "7776000s"
}
@@ -88,12 +90,6 @@ module "sec-kms" {
location = each.key
name = "sec-${each.key}"
}
- key_iam = {
- for k, v in local.kms_locations_keys[each.key] : k => v.iam
- }
- key_iam_bindings_additive = {
- for k, v in local.kms_locations_keys[each.key] : k => v.iam_bindings_additive
- }
keys = local.kms_locations_keys[each.key]
}
diff --git a/blueprints/data-solutions/shielded-folder/variables.tf b/blueprints/data-solutions/shielded-folder/variables.tf
index 5bb80d5783..03fea7c4ab 100644
--- a/blueprints/data-solutions/shielded-folder/variables.tf
+++ b/blueprints/data-solutions/shielded-folder/variables.tf
@@ -75,11 +75,35 @@ variable "groups" {
variable "kms_keys" {
description = "KMS keys to create, keyed by name."
type = map(object({
- iam = optional(map(list(string)), {})
- iam_bindings_additive = optional(map(map(any)), {})
- labels = optional(map(string), {})
- locations = optional(list(string), ["global", "europe", "europe-west1"])
- rotation_period = optional(string, "7776000s")
+ labels = optional(map(string))
+ locations = optional(list(string), ["global", "europe", "europe-west1"])
+ rotation_period = optional(string, "7776000s")
+ purpose = optional(string, "ENCRYPT_DECRYPT")
+ skip_initial_version_creation = optional(bool, false)
+ version_template = optional(object({
+ algorithm = string
+ protection_level = optional(string, "SOFTWARE")
+ }))
+
+ iam = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ members = list(string)
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+ iam_bindings_additive = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+
}))
default = {}
}
@@ -92,12 +116,7 @@ variable "log_locations" {
logging = optional(string, "global")
pubsub = optional(string, "global")
})
- default = {
- bq = "europe"
- storage = "europe"
- logging = "global"
- pubsub = null
- }
+ default = {}
nullable = false
}
diff --git a/blueprints/gke/binauthz/main.tf b/blueprints/gke/binauthz/main.tf
index 2eac7c560e..8cff68a020 100644
--- a/blueprints/gke/binauthz/main.tf
+++ b/blueprints/gke/binauthz/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -115,20 +115,16 @@ module "kms" {
project_id = module.project.project_id
keyring = { location = var.region, name = "test-keyring" }
keyring_create = true
- keys = { test-key = null }
- key_purpose = {
+ keys = {
test-key = {
purpose = "ASYMMETRIC_SIGN"
version_template = {
- algorithm = "RSA_SIGN_PKCS1_4096_SHA512"
- protection_level = null
+ algorithm = "RSA_SIGN_PKCS1_4096_SHA512"
+ }
+ iam = {
+ "roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email]
+ "roles/cloudkms.signer" = [module.image_cb_sa.iam_email]
}
- }
- }
- key_iam = {
- test-key = {
- "roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email]
- "roles/cloudkms.signer" = [module.image_cb_sa.iam_email]
}
}
}
diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md
index e28aac7b5d..9d47bdaf0a 100644
--- a/fast/stages/2-security/README.md
+++ b/fast/stages/2-security/README.md
@@ -284,13 +284,12 @@ Some references that might be useful in setting up this stage:
-
## Files
| name | description | modules | resources |
|---|---|---|---|
-| [core-dev.tf](./core-dev.tf) | None | kms
· project
| google_project_iam_member
|
-| [core-prod.tf](./core-prod.tf) | None | kms
· project
| google_project_iam_member
|
+| [core-dev.tf](./core-dev.tf) | None | kms
· project
| |
+| [core-prod.tf](./core-prod.tf) | None | kms
· project
| |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object
· local_file
|
| [variables.tf](./variables.tf) | Module variables. | | |
@@ -303,17 +302,16 @@ Some references that might be useful in setting up this stage:
| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…})
| ✓ | | 0-bootstrap
|
| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…})
| ✓ | | 0-bootstrap
|
| [folder_ids](variables.tf#L38) | Folder name => id mappings, the 'security' folder name must exist. | object({…})
| ✓ | | 1-resman
|
-| [organization](variables.tf#L84) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
-| [prefix](variables.tf#L100) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
-| [service_accounts](variables.tf#L111) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…})
| ✓ | | 1-resman
|
+| [organization](variables.tf#L97) | Organization details. | object({…})
| ✓ | | 0-bootstrap
|
+| [prefix](variables.tf#L113) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 0-bootstrap
|
+| [service_accounts](variables.tf#L124) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…})
| ✓ | | 1-resman
|
| [groups](variables.tf#L46) | Group names to grant organization-level permissions. | map(string)
| | {…}
| 0-bootstrap
|
-| [kms_defaults](variables.tf#L61) | Defaults used for KMS keys. | object({…})
| | {…}
| |
-| [kms_keys](variables.tf#L73) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…}))
| | {}
| |
-| [outputs_location](variables.tf#L94) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string
| | null
| |
-| [vpc_sc_access_levels](variables.tf#L122) | VPC SC access level definitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_egress_policies](variables.tf#L151) | VPC SC egress policy definitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_ingress_policies](variables.tf#L171) | VPC SC ingress policy definitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_perimeters](variables.tf#L192) | VPC SC regular perimeter definitions. | object({…})
| | {}
| |
+| [kms_keys](variables.tf#L61) | KMS keys to create, keyed by name. | map(object({…}))
| | {}
| |
+| [outputs_location](variables.tf#L107) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string
| | null
| |
+| [vpc_sc_access_levels](variables.tf#L135) | VPC SC access level definitions. | map(object({…}))
| | {}
| |
+| [vpc_sc_egress_policies](variables.tf#L164) | VPC SC egress policy definitions. | map(object({…}))
| | {}
| |
+| [vpc_sc_ingress_policies](variables.tf#L184) | VPC SC ingress policy definitions. | map(object({…}))
| | {}
| |
+| [vpc_sc_perimeters](variables.tf#L205) | VPC SC regular perimeter definitions. | object({…})
| | {}
| |
## Outputs
@@ -322,5 +320,4 @@ Some references that might be useful in setting up this stage:
| [kms_keys](outputs.tf#L59) | KMS key ids. | | |
| [stage_perimeter_projects](outputs.tf#L64) | Security project numbers. They can be added to perimeter resources. | | |
| [tfvars](outputs.tf#L74) | Terraform variable files for the following stages. | ✓ | |
-
diff --git a/fast/stages/2-security/core-dev.tf b/fast/stages/2-security/core-dev.tf
index 1b49494721..6f71318daf 100644
--- a/fast/stages/2-security/core-dev.tf
+++ b/fast/stages/2-security/core-dev.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -16,11 +16,11 @@
locals {
dev_kms_restricted_admins = [
- for sa in compact([
+ for sa in distinct(compact([
var.service_accounts.data-platform-dev,
var.service_accounts.project-factory-dev,
var.service_accounts.project-factory-prod
- ]) : "serviceAccount:${sa}"
+ ])) : "serviceAccount:${sa}"
]
}
@@ -33,6 +33,12 @@ module "dev-sec-project" {
iam = {
"roles/cloudkms.viewer" = local.dev_kms_restricted_admins
}
+ iam_bindings_additive = {
+ for member in local.dev_kms_restricted_admins :
+ "kms_restricted_admin.${member}" => merge(local.kms_restricted_admin_template, {
+ member = member
+ })
+ }
labels = { environment = "dev", team = "security" }
services = local.project_services
}
@@ -45,30 +51,5 @@ module "dev-sec-kms" {
location = each.key
name = "dev-${each.key}"
}
- # rename to `key_iam` to switch to authoritative bindings
- key_iam = {
- for k, v in local.kms_locations_keys[each.key] : k => v.iam
- }
keys = local.kms_locations_keys[each.key]
}
-
-# TODO(ludo): add support for conditions to Fabric modules
-
-resource "google_project_iam_member" "dev_key_admin_delegated" {
- for_each = toset(local.dev_kms_restricted_admins)
- project = module.dev-sec-project.project_id
- role = "roles/cloudkms.admin"
- member = each.key
- condition {
- title = "kms_sa_delegated_grants"
- description = "Automation service account delegated grants."
- expression = format(
- "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'",
- join(",", formatlist("'%s'", [
- "roles/cloudkms.cryptoKeyEncrypterDecrypter",
- "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
- ]))
- )
- }
- depends_on = [module.dev-sec-project]
-}
diff --git a/fast/stages/2-security/core-prod.tf b/fast/stages/2-security/core-prod.tf
index 559ff32f6b..1d53624950 100644
--- a/fast/stages/2-security/core-prod.tf
+++ b/fast/stages/2-security/core-prod.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -16,10 +16,10 @@
locals {
prod_kms_restricted_admins = [
- for sa in compact([
+ for sa in distinct(compact([
var.service_accounts.data-platform-prod,
var.service_accounts.project-factory-prod
- ]) : "serviceAccount:${sa}"
+ ])) : "serviceAccount:${sa}"
]
}
@@ -32,6 +32,12 @@ module "prod-sec-project" {
iam = {
"roles/cloudkms.viewer" = local.prod_kms_restricted_admins
}
+ iam_bindings_additive = {
+ for member in local.prod_kms_restricted_admins :
+ "kms_restricted_admin.${member}" => merge(local.kms_restricted_admin_template, {
+ member = member
+ })
+ }
labels = { environment = "prod", team = "security" }
services = local.project_services
}
@@ -44,30 +50,5 @@ module "prod-sec-kms" {
location = each.key
name = "prod-${each.key}"
}
- # rename to `key_iam` to switch to authoritative bindings
- key_iam = {
- for k, v in local.kms_locations_keys[each.key] : k => v.iam
- }
keys = local.kms_locations_keys[each.key]
}
-
-# TODO(ludo): add support for conditions to Fabric modules
-
-resource "google_project_iam_member" "prod_key_admin_delegated" {
- for_each = toset(local.prod_kms_restricted_admins)
- project = module.prod-sec-project.project_id
- role = "roles/cloudkms.admin"
- member = each.key
- condition {
- title = "kms_sa_delegated_grants"
- description = "Automation service account delegated grants."
- expression = format(
- "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'",
- join(",", formatlist("'%s'", [
- "roles/cloudkms.cryptoKeyEncrypterDecrypter",
- "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
- ]))
- )
- }
- depends_on = [module.prod-sec-project]
-}
diff --git a/fast/stages/2-security/main.tf b/fast/stages/2-security/main.tf
index 13078d12dd..707990116c 100644
--- a/fast/stages/2-security/main.tf
+++ b/fast/stages/2-security/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -15,28 +15,36 @@
*/
locals {
- kms_keys = {
- for k, v in var.kms_keys : k => {
- iam = coalesce(v.iam, {})
- labels = coalesce(v.labels, {})
- locations = (
- v.locations == null
- ? var.kms_defaults.locations
- : v.locations
- )
- rotation_period = (
- v.rotation_period == null
- ? var.kms_defaults.rotation_period
- : v.rotation_period
+ # additive IAM binding for delegated KMS admins
+ kms_restricted_admin_template = {
+ role = "roles/cloudkms.admin"
+ condition = {
+ title = "kms_sa_delegated_grants"
+ description = "Automation service account delegated grants."
+ expression = format(
+ <<-EOT
+ api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) &&
+ resource.type == 'cloudkms.googleapis.com/CryptoKey'
+ EOT
+ , join(",", formatlist("'%s'", [
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter",
+ "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
+ ]))
)
}
}
+
+ # list of locations with keys
kms_locations = distinct(flatten([
- for k, v in local.kms_keys : v.locations
+ for k, v in var.kms_keys : v.locations
]))
+ # map { location -> { key_name -> key_details } }
kms_locations_keys = {
- for loc in local.kms_locations : loc => {
- for k, v in local.kms_keys : k => v if contains(v.locations, loc)
+ for loc in local.kms_locations :
+ loc => {
+ for k, v in var.kms_keys :
+ k => v
+ if contains(v.locations, loc)
}
}
project_services = [
diff --git a/fast/stages/2-security/variables.tf b/fast/stages/2-security/variables.tf
index f798de7866..fa439c8ce6 100644
--- a/fast/stages/2-security/variables.tf
+++ b/fast/stages/2-security/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -58,27 +58,40 @@ variable "groups" {
}
}
-variable "kms_defaults" {
- description = "Defaults used for KMS keys."
- type = object({
- locations = list(string)
- rotation_period = string
- })
- default = {
- locations = ["europe", "europe-west1", "europe-west3", "global"]
- rotation_period = "7776000s"
- }
-}
-
variable "kms_keys" {
- description = "KMS keys to create, keyed by name. Null attributes will be interpolated with defaults."
+ description = "KMS keys to create, keyed by name."
type = map(object({
- iam = map(list(string))
- labels = map(string)
- locations = list(string)
- rotation_period = string
+ rotation_period = optional(string, "7776000s")
+ labels = optional(map(string))
+ locations = optional(list(string), ["europe", "europe-west1", "europe-west3", "global"])
+ purpose = optional(string, "ENCRYPT_DECRYPT")
+ skip_initial_version_creation = optional(bool, false)
+ version_template = optional(object({
+ algorithm = string
+ protection_level = optional(string, "SOFTWARE")
+ }))
+
+ iam = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ members = list(string)
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+ iam_bindings_additive = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
}))
- default = {}
+ default = {}
+ nullable = false
}
variable "organization" {
diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md
index 00cf5ded30..74afa419be 100644
--- a/modules/cloudsql-instance/README.md
+++ b/modules/cloudsql-instance/README.md
@@ -116,13 +116,12 @@ module "kms" {
location = var.region
}
keys = {
- key-sql = null
- }
- key_iam = {
key-sql = {
- "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
- "serviceAccount:${module.project.service_accounts.robots.sqladmin}"
- ]
+ iam = {
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
+ "serviceAccount:${module.project.service_accounts.robots.sqladmin}"
+ ]
+ }
}
}
}
diff --git a/modules/kms/README.md b/modules/kms/README.md
index a3b2c902f9..ddbf4b5c86 100644
--- a/modules/kms/README.md
+++ b/modules/kms/README.md
@@ -31,7 +31,7 @@ module "kms" {
}
keyring = { location = "europe-west1", name = "test" }
keyring_create = false
- keys = { key-a = null, key-b = null, key-c = null }
+ keys = { key-a = {}, key-b = {}, key-c = {} }
}
# tftest skip (uses data sources)
```
@@ -42,26 +42,34 @@ module "kms" {
module "kms" {
source = "./fabric/modules/kms"
project_id = "my-project"
- key_iam = {
+ keyring = {
+ location = "europe-west1"
+ name = "test"
+ }
+ keys = {
key-a = {
- "roles/cloudkms.admin" = ["user:user3@example.com"]
+ iam = {
+ "roles/cloudkms.admin" = ["user:user3@example.com"]
+ }
}
- }
- key_iam_bindings_additive = {
- key-b-am1 = {
- key = "key-b"
- member = "user:am1@example.com"
- role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
+ key-b = {
+ rotation_period = "604800s"
+ iam_bindings_additive = {
+ key-b-iam1 = {
+ key = "key-b"
+ member = "user:am1@example.com"
+ role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
+ }
+ }
+ }
+ key-c = {
+ labels = {
+ env = "test"
+ }
}
- }
- keyring = { location = "europe-west1", name = "test" }
- keys = {
- key-a = null
- key-b = { rotation_period = "604800s", labels = null }
- key-c = { rotation_period = null, labels = { env = "test" } }
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=6 inventory=basic.yaml
```
### Crypto key purpose
@@ -70,38 +78,35 @@ module "kms" {
module "kms" {
source = "./fabric/modules/kms"
project_id = "my-project"
- key_purpose = {
- key-c = {
+ keyring = {
+ location = "europe-west1"
+ name = "test"
+ }
+ keys = {
+ key-a = {
purpose = "ASYMMETRIC_SIGN"
version_template = {
algorithm = "EC_SIGN_P384_SHA384"
- protection_level = null
+ protection_level = "HSM"
}
}
}
- keyring = { location = "europe-west1", name = "test" }
- keys = { key-a = null, key-b = null, key-c = null }
}
-# tftest modules=1 resources=4
+# tftest modules=1 resources=2 inventory=purpose.yaml
```
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [keyring](variables.tf#L119) | Keyring attributes. | object({…})
| ✓ | |
-| [project_id](variables.tf#L142) | Project id where the keyring will be created. | string
| ✓ | |
+| [keyring](variables.tf#L54) | Keyring attributes. | object({…})
| ✓ | |
+| [project_id](variables.tf#L103) | Project id where the keyring will be created. | string
| ✓ | |
| [iam](variables.tf#L17) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_bindings](variables.tf#L23) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [iam_bindings_additive](variables.tf#L38) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [key_iam](variables.tf#L53) | Key IAM bindings in {KEY => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
-| [key_iam_bindings](variables.tf#L59) | Key authoritative IAM bindings in {KEY => {BINDING_KEY => {role = ROLE, members = [], condition = {}}}}. | map(object({…}))
| | {}
|
-| [key_iam_bindings_additive](variables.tf#L74) | Key individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
-| [key_purpose](variables.tf#L90) | Per-key purpose, if not set defaults will be used. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required. | map(object({…}))
| | {}
|
-| [key_purpose_defaults](variables.tf#L102) | Defaults used for key purpose when not defined at the key level. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required. | object({…})
| | {…}
|
-| [keyring_create](variables.tf#L127) | Set to false to manage keys and IAM bindings in an existing keyring. | bool
| | true
|
-| [keys](variables.tf#L133) | Key names and base attributes. Set attributes to null if not needed. | map(object({…}))
| | {}
|
-| [tag_bindings](variables.tf#L147) | Tag bindings for this keyring, in key => tag value id format. | map(string)
| | null
|
+| [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [keyring_create](variables.tf#L62) | Set to false to manage keys and IAM bindings in an existing keyring. | bool
| | true
|
+| [keys](variables.tf#L68) | Key names and base attributes. Set attributes to null if not needed. | map(object({…}))
| | {}
|
+| [tag_bindings](variables.tf#L108) | Tag bindings for this keyring, in key => tag value id format. | map(string)
| | {}
|
## Outputs
diff --git a/modules/kms/iam.tf b/modules/kms/iam.tf
index ff8279a254..8ac17a567d 100644
--- a/modules/kms/iam.tf
+++ b/modules/kms/iam.tf
@@ -16,25 +16,36 @@
locals {
key_iam = flatten([
- for key, roles in var.key_iam : [
- for role, members in roles : {
- key = key
+ for k, v in var.keys : [
+ for role, members in v.iam : {
+ key = k
role = role
members = members
}
]
])
- key_iam_bindings = flatten([
- for key, bindings in var.key_iam_bindings : [
- for binding_key, binding_data in bindings : {
- key = key
- binding_key = "${key}.${binding_key}"
- role = binding_data.role
- members = binding_data.members
- condition = binding_data.condition
+ key_iam_bindings = merge([
+ for k, v in var.keys : {
+ for binding_key, data in v.iam_bindings :
+ binding_key => {
+ key = k
+ role = data.role
+ members = data.members
+ condition = data.condition
}
- ]
- ])
+ }
+ ]...)
+ key_iam_bindings_additive = merge([
+ for k, v in var.keys : {
+ for binding_key, data in v.iam_bindings_additive :
+ binding_key => {
+ key = k
+ role = data.role
+ member = data.member
+ condition = data.condition
+ }
+ }
+ ]...)
}
resource "google_kms_key_ring_iam_binding" "authoritative" {
@@ -85,9 +96,7 @@ resource "google_kms_crypto_key_iam_binding" "authoritative" {
}
resource "google_kms_crypto_key_iam_binding" "bindings" {
- for_each = {
- for binding in local.key_iam_bindings : binding.binding_key => binding
- }
+ for_each = local.key_iam_bindings
role = each.value.role
crypto_key_id = google_kms_crypto_key.default[each.value.key].id
members = each.value.members
@@ -102,7 +111,7 @@ resource "google_kms_crypto_key_iam_binding" "bindings" {
}
resource "google_kms_crypto_key_iam_member" "members" {
- for_each = var.key_iam_bindings_additive
+ for_each = local.key_iam_bindings_additive
crypto_key_id = google_kms_crypto_key.default[each.value.key].id
role = each.value.role
member = each.value.member
diff --git a/modules/kms/main.tf b/modules/kms/main.tf
index 26624f1504..6be7c812da 100644
--- a/modules/kms/main.tf
+++ b/modules/kms/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -15,11 +15,6 @@
*/
locals {
- key_purpose = {
- for key, attrs in var.keys : key => try(
- var.key_purpose[key], var.key_purpose_defaults
- )
- }
keyring = (
var.keyring_create
? google_kms_key_ring.default.0
@@ -42,17 +37,19 @@ resource "google_kms_key_ring" "default" {
}
resource "google_kms_crypto_key" "default" {
- for_each = var.keys
- key_ring = local.keyring.id
- name = each.key
- rotation_period = try(each.value.rotation_period, null)
- labels = try(each.value.labels, null)
- purpose = try(local.key_purpose[each.key].purpose, null)
+ for_each = var.keys
+ key_ring = local.keyring.id
+ name = each.key
+ rotation_period = each.value.rotation_period
+ labels = each.value.labels
+ purpose = each.value.purpose
+ skip_initial_version_creation = each.value.skip_initial_version_creation
+
dynamic "version_template" {
- for_each = local.key_purpose[each.key].version_template == null ? [] : [""]
+ for_each = each.value.version_template == null ? [] : [""]
content {
- algorithm = local.key_purpose[each.key].version_template.algorithm
- protection_level = local.key_purpose[each.key].version_template.protection_level
+ algorithm = each.value.version_template.algorithm
+ protection_level = each.value.version_template.protection_level
}
}
}
diff --git a/modules/kms/tags.tf b/modules/kms/tags.tf
index 894c28aa39..c0955c6241 100644
--- a/modules/kms/tags.tf
+++ b/modules/kms/tags.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -15,7 +15,7 @@
*/
resource "google_tags_tag_binding" "binding" {
- for_each = coalesce(var.tag_bindings, {})
+ for_each = var.tag_bindings
parent = "//cloudresourcemanager.googleapis.com/${local.keyring.id}"
tag_value = each.value
}
diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf
index 93e42dc462..3086176417 100644
--- a/modules/kms/variables.tf
+++ b/modules/kms/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -18,6 +18,7 @@ variable "iam" {
description = "Keyring IAM bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
+ nullable = false
}
variable "iam_bindings" {
@@ -50,72 +51,6 @@ variable "iam_bindings_additive" {
default = {}
}
-variable "key_iam" {
- description = "Key IAM bindings in {KEY => {ROLE => [MEMBERS]}} format."
- type = map(map(list(string)))
- default = {}
-}
-
-variable "key_iam_bindings" {
- description = "Key authoritative IAM bindings in {KEY => {BINDING_KEY => {role = ROLE, members = [], condition = {}}}}."
- type = map(object({
- members = list(string)
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "key_iam_bindings_additive" {
- description = "Key individual additive IAM bindings. Keys are arbitrary."
- type = map(object({
- key = string
- member = string
- role = string
- condition = optional(object({
- expression = string
- title = string
- description = optional(string)
- }))
- }))
- nullable = false
- default = {}
-}
-
-variable "key_purpose" {
- description = "Per-key purpose, if not set defaults will be used. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required."
- type = map(object({
- purpose = string
- version_template = object({
- algorithm = string
- protection_level = string
- })
- }))
- default = {}
-}
-
-variable "key_purpose_defaults" {
- description = "Defaults used for key purpose when not defined at the key level. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required."
- type = object({
- purpose = string
- version_template = object({
- algorithm = string
- protection_level = string
- })
- })
- default = {
- purpose = null
- version_template = null
- }
-}
-
-# cf https://cloud.google.com/kms/docs/locations
-
variable "keyring" {
description = "Keyring attributes."
type = object({
@@ -133,10 +68,36 @@ variable "keyring_create" {
variable "keys" {
description = "Key names and base attributes. Set attributes to null if not needed."
type = map(object({
- rotation_period = string
- labels = map(string)
+ rotation_period = optional(string)
+ labels = optional(map(string))
+ purpose = optional(string, "ENCRYPT_DECRYPT")
+ skip_initial_version_creation = optional(bool, false)
+ version_template = optional(object({
+ algorithm = string
+ protection_level = optional(string, "SOFTWARE")
+ }))
+
+ iam = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ members = list(string)
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+ iam_bindings_additive = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
}))
- default = {}
+ default = {}
+ nullable = false
}
variable "project_id" {
@@ -147,5 +108,6 @@ variable "project_id" {
variable "tag_bindings" {
description = "Tag bindings for this keyring, in key => tag value id format."
type = map(string)
- default = null
+ default = {}
+ nullable = false
}
diff --git a/modules/pubsub/README.md b/modules/pubsub/README.md
index 8dd9e5ff32..69a18dbe35 100644
--- a/modules/pubsub/README.md
+++ b/modules/pubsub/README.md
@@ -61,16 +61,10 @@ module "pubsub" {
project_id = "my-project"
name = "my-topic"
subscriptions = {
- test-pull = null
+ test-pull = {}
test-pull-override = {
- labels = { test = "override" }
- options = {
- ack_deadline_seconds = null
- message_retention_duration = null
- retain_acked_messages = true
- expiration_policy_ttl = null
- filter = null
- }
+ labels = { test = "override" }
+ retain_acked_messages = true
}
}
}
@@ -87,13 +81,10 @@ module "pubsub" {
project_id = "my-project"
name = "my-topic"
subscriptions = {
- test-push = null
- }
- push_configs = {
test-push = {
- endpoint = "https://example.com/foo"
- attributes = null
- oidc_token = null
+ push = {
+ endpoint = "https://example.com/foo"
+ }
}
}
}
@@ -110,14 +101,13 @@ module "pubsub" {
project_id = "my-project"
name = "my-topic"
subscriptions = {
- test-bigquery = null
- }
- bigquery_subscription_configs = {
test-bigquery = {
- table = "my_project_id:my_dataset.my_table"
- use_topic_schema = true
- write_metadata = false
- drop_unknown_fields = true
+ bigquery = {
+ table = "my_project_id:my_dataset.my_table"
+ use_topic_schema = true
+ write_metadata = false
+ drop_unknown_fields = true
+ }
}
}
}
@@ -134,17 +124,16 @@ module "pubsub" {
project_id = "my-project"
name = "my-topic"
subscriptions = {
- test-cloudstorage = null
- }
- cloud_storage_subscription_configs = {
test-cloudstorage = {
- bucket = "my-bucket"
- filename_prefix = "test_prefix"
- filename_suffix = "test_suffix"
- max_duration = "100s"
- max_bytes = 1000
- avro_config = {
- write_metadata = true
+ cloud_storage = {
+ bucket = "my-bucket"
+ filename_prefix = "test_prefix"
+ filename_suffix = "test_suffix"
+ max_duration = "100s"
+ max_bytes = 1000
+ avro_config = {
+ write_metadata = true
+ }
}
}
}
@@ -159,12 +148,10 @@ module "pubsub" {
project_id = "my-project"
name = "my-topic"
subscriptions = {
- test-1 = null
- test-1 = null
- }
- subscription_iam = {
test-1 = {
- "roles/pubsub.subscriber" = ["user:user1@ludomagno.net"]
+ iam = {
+ "roles/pubsub.subscriber" = ["user:user1@example.com"]
+ }
}
}
}
@@ -175,30 +162,26 @@ module "pubsub" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L94) | PubSub topic name. | string
| ✓ | |
-| [project_id](variables.tf#L99) | Project used for resources. | string
| ✓ | |
-| [bigquery_subscription_configs](variables.tf#L17) | Configuration parameters for BigQuery subscriptions. | map(object({…}))
| | {}
|
-| [cloud_storage_subscription_configs](variables.tf#L28) | Configuration parameters for Cloud Storage subscriptions. | map(object({…}))
| | {}
|
-| [dead_letter_configs](variables.tf#L43) | Per-subscription dead letter policy configuration. | map(object({…}))
| | {}
|
-| [defaults](variables.tf#L52) | Subscription defaults for options. | object({…})
| | {…}
|
-| [iam](variables.tf#L70) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [kms_key](variables.tf#L76) | KMS customer managed encryption key. | string
| | null
|
-| [labels](variables.tf#L82) | Labels. | map(string)
| | {}
|
-| [message_retention_duration](variables.tf#L88) | Minimum duration to retain a message after it is published to the topic. | string
| | null
|
-| [push_configs](variables.tf#L104) | Push subscription configurations. | map(object({…}))
| | {}
|
-| [regions](variables.tf#L117) | List of regions used to set persistence policy. | list(string)
| | []
|
-| [schema](variables.tf#L123) | Topic schema. If set, all messages in this topic should follow this schema. | object({…})
| | null
|
-| [subscription_iam](variables.tf#L133) | IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
-| [subscriptions](variables.tf#L139) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…}))
| | {}
|
+| [name](variables.tf#L73) | PubSub topic name. | string
| ✓ | |
+| [project_id](variables.tf#L78) | Project used for resources. | string
| ✓ | |
+| [iam](variables.tf#L17) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…}))
| | {}
|
+| [kms_key](variables.tf#L54) | KMS customer managed encryption key. | string
| | null
|
+| [labels](variables.tf#L60) | Labels. | map(string)
| | {}
|
+| [message_retention_duration](variables.tf#L67) | Minimum duration to retain a message after it is published to the topic. | string
| | null
|
+| [regions](variables.tf#L83) | List of regions used to set persistence policy. | list(string)
| | []
|
+| [schema](variables.tf#L90) | Topic schema. If set, all messages in this topic should follow this schema. | object({…})
| | null
|
+| [subscriptions](variables.tf#L100) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…}))
| | {}
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Fully qualified topic id. | |
-| [schema](outputs.tf#L26) | Schema resource. | |
-| [schema_id](outputs.tf#L31) | Schema resource id. | |
-| [subscription_id](outputs.tf#L36) | Subscription ids. | |
-| [subscriptions](outputs.tf#L46) | Subscription resources. | |
-| [topic](outputs.tf#L54) | Topic resource. | |
+| [schema](outputs.tf#L27) | Schema resource. | |
+| [schema_id](outputs.tf#L32) | Schema resource id. | |
+| [subscription_id](outputs.tf#L37) | Subscription ids. | |
+| [subscriptions](outputs.tf#L48) | Subscription resources. | |
+| [topic](outputs.tf#L57) | Topic resource. | |
diff --git a/modules/pubsub/iam.tf b/modules/pubsub/iam.tf
new file mode 100644
index 0000000000..4e39b43a1b
--- /dev/null
+++ b/modules/pubsub/iam.tf
@@ -0,0 +1,140 @@
+/**
+ * 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 authoritative.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ subscription_iam = flatten([
+ for k, v in var.subscriptions : [
+ for role, members in v.iam : {
+ subscription = k
+ role = role
+ members = members
+ }
+ ]
+ ])
+ subscription_iam_bindings = merge([
+ for k, v in var.subscriptions : {
+ for binding_key, data in v.iam_bindings :
+ binding_key => {
+ subscription = k
+ role = data.role
+ members = data.members
+ condition = data.condition
+ }
+ }
+ ]...)
+ subscription_iam_bindings_additive = merge([
+ for k, v in var.subscriptions : {
+ for binding_key, data in v.iam_bindings_additive :
+ binding_key => {
+ subscription = k
+ role = data.role
+ member = data.member
+ condition = data.condition
+ }
+ }
+ ]...)
+}
+
+moved {
+ from = google_pubsub_topic_iam_binding.default
+ to = google_pubsub_topic_iam_binding.authoritative
+}
+
+resource "google_pubsub_topic_iam_binding" "authoritative" {
+ for_each = var.iam
+ project = var.project_id
+ topic = google_pubsub_topic.default.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_pubsub_topic_iam_binding" "bindings" {
+ for_each = var.iam_bindings
+ topic = google_pubsub_topic.default.name
+ role = each.value.role
+ members = each.value.members
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_pubsub_topic_iam_member" "bindings" {
+ for_each = var.iam_bindings_additive
+ topic = google_pubsub_topic.default.name
+ role = each.value.role
+ member = each.value.member
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+moved {
+ from = google_pubsub_subscription_iam_binding.default
+ to = google_pubsub_subscription_iam_binding.authoritative
+}
+
+resource "google_pubsub_subscription_iam_binding" "authoritative" {
+ for_each = {
+ for binding in local.subscription_iam :
+ "${binding.subscription}.${binding.role}" => binding
+ }
+ project = var.project_id
+ subscription = google_pubsub_subscription.default[each.value.subscription].name
+ role = each.value.role
+ members = each.value.members
+}
+
+resource "google_pubsub_subscription_iam_binding" "bindings" {
+ for_each = local.subscription_iam_bindings
+ project = var.project_id
+ subscription = google_pubsub_subscription.default[each.value.subscription].name
+ role = each.value.role
+ members = each.value.members
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_pubsub_subscription_iam_member" "members" {
+ for_each = local.subscription_iam_bindings_additive
+ project = var.project_id
+ subscription = google_pubsub_subscription.default[each.value.subscription].name
+ role = each.value.role
+ member = each.value.member
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
diff --git a/modules/pubsub/main.tf b/modules/pubsub/main.tf
index 2a7f873fee..de0650297c 100644
--- a/modules/pubsub/main.tf
+++ b/modules/pubsub/main.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -15,27 +15,6 @@
*/
locals {
- sub_iam_members = flatten([
- for sub, roles in var.subscription_iam : [
- for role, members in roles : {
- sub = sub
- role = role
- members = members
- }
- ]
- ])
- metadata_config = {
- for k, v in var.cloud_storage_subscription_configs : k => v.avro_config
- }
- oidc_config = {
- for k, v in var.push_configs : k => v.oidc_token
- }
- subscriptions = {
- for k, v in var.subscriptions : k => {
- labels = try(v.labels, v, null) == null ? var.labels : v.labels
- options = try(v.options, v, null) == null ? var.defaults : v.options
- }
- }
topic_id_static = "projects/${var.project_id}/topics/${var.name}"
}
@@ -70,94 +49,73 @@ resource "google_pubsub_topic" "default" {
}
}
-resource "google_pubsub_topic_iam_binding" "default" {
- for_each = var.iam
- project = var.project_id
- topic = google_pubsub_topic.default.name
- role = each.key
- members = each.value
-}
-
resource "google_pubsub_subscription" "default" {
- for_each = local.subscriptions
- project = var.project_id
- name = each.key
- topic = google_pubsub_topic.default.name
- labels = each.value.labels
- ack_deadline_seconds = each.value.options.ack_deadline_seconds
- message_retention_duration = each.value.options.message_retention_duration
- retain_acked_messages = each.value.options.retain_acked_messages
- filter = each.value.options.filter
+ for_each = var.subscriptions
+ project = var.project_id
+ name = each.key
+ topic = google_pubsub_topic.default.name
+ labels = each.value.labels
+ ack_deadline_seconds = each.value.ack_deadline_seconds
+ message_retention_duration = each.value.message_retention_duration
+ retain_acked_messages = each.value.retain_acked_messages
+ filter = each.value.filter
+ enable_message_ordering = each.value.enable_message_ordering
+ enable_exactly_once_delivery = each.value.enable_exactly_once_delivery
dynamic "expiration_policy" {
- for_each = each.value.options.expiration_policy_ttl == null ? [] : [""]
+ for_each = each.value.expiration_policy_ttl == null ? [] : [""]
content {
- ttl = each.value.options.expiration_policy_ttl
+ ttl = each.value.expiration_policy_ttl
}
}
dynamic "dead_letter_policy" {
- for_each = try(var.dead_letter_configs[each.key], null) == null ? [] : [""]
+ for_each = each.value.dead_letter_policy == null ? [] : [""]
content {
- dead_letter_topic = var.dead_letter_configs[each.key].topic
- max_delivery_attempts = var.dead_letter_configs[each.key].max_delivery_attempts
+ dead_letter_topic = each.value.dead_letter_policy.topic
+ max_delivery_attempts = each.value.dead_letter_policy.max_delivery_attempts
}
}
dynamic "push_config" {
- for_each = try(var.push_configs[each.key], null) == null ? [] : [""]
+ for_each = each.value.push == null ? [] : [""]
content {
- push_endpoint = var.push_configs[each.key].endpoint
- attributes = var.push_configs[each.key].attributes
+ push_endpoint = each.value.push.endpoint
+ attributes = each.value.push.attributes
dynamic "oidc_token" {
- for_each = (
- local.oidc_config[each.key] == null ? [] : [""]
- )
+ for_each = each.value.push.oidc_token == null ? [] : [""]
content {
- service_account_email = local.oidc_config[each.key].service_account_email
- audience = local.oidc_config[each.key].audience
+ service_account_email = each.value.push.oidc_token.service_account_email
+ audience = each.value.push.oidc_token.audience
}
}
}
}
dynamic "bigquery_config" {
- for_each = try(var.bigquery_subscription_configs[each.key], null) == null ? [] : [""]
+ for_each = each.value.bigquery == null ? [] : [""]
content {
- table = var.bigquery_subscription_configs[each.key].table
- use_topic_schema = var.bigquery_subscription_configs[each.key].use_topic_schema
- write_metadata = var.bigquery_subscription_configs[each.key].write_metadata
- drop_unknown_fields = var.bigquery_subscription_configs[each.key].drop_unknown_fields
+ table = each.value.bigquery.table
+ use_topic_schema = each.value.bigquery.use_topic_schema
+ write_metadata = each.value.bigquery.write_metadata
+ drop_unknown_fields = each.value.bigquery.drop_unknown_fields
}
}
dynamic "cloud_storage_config" {
- for_each = try(var.cloud_storage_subscription_configs[each.key], null) == null ? [] : [""]
+ for_each = each.value.cloud_storage == null ? [] : [""]
content {
- bucket = var.cloud_storage_subscription_configs[each.key].bucket
- filename_prefix = var.cloud_storage_subscription_configs[each.key].filename_prefix
- filename_suffix = var.cloud_storage_subscription_configs[each.key].filename_suffix
- max_duration = var.cloud_storage_subscription_configs[each.key].max_duration
- max_bytes = var.cloud_storage_subscription_configs[each.key].max_bytes
+ bucket = each.value.cloud_storage.bucket
+ filename_prefix = each.value.cloud_storage.filename_prefix
+ filename_suffix = each.value.cloud_storage.filename_suffix
+ max_duration = each.value.cloud_storage.max_duration
+ max_bytes = each.value.cloud_storage.max_bytes
dynamic "avro_config" {
- for_each = (
- local.metadata_config[each.key] == null ? [] : [""]
- )
+ for_each = each.value.cloud_storage.avro_config == null ? [] : [""]
content {
- write_metadata = local.metadata_config[each.key].write_metadata
+ write_metadata = each.value.cloud_storage.avro_config.write_metadata
}
}
}
}
}
-
-resource "google_pubsub_subscription_iam_binding" "default" {
- for_each = {
- for binding in local.sub_iam_members :
- "${binding.sub}.${binding.role}" => binding
- }
- project = var.project_id
- subscription = google_pubsub_subscription.default[each.value.sub].name
- role = each.value.role
- members = each.value.members
-}
diff --git a/modules/pubsub/outputs.tf b/modules/pubsub/outputs.tf
index 0d14930287..8218e2b335 100644
--- a/modules/pubsub/outputs.tf
+++ b/modules/pubsub/outputs.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -19,7 +19,8 @@ output "id" {
value = local.topic_id_static
depends_on = [
google_pubsub_topic.default,
- google_pubsub_topic_iam_binding.default
+ google_pubsub_topic_iam_binding.authoritative,
+ google_pubsub_topic_iam_binding.bindings
]
}
@@ -39,7 +40,8 @@ output "subscription_id" {
for k, v in google_pubsub_subscription.default : k => v.id
}
depends_on = [
- google_pubsub_subscription_iam_binding.default
+ google_pubsub_subscription_iam_binding.authoritative,
+ google_pubsub_subscription_iam_binding.bindings
]
}
@@ -47,7 +49,8 @@ output "subscriptions" {
description = "Subscription resources."
value = google_pubsub_subscription.default
depends_on = [
- google_pubsub_subscription_iam_binding.default
+ google_pubsub_subscription_iam_binding.authoritative,
+ google_pubsub_subscription_iam_binding.bindings
]
}
@@ -55,6 +58,7 @@ output "topic" {
description = "Topic resource."
value = google_pubsub_topic.default
depends_on = [
- google_pubsub_topic_iam_binding.default
+ google_pubsub_topic_iam_binding.authoritative,
+ google_pubsub_topic_iam_binding.bindings
]
}
diff --git a/modules/pubsub/variables.tf b/modules/pubsub/variables.tf
index 35dfc172dc..370c42fa77 100644
--- a/modules/pubsub/variables.tf
+++ b/modules/pubsub/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 Google LLC
+ * 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.
@@ -14,63 +14,41 @@
* limitations under the License.
*/
-variable "bigquery_subscription_configs" {
- description = "Configuration parameters for BigQuery subscriptions."
- type = map(object({
- table = string
- use_topic_schema = bool
- write_metadata = bool
- drop_unknown_fields = bool
- }))
- default = {}
+variable "iam" {
+ description = "IAM bindings for topic in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
}
-variable "cloud_storage_subscription_configs" {
- description = "Configuration parameters for Cloud Storage subscriptions."
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
type = map(object({
- bucket = string
- filename_prefix = string
- filename_suffix = string
- max_duration = string
- max_bytes = number
- avro_config = object({
- write_metadata = bool
- })
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
}))
- default = {}
+ nullable = false
+ default = {}
}
-variable "dead_letter_configs" {
- description = "Per-subscription dead letter policy configuration."
+variable "iam_bindings_additive" {
+ description = "Keyring individual additive IAM bindings. Keys are arbitrary."
type = map(object({
- topic = string
- max_delivery_attempts = number
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
}))
- default = {}
-}
-
-variable "defaults" {
- description = "Subscription defaults for options."
- type = object({
- ack_deadline_seconds = number
- message_retention_duration = string
- retain_acked_messages = bool
- expiration_policy_ttl = string
- filter = string
- })
- default = {
- ack_deadline_seconds = null
- message_retention_duration = null
- retain_acked_messages = null
- expiration_policy_ttl = null
- filter = null
- }
-}
-
-variable "iam" {
- description = "IAM bindings for topic in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
+ nullable = false
+ default = {}
}
variable "kms_key" {
@@ -83,6 +61,7 @@ variable "labels" {
description = "Labels."
type = map(string)
default = {}
+ nullable = false
}
variable "message_retention_duration" {
@@ -101,23 +80,11 @@ variable "project_id" {
type = string
}
-variable "push_configs" {
- description = "Push subscription configurations."
- type = map(object({
- attributes = map(string)
- endpoint = string
- oidc_token = object({
- audience = string
- service_account_email = string
- })
- }))
- default = {}
-}
-
variable "regions" {
description = "List of regions used to set persistence policy."
type = list(string)
default = []
+ nullable = false
}
variable "schema" {
@@ -130,23 +97,72 @@ variable "schema" {
default = null
}
-variable "subscription_iam" {
- description = "IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format."
- type = map(map(list(string)))
- default = {}
-}
-
variable "subscriptions" {
description = "Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null."
type = map(object({
- labels = map(string)
- options = object({
- ack_deadline_seconds = number
- message_retention_duration = string
- retain_acked_messages = bool
- expiration_policy_ttl = string
- filter = string
- })
+ labels = optional(map(string))
+ ack_deadline_seconds = optional(number)
+ message_retention_duration = optional(string)
+ retain_acked_messages = optional(bool, false)
+ expiration_policy_ttl = optional(string)
+ filter = optional(string)
+ enable_message_ordering = optional(bool, false)
+ enable_exactly_once_delivery = optional(bool, false)
+ dead_letter_policy = optional(object({
+ topic = string
+ max_delivery_attempts = optional(number)
+ }))
+ retry_policy = optional(object({
+ minimum_backoff = optional(number)
+ maximum_backoff = optional(number)
+ }))
+
+ bigquery = optional(object({
+ table = string
+ use_topic_schema = optional(bool, false)
+ write_metadata = optional(bool, false)
+ drop_unknown_fields = optional(bool, false)
+ }))
+ cloud_storage = optional(object({
+ bucket = string
+ filename_prefix = optional(string)
+ filename_suffix = optional(string)
+ max_duration = optional(string)
+ max_bytes = optional(number)
+ avro_config = optional(object({
+ write_metadata = optional(bool, false)
+ }))
+ }))
+ push = optional(object({
+ endpoint = string
+ attributes = optional(map(string))
+ no_wrapper = optional(bool, false)
+ oidc_token = optional(object({
+ audience = optional(string)
+ service_account_email = string
+ }))
+ }))
+
+ iam = optional(map(list(string)), {})
+ iam_bindings = optional(map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
+ iam_bindings_additive = optional(map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ })), {})
}))
- default = {}
+ default = {}
+ nullable = false
}
diff --git a/tests/modules/kms/examples/basic.yaml b/tests/modules/kms/examples/basic.yaml
index e29297a1f7..30f40627bc 100644
--- a/tests/modules/kms/examples/basic.yaml
+++ b/tests/modules/kms/examples/basic.yaml
@@ -18,37 +18,26 @@ values:
name: key-a
purpose: ENCRYPT_DECRYPT
rotation_period: null
- skip_initial_version_creation: null
- timeouts: null
+ skip_initial_version_creation: false
module.kms.google_kms_crypto_key.default["key-b"]:
labels: null
name: key-b
purpose: ENCRYPT_DECRYPT
rotation_period: 604800s
- skip_initial_version_creation: null
- timeouts: null
+ skip_initial_version_creation: false
module.kms.google_kms_crypto_key.default["key-c"]:
labels:
env: test
name: key-c
purpose: ENCRYPT_DECRYPT
rotation_period: null
- skip_initial_version_creation: null
- timeouts: null
- module.kms.google_kms_crypto_key_iam_binding.default["key-a.roles/cloudkms.admin"]:
+ skip_initial_version_creation: false
+ module.kms.google_kms_crypto_key_iam_binding.authoritative["key-a.roles/cloudkms.admin"]:
condition: []
members:
- user:user3@example.com
role: roles/cloudkms.admin
- ? module.kms.google_kms_crypto_key_iam_member.default["key-b.roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user4@example.com"]
- : condition: []
- member: user:user4@example.com
- role: roles/cloudkms.cryptoKeyEncrypterDecrypter
- ? module.kms.google_kms_crypto_key_iam_member.default["key-b.roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user5@example.com"]
- : condition: []
- member: user:user5@example.com
- role: roles/cloudkms.cryptoKeyEncrypterDecrypter
- module.kms.google_kms_crypto_key_iam_member.members["key-b-am1"]:
+ module.kms.google_kms_crypto_key_iam_member.members["key-b-iam1"]:
condition: []
member: user:am1@example.com
role: roles/cloudkms.cryptoKeyEncrypterDecrypter
@@ -56,23 +45,9 @@ values:
location: europe-west1
name: test
project: my-project
- timeouts: null
- module.kms.google_kms_key_ring_iam_member.default["roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user1@example.com"]:
- condition: []
- member: user:user1@example.com
- role: roles/cloudkms.cryptoKeyEncrypterDecrypter
- module.kms.google_kms_key_ring_iam_member.default["roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user2@example.com"]:
- condition: []
- member: user:user2@example.com
- role: roles/cloudkms.cryptoKeyEncrypterDecrypter
counts:
google_kms_crypto_key: 3
google_kms_crypto_key_iam_binding: 1
- google_kms_crypto_key_iam_member: 3
+ google_kms_crypto_key_iam_member: 1
google_kms_key_ring: 1
- google_kms_key_ring_iam_member: 2
- modules: 1
- resources: 10
-
-outputs: {}
diff --git a/tests/modules/kms/examples/purpose.yaml b/tests/modules/kms/examples/purpose.yaml
index c08779b220..9f97ad5298 100644
--- a/tests/modules/kms/examples/purpose.yaml
+++ b/tests/modules/kms/examples/purpose.yaml
@@ -15,25 +15,19 @@
values:
module.kms.google_kms_crypto_key.default["key-a"]:
name: key-a
- purpose: ENCRYPT_DECRYPT
- module.kms.google_kms_crypto_key.default["key-b"]:
- name: key-b
- purpose: ENCRYPT_DECRYPT
- module.kms.google_kms_crypto_key.default["key-c"]:
- name: key-c
purpose: ASYMMETRIC_SIGN
version_template:
- algorithm: EC_SIGN_P384_SHA384
- protection_level: SOFTWARE
+ protection_level: HSM
module.kms.google_kms_key_ring.default[0]:
location: europe-west1
name: test
project: my-project
counts:
- google_kms_crypto_key: 3
+ google_kms_crypto_key: 1
google_kms_key_ring: 1
modules: 1
- resources: 4
+ resources: 2
outputs: {}
diff --git a/tests/modules/pubsub/examples/simple.yaml b/tests/modules/pubsub/examples/simple.yaml
index 51094a51e5..6fe54ec689 100644
--- a/tests/modules/pubsub/examples/simple.yaml
+++ b/tests/modules/pubsub/examples/simple.yaml
@@ -16,14 +16,14 @@ values:
module.pubsub.google_pubsub_topic.default:
name: my-topic
project: my-project
- module.pubsub.google_pubsub_topic_iam_binding.default["roles/pubsub.subscriber"]:
+ module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.subscriber"]:
condition: []
members:
- user:user1@example.com
project: my-project
role: roles/pubsub.subscriber
topic: my-topic
- module.pubsub.google_pubsub_topic_iam_binding.default["roles/pubsub.viewer"]:
+ module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.viewer"]:
condition: []
members:
- group:foo@example.com
diff --git a/tests/modules/pubsub/examples/subscription-iam.yaml b/tests/modules/pubsub/examples/subscription-iam.yaml
index d0fa9fb62b..42ed25659b 100644
--- a/tests/modules/pubsub/examples/subscription-iam.yaml
+++ b/tests/modules/pubsub/examples/subscription-iam.yaml
@@ -13,10 +13,10 @@
# limitations under the License.
values:
- module.pubsub.google_pubsub_subscription_iam_binding.default["test-1.roles/pubsub.subscriber"]:
+ module.pubsub.google_pubsub_subscription_iam_binding.authoritative["test-1.roles/pubsub.subscriber"]:
condition: []
members:
- - user:user1@ludomagno.net
+ - user:user1@example.com
project: my-project
role: roles/pubsub.subscriber
subscription: test-1
diff --git a/tests/modules/pubsub/examples/subscriptions.yaml b/tests/modules/pubsub/examples/subscriptions.yaml
index a87a6d4756..b1a94212ea 100644
--- a/tests/modules/pubsub/examples/subscriptions.yaml
+++ b/tests/modules/pubsub/examples/subscriptions.yaml
@@ -16,22 +16,22 @@ values:
module.pubsub.google_pubsub_subscription.default["test-pull"]:
bigquery_config: []
dead_letter_policy: []
- enable_exactly_once_delivery: null
- enable_message_ordering: null
+ enable_exactly_once_delivery: False
+ enable_message_ordering: False
filter: null
labels: null
message_retention_duration: 604800s
name: test-pull
project: my-project
push_config: []
- retain_acked_messages: null
+ retain_acked_messages: False
retry_policy: []
topic: my-topic
module.pubsub.google_pubsub_subscription.default["test-pull-override"]:
bigquery_config: []
dead_letter_policy: []
- enable_exactly_once_delivery: null
- enable_message_ordering: null
+ enable_exactly_once_delivery: False
+ enable_message_ordering: False
filter: null
labels:
test: override
@@ -39,7 +39,7 @@ values:
name: test-pull-override
project: my-project
push_config: []
- retain_acked_messages: true
+ retain_acked_messages: True
retry_policy: []
topic: my-topic
module.pubsub.google_pubsub_topic.default: