From d9e1b924a190b94a3052d7a84cf41858a4bd916c Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 20 Jan 2026 15:37:35 +0100 Subject: [PATCH] Add `asset_feeds` to resman modules (#3658) * Add asset_feeds to resman modules * Add examples and update readmes * Extend pubsub_topic context to project and folder modules * Use pubsub_topic context for pubsub_destination * Update readmes and add project-factory asset_feed example * Update context tests * Update schemas --- fast/stages/0-org-setup/organization.tf | 1 + .../0-org-setup/schemas/folder.schema.json | 84 ++++++++++++++++ .../schemas/organization.schema.json | 84 ++++++++++++++++ .../0-org-setup/schemas/project.schema.json | 83 ++++++++++++++++ .../2-networking/schemas/folder.schema.json | 84 ++++++++++++++++ .../2-networking/schemas/project.schema.json | 83 ++++++++++++++++ .../schemas/folder.schema.json | 84 ++++++++++++++++ .../schemas/project.schema.json | 83 ++++++++++++++++ .../2-security/schemas/folder.schema.json | 84 ++++++++++++++++ .../2-security/schemas/project.schema.json | 83 ++++++++++++++++ modules/folder/README.md | 62 +++++++++--- modules/folder/assets.tf | 46 +++++++++ modules/folder/logging.tf | 39 ++++++-- modules/folder/variables.tf | 56 +++++++++-- modules/organization/README.md | 48 +++++++-- modules/organization/assets.tf | 46 +++++++++ modules/organization/variables.tf | 34 +++++++ modules/project-factory/README.md | 29 ++++-- modules/project-factory/folders.tf | 4 + modules/project-factory/projects-defaults.tf | 1 + modules/project-factory/projects.tf | 1 + .../schemas/folder.schema.json | 84 ++++++++++++++++ .../schemas/project.schema.json | 83 ++++++++++++++++ modules/project-factory/variables-folders.tf | 17 ++++ modules/project-factory/variables-projects.tf | 17 ++++ modules/project-factory/variables.tf | 1 + modules/project/README.md | 98 +++++++++++++------ modules/project/assets.tf | 46 +++++++++ modules/project/logging.tf | 36 +++++-- modules/project/variables.tf | 43 +++++++- tests/modules/folder/context.tfvars | 20 ++++ tests/modules/folder/context.yaml | 34 ++++++- tests/modules/folder/examples/feeds.yaml | 48 +++++++++ tests/modules/folder/examples/logging.yaml | 46 ++++++++- tests/modules/organization/context.tfvars | 10 ++ tests/modules/organization/context.yaml | 20 +++- .../modules/organization/examples/feeds.yaml | 50 ++++++++++ tests/modules/project/context.tfvars | 20 ++++ tests/modules/project/context.yaml | 39 +++++++- tests/modules/project/examples/feeds.yaml | 49 ++++++++++ tests/modules/project/examples/logging.yaml | 47 ++++++++- .../project_factory/examples/example.yaml | 91 ++++++++++++++--- tools/tfdoc.py | 43 ++++---- 43 files changed, 1935 insertions(+), 126 deletions(-) create mode 100644 modules/folder/assets.tf create mode 100644 modules/organization/assets.tf create mode 100644 modules/project/assets.tf create mode 100644 tests/modules/folder/examples/feeds.yaml create mode 100644 tests/modules/organization/examples/feeds.yaml create mode 100644 tests/modules/project/examples/feeds.yaml diff --git a/fast/stages/0-org-setup/organization.tf b/fast/stages/0-org-setup/organization.tf index 0786ded42..0ba906514 100644 --- a/fast/stages/0-org-setup/organization.tf +++ b/fast/stages/0-org-setup/organization.tf @@ -80,6 +80,7 @@ module "organization" { source = "../../../modules/organization" count = local.organization_id != null ? 1 : 0 organization_id = "organizations/${local.organization_id}" + asset_feeds = lookup(local.organization, "asset_feeds", {}) logging_settings = lookup(local.organization, "logging", null) context = { condition_vars = { diff --git a/fast/stages/0-org-setup/schemas/folder.schema.json b/fast/stages/0-org-setup/schemas/folder.schema.json index 998cfccc6..21e8b8d3b 100644 --- a/fast/stages/0-org-setup/schemas/folder.schema.json +++ b/fast/stages/0-org-setup/schemas/folder.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/0-org-setup/schemas/organization.schema.json b/fast/stages/0-org-setup/schemas/organization.schema.json index 15de3782e..e1d502d78 100644 --- a/fast/stages/0-org-setup/schemas/organization.schema.json +++ b/fast/stages/0-org-setup/schemas/organization.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "id": { "type": "string" }, diff --git a/fast/stages/0-org-setup/schemas/project.schema.json b/fast/stages/0-org-setup/schemas/project.schema.json index 9d20bebe8..2488b4fc7 100644 --- a/fast/stages/0-org-setup/schemas/project.schema.json +++ b/fast/stages/0-org-setup/schemas/project.schema.json @@ -4,6 +4,89 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-networking/schemas/folder.schema.json b/fast/stages/2-networking/schemas/folder.schema.json index 998cfccc6..21e8b8d3b 100644 --- a/fast/stages/2-networking/schemas/folder.schema.json +++ b/fast/stages/2-networking/schemas/folder.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-networking/schemas/project.schema.json b/fast/stages/2-networking/schemas/project.schema.json index 9d20bebe8..2488b4fc7 100644 --- a/fast/stages/2-networking/schemas/project.schema.json +++ b/fast/stages/2-networking/schemas/project.schema.json @@ -4,6 +4,89 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-project-factory/schemas/folder.schema.json b/fast/stages/2-project-factory/schemas/folder.schema.json index 998cfccc6..21e8b8d3b 100644 --- a/fast/stages/2-project-factory/schemas/folder.schema.json +++ b/fast/stages/2-project-factory/schemas/folder.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json index 9d20bebe8..2488b4fc7 100644 --- a/fast/stages/2-project-factory/schemas/project.schema.json +++ b/fast/stages/2-project-factory/schemas/project.schema.json @@ -4,6 +4,89 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-security/schemas/folder.schema.json b/fast/stages/2-security/schemas/folder.schema.json index 998cfccc6..21e8b8d3b 100644 --- a/fast/stages/2-security/schemas/folder.schema.json +++ b/fast/stages/2-security/schemas/folder.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-security/schemas/project.schema.json b/fast/stages/2-security/schemas/project.schema.json index 9d20bebe8..2488b4fc7 100644 --- a/fast/stages/2-security/schemas/project.schema.json +++ b/fast/stages/2-security/schemas/project.schema.json @@ -4,6 +4,89 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/modules/folder/README.md b/modules/folder/README.md index 28807e547..2ae84ba27 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -17,6 +17,7 @@ This module allows the creation and management of folders, including support for - [KMS Autokey](#kms-autokey) - [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) +- [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [Tags](#tags) - [Files](#files) - [Variables](#variables) @@ -568,6 +569,39 @@ cloudkmKeyRotationPeriod: - "cloudkms.googleapis.com/CryptoKey" ``` +## Cloud Asset Inventory Feeds + +Cloud Asset Inventory feeds allow you to monitor asset changes in real-time by publishing notifications to a Pub/Sub topic. Feeds configured at the folder level will monitor all resources within the folder and its subfolders. + +```hcl +module "pubsub" { + source = "./fabric/modules/pubsub" + project_id = var.project_id + name = "folder-asset-feed" +} + +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Monitored Folder" + asset_feeds = { + compute-instances = { + billing_project = var.project_id + feed_output_config = { + pubsub_destination = { + topic = module.pubsub.id + } + } + content_type = "RESOURCE" + asset_types = [ + "compute.googleapis.com/Instance" + ] + } + } +} +# tftest modules=2 resources=3 inventory=feeds.yaml +``` + ## Tags Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. @@ -604,6 +638,7 @@ module "folder" { | name | description | resources | |---|---|---| +| [assets.tf](./assets.tf) | None | google_cloud_asset_folder_feed | | [iam.tf](./iam.tf) | IAM bindings. | google_folder_iam_binding · google_folder_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_folder_iam_audit_config · google_logging_folder_exclusion · google_logging_folder_settings · 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_assured_workloads_workload · google_compute_firewall_policy_association · google_essential_contacts_contact · google_folder · google_kms_autokey_config | @@ -624,31 +659,32 @@ module "folder" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [assured_workload_config](variables.tf#L17) | Create AssuredWorkloads folder instead of regular folder when value is provided. Incompatible with folder_create=false. | object({…}) | | null | -| [autokey_config](variables.tf#L70) | Enable autokey support for this folder's children. Project accepts either project id or number. | object({…}) | | null | -| [contacts](variables.tf#L79) | 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)) | | {} | -| [context](variables.tf#L98) | Context-specific interpolations. | object({…}) | | {} | -| [deletion_protection](variables.tf#L114) | Deletion protection setting for this folder. | bool | | false | -| [factories_config](variables.tf#L120) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L131) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L140) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | +| [assured_workload_config](variables.tf#L51) | Create AssuredWorkloads folder instead of regular folder when value is provided. Incompatible with folder_create=false. | object({…}) | | null | +| [autokey_config](variables.tf#L104) | Enable autokey support for this folder's children. Project accepts either project id or number. | object({…}) | | null | +| [contacts](variables.tf#L113) | 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)) | | {} | +| [context](variables.tf#L132) | Context-specific interpolations. | object({…}) | | {} | +| [deletion_protection](variables.tf#L152) | Deletion protection setting for this folder. | bool | | false | +| [factories_config](variables.tf#L158) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L169) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L178) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [id](variables.tf#L150) | Folder ID in case you use folder_create=false. | string | | null | +| [id](variables.tf#L188) | Folder ID in case you use folder_create=false. | string | | null | | [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-logging.tf#L28) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L45) | Logging sinks to create for the folder. | map(object({…})) | | {} | -| [name](variables.tf#L156) | Folder name. | string | | null | -| [org_policies](variables.tf#L162) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [name](variables.tf#L194) | Folder name. | string | | null | +| [org_policies](variables.tf#L200) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L190) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [parent](variables.tf#L228) | Parent in folders/folder_id or organizations/org_id format. | string | | null | | [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L204) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [tag_bindings](variables.tf#L242) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/assets.tf b/modules/folder/assets.tf new file mode 100644 index 000000000..26aee4f43 --- /dev/null +++ b/modules/folder/assets.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2026 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. + */ + +resource "google_cloud_asset_folder_feed" "default" { + for_each = var.asset_feeds + billing_project = each.value.billing_project + folder = local.folder_id + feed_id = each.key + content_type = each.value.content_type + + asset_types = each.value.asset_types + asset_names = each.value.asset_names + + feed_output_config { + pubsub_destination { + topic = lookup( + local.ctx.pubsub_topics, + each.value.feed_output_config.pubsub_destination.topic, + each.value.feed_output_config.pubsub_destination.topic + ) + } + } + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [each.value.condition] + content { + expression = condition.value.expression + title = condition.value.title + description = condition.value.description + location = condition.value.location + } + } +} diff --git a/modules/folder/logging.tf b/modules/folder/logging.tf index c050e2251..a44f62b26 100644 --- a/modules/folder/logging.tf +++ b/modules/folder/logging.tf @@ -19,18 +19,39 @@ locals { logging_sinks = { for k, v in var.logging_sinks : - # rewrite destination and type when type="project" - k => merge(v, v.type != "project" ? {} : { - destination = "projects/${v.destination}" - type = "logging" - }) + # expand destination contexts + k => merge(v, + v.type != "bigquery" ? {} : { + destination = lookup( + local.ctx.bigquery_datasets, v.destination, v.destination + ) + }, + v.type != "logging" ? {} : { + destination = lookup( + local.ctx.log_buckets, v.destination, v.destination + ) + }, + v.type != "project" ? {} : { + api = "logging" + destination = "projects/${lookup(local.ctx.project_ids, v.destination, v.destination)}" + }, + v.type != "pubsub" ? {} : { + destination = lookup( + local.ctx.pubsub_topics, v.destination, v.destination + ) + }, + v.type != "storage" ? {} : { + destination = lookup( + local.ctx.storage_buckets, v.destination, v.destination + ) + } + ) } sink_bindings = { for type in ["bigquery", "logging", "project", "pubsub", "storage"] : type => { - for name, sink in var.logging_sinks : - name => sink - if sink.iam == true && sink.type == type + for name, sink in local.logging_sinks : + name => sink if sink.iam && sink.type == type } } } @@ -63,7 +84,7 @@ resource "google_logging_folder_sink" "sink" { name = each.key description = coalesce(each.value.description, "${each.key} (Terraform-managed).") folder = local.folder_id - destination = "${each.value.type}.googleapis.com/${each.value.destination}" + destination = "${lookup(each.value, "api", each.value.type)}.googleapis.com/${each.value.destination}" filter = each.value.filter include_children = each.value.include_children intercept_children = each.value.intercept_children diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 2f97d0c26..9beadb855 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2025 Google LLC + * Copyright 2026 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,6 +14,40 @@ * limitations under the License. */ + +variable "asset_feeds" { + description = "Cloud Asset Inventory feeds." + type = map(object({ + billing_project = string + content_type = optional(string) + asset_types = optional(list(string)) + asset_names = optional(list(string)) + feed_output_config = object({ + pubsub_destination = object({ + topic = string + }) + }) + condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.asset_feeds : + v.content_type == null || contains( + ["RESOURCE", "IAM_POLICY", "ORG_POLICY", "ACCESS_POLICY", "OS_INVENTORY", "RELATIONSHIP"], + v.content_type + ) + ]) + error_message = "Content type must be one of RESOURCE, IAM_POLICY, ORG_POLICY, ACCESS_POLICY, OS_INVENTORY, RELATIONSHIP." + } +} + variable "assured_workload_config" { description = "Create AssuredWorkloads folder instead of regular folder when value is provided. Incompatible with folder_create=false." type = object({ @@ -98,14 +132,18 @@ variable "contacts" { variable "context" { description = "Context-specific interpolations." type = object({ - condition_vars = optional(map(map(string)), {}) - custom_roles = optional(map(string), {}) - email_addresses = optional(map(string), {}) - folder_ids = optional(map(string), {}) - iam_principals = optional(map(string), {}) - project_ids = optional(map(string), {}) - project_numbers = optional(map(string), {}) - tag_values = optional(map(string), {}) + bigquery_datasets = optional(map(string), {}) + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + email_addresses = optional(map(string), {}) + folder_ids = optional(map(string), {}) + iam_principals = optional(map(string), {}) + log_buckets = optional(map(string), {}) + project_ids = optional(map(string), {}) + project_numbers = optional(map(string), {}) + pubsub_topics = optional(map(string), {}) + storage_buckets = optional(map(string), {}) + tag_values = optional(map(string), {}) }) default = {} nullable = false diff --git a/modules/organization/README.md b/modules/organization/README.md index 6fdf0fc49..cbabee0fe 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -31,6 +31,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [Custom Roles Factory](#custom-roles-factory) - [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) +- [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [Tags](#tags) - [Tags Factory](#tags-factory) - [Workforce Identity](#workforce-identity) @@ -578,6 +579,35 @@ cloudkmKeyRotationPeriod: - "cloudkms.googleapis.com/CryptoKey" ``` +## Cloud Asset Inventory Feeds + +Cloud Asset Inventory feeds allow you to monitor asset changes in real-time by publishing notifications to a Pub/Sub topic. Feeds configured at the organization level will monitor all resources within the organization. + +```hcl +module "pubsub" { + source = "./fabric/modules/pubsub" + project_id = var.project_id + name = "org-asset-feed" +} + +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + asset_feeds = { + security-monitoring = { + billing_project = var.project_id + feed_output_config = { + pubsub_destination = { + topic = module.pubsub.id + } + } + content_type = "IAM_POLICY" + } + } +} +# tftest inventory=feeds.yaml +``` + ## Tags Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. @@ -808,6 +838,7 @@ module "org" { | name | description | resources | |---|---|---| +| [assets.tf](./assets.tf) | None | google_cloud_asset_organization_feed | | [iam.tf](./iam.tf) | IAM bindings. | google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member | | [identity-providers.tf](./identity-providers.tf) | Workforce Identity Federation provider definitions. | google_iam_workforce_pool · google_iam_workforce_pool_provider | | [logging.tf](./logging.tf) | Log sinks and data access logs. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_settings · google_logging_organization_sink · google_organization_iam_audit_config · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | @@ -832,12 +863,13 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L127) | Organization id in organizations/nnnnnn format. | string | ✓ | | -| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | -| [context](variables.tf#L35) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L55) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L62) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L76) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [organization_id](variables.tf#L161) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | +| [contacts](variables.tf#L51) | 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)) | | {} | +| [context](variables.tf#L69) | Context-specific interpolations. | object({…}) | | {} | +| [custom_roles](variables.tf#L89) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [factories_config](variables.tf#L96) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L110) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | @@ -849,8 +881,8 @@ module "org" { | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L46) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L85) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L113) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L119) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L147) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | diff --git a/modules/organization/assets.tf b/modules/organization/assets.tf new file mode 100644 index 000000000..55072d087 --- /dev/null +++ b/modules/organization/assets.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2026 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. + */ + +resource "google_cloud_asset_organization_feed" "default" { + for_each = var.asset_feeds + billing_project = each.value.billing_project + org_id = var.organization_id + feed_id = each.key + content_type = each.value.content_type + + asset_types = each.value.asset_types + asset_names = each.value.asset_names + + feed_output_config { + pubsub_destination { + topic = lookup( + local.ctx.pubsub_topics, + each.value.feed_output_config.pubsub_destination.topic, + each.value.feed_output_config.pubsub_destination.topic + ) + } + } + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [each.value.condition] + content { + expression = condition.value.expression + title = condition.value.title + description = condition.value.description + location = condition.value.location + } + } +} diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index fb4c81f47..fcda66109 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -14,6 +14,40 @@ * limitations under the License. */ + +variable "asset_feeds" { + description = "Cloud Asset Inventory feeds." + type = map(object({ + billing_project = string + content_type = optional(string) + asset_types = optional(list(string)) + asset_names = optional(list(string)) + feed_output_config = object({ + pubsub_destination = object({ + topic = string + }) + }) + condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.asset_feeds : + v.content_type == null || contains( + ["RESOURCE", "IAM_POLICY", "ORG_POLICY", "ACCESS_POLICY", "OS_INVENTORY", "RELATIONSHIP"], + v.content_type + ) + ]) + error_message = "Content type must be one of RESOURCE, IAM_POLICY, ORG_POLICY, ACCESS_POLICY, OS_INVENTORY, RELATIONSHIP." + } +} + 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)) diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md index 279a5e65a..d38fecddd 100644 --- a/modules/project-factory/README.md +++ b/modules/project-factory/README.md @@ -418,6 +418,12 @@ module "project-factory" { iam_principals = { gcp-devops = "group:gcp-devops@example.org" } + project_ids = { + feeds-project = "my-cai-feeds-project" + } + pubsub_topics = { + feeds-topic = "projects/my-cai-feeds-project/topics/feed" + } tag_values = { "context/gke" = "tagValues/654321" "org-policies/drs-allow-all" = "tagValues/123456" @@ -532,6 +538,15 @@ name: Test ```yaml name: App X +asset_feeds: + compute-instances: + billing_project: $project_ids:feeds-project + feed_output_config: + pubsub_destination: + topic: $pubsub_topics:feeds-topic + content_type: RESOURCE + asset_types: + - compute.googleapis.com/Instance # tftest-file id=2.3 path=data/hierarchy/team-c/apps/test/app-x/.config.yaml schema=folder.schema.json ``` @@ -822,14 +837,14 @@ compute.disableSerialPortAccess: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factories_config](variables.tf#L162) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | -| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | -| [data_defaults](variables.tf#L39) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L104) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L123) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | -| [folders](variables-folders.tf#L17) | Folders data merged with factory data. | map(object({…})) | | {} | +| [factories_config](variables.tf#L163) | Path to folder with YAML resource description data files. | object({…}) | ✓ | | +| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} | +| [data_defaults](variables.tf#L40) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L105) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | +| [data_overrides](variables.tf#L124) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [folders](variables-folders.tf#L17) | Folders data merged with factory data. | map(object({…})) | | {} | | [notification_channels](variables-billing.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} | -| [projects](variables-projects.tf#L17) | Projects data merged with factory data. | map(object({…})) | | {} | +| [projects](variables-projects.tf#L17) | Projects data merged with factory data. | map(object({…})) | | {} | ## Outputs diff --git a/modules/project-factory/folders.tf b/modules/project-factory/folders.tf index 6cb8fcc30..656900ffc 100644 --- a/modules/project-factory/folders.tf +++ b/modules/project-factory/folders.tf @@ -58,6 +58,7 @@ module "folder-1" { for k, v in local.folders_input : k => v if v.level == 1 } deletion_protection = lookup(each.value, "deletion_protection", false) + asset_feeds = lookup(each.value, "asset_feeds", {}) parent = coalesce(each.value.parent, "$folder_ids:default") name = each.value.name factories_config = { @@ -101,6 +102,7 @@ module "folder-2" { for k, v in local.folders_input : k => v if v.level == 2 } deletion_protection = lookup(each.value, "deletion_protection", false) + asset_feeds = lookup(each.value, "asset_feeds", {}) parent = coalesce( each.value.parent, "$folder_ids:${each.value.parent_key}" ) @@ -154,6 +156,7 @@ module "folder-3" { for k, v in local.folders_input : k => v if v.level == 3 } deletion_protection = lookup(each.value, "deletion_protection", false) + asset_feeds = lookup(each.value, "asset_feeds", {}) parent = coalesce( each.value.parent, "$folder_ids:${each.value.parent_key}" ) @@ -207,6 +210,7 @@ module "folder-4" { for k, v in local.folders_input : k => v if v.level == 4 } deletion_protection = lookup(each.value, "deletion_protection", false) + asset_feeds = lookup(each.value, "asset_feeds", {}) parent = coalesce( each.value.parent, "$folder_ids:${each.value.parent_key}" ) diff --git a/modules/project-factory/projects-defaults.tf b/modules/project-factory/projects-defaults.tf index dcdbb909c..a29ee9b32 100644 --- a/modules/project-factory/projects-defaults.tf +++ b/modules/project-factory/projects-defaults.tf @@ -34,6 +34,7 @@ locals { # set data_overrides. to "", [] or {} to ensure, that empty value is always passed, or do # the same in _projects_input to prevent falling back to default value for k, v in local._projects_input : k => merge(v, { + asset_feeds = try(v.asset_feeds, {}) billing_account = try(coalesce( # type: string local.data_defaults.overrides.billing_account, try(v.billing_account, null), diff --git a/modules/project-factory/projects.tf b/modules/project-factory/projects.tf index 3e59718bd..cb7af607a 100644 --- a/modules/project-factory/projects.tf +++ b/modules/project-factory/projects.tf @@ -87,6 +87,7 @@ module "projects" { prefix = each.value.prefix project_reuse = each.value.project_reuse alerts = try(each.value.alerts, null) + asset_feeds = each.value.asset_feeds auto_create_network = try(each.value.auto_create_network, false) compute_metadata = try(each.value.compute_metadata, {}) # TODO: concat lists for each key diff --git a/modules/project-factory/schemas/folder.schema.json b/modules/project-factory/schemas/folder.schema.json index 998cfccc6..21e8b8d3b 100644 --- a/modules/project-factory/schemas/folder.schema.json +++ b/modules/project-factory/schemas/folder.schema.json @@ -4,6 +4,90 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "billing_project", + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json index 9d20bebe8..2488b4fc7 100644 --- a/modules/project-factory/schemas/project.schema.json +++ b/modules/project-factory/schemas/project.schema.json @@ -4,6 +4,89 @@ "type": "object", "additionalProperties": false, "properties": { + "asset_feeds": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "feed_output_config" + ], + "properties": { + "billing_project": { + "type": "string" + }, + "content_type": { + "type": "string", + "enum": [ + "RESOURCE", + "IAM_POLICY", + "ORG_POLICY", + "ACCESS_POLICY", + "OS_INVENTORY", + "RELATIONSHIP" + ] + }, + "asset_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "asset_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "feed_output_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "pubsub_destination" + ], + "properties": { + "pubsub_destination": { + "type": "object", + "additionalProperties": false, + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string" + } + } + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + } + } + } + } + }, "automation": { "type": "object", "additionalProperties": false, diff --git a/modules/project-factory/variables-folders.tf b/modules/project-factory/variables-folders.tf index bf50f926c..bc1c368d8 100644 --- a/modules/project-factory/variables-folders.tf +++ b/modules/project-factory/variables-folders.tf @@ -17,6 +17,23 @@ variable "folders" { description = "Folders data merged with factory data." type = map(object({ + asset_feeds = optional(map(object({ + billing_project = string + content_type = optional(string) + asset_types = optional(list(string)) + asset_names = optional(list(string)) + feed_output_config = object({ + pubsub_destination = object({ + topic = string + }) + }) + condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + })), {}) name = optional(string) parent = optional(string) deletion_protection = optional(bool) diff --git a/modules/project-factory/variables-projects.tf b/modules/project-factory/variables-projects.tf index 1177f0dbc..d63c7d2e5 100644 --- a/modules/project-factory/variables-projects.tf +++ b/modules/project-factory/variables-projects.tf @@ -17,6 +17,23 @@ variable "projects" { description = "Projects data merged with factory data." type = map(object({ + asset_feeds = optional(map(object({ + billing_project = optional(string) + content_type = optional(string) + asset_types = optional(list(string)) + asset_names = optional(list(string)) + feed_output_config = object({ + pubsub_destination = object({ + topic = string + }) + }) + condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + })), {}) automation = optional(object({ project = string bucket = optional(object({ diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf index 64b471889..f9e68249f 100644 --- a/modules/project-factory/variables.tf +++ b/modules/project-factory/variables.tf @@ -28,6 +28,7 @@ variable "context" { notification_channels = optional(map(string), {}) project_ids = optional(map(string), {}) project_numbers = optional(map(string), {}) + pubsub_topics = optional(map(string), {}) tag_values = optional(map(string), {}) vpc_host_projects = optional(map(string), {}) vpc_sc_perimeters = optional(map(string), {}) diff --git a/modules/project/README.md b/modules/project/README.md index 655952bc9..01a5c5fff 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -37,6 +37,7 @@ This module implements the creation and management of one GCP project including - [Privileged Access Manager (PAM) Entitlements Factory](#privileged-access-manager-pam-entitlements-factory) - [VPC Service Controls](#vpc-service-controls) - [Default compute network tier](#default-compute-network-tier) +- [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [BigQuery Reservations](#bigquery-reservations) - [Project Related Outputs](#project-related-outputs) - [Managing project related configuration without creating it](#managing-project-related-configuration-without-creating-it) @@ -1588,6 +1589,43 @@ module "project" { # tftest modules=1 resources=4 ``` +## Cloud Asset Inventory Feeds + +Cloud Asset Inventory feeds allow you to monitor asset changes in real-time by publishing notifications to a Pub/Sub topic. Feeds can be configured to monitor specific asset types, filter by conditions, and export different content types. + +```hcl +module "pubsub" { + source = "./fabric/modules/pubsub" + project_id = var.project_id + name = "asset-feed" +} + +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + parent = var.folder_id + prefix = var.prefix + services = [ + "cloudasset.googleapis.com" + ] + asset_feeds = { + compute-instances = { + feed_output_config = { + pubsub_destination = { + topic = module.pubsub.id + } + } + content_type = "RESOURCE" + asset_types = [ + "compute.googleapis.com/Instance" + ] + } + } +} +# tftest modules=2 resources=6 inventory=feeds.yaml +``` + ## BigQuery Reservations BigQuery reservations are primarily used to manage and allocate dedicated compute capacity for running queries, which helps provide predictable and consistent performance and costs. You can configure BigQuery reservations and assign projects, folders, or organizations to them by specifying the job type. @@ -2134,6 +2172,7 @@ module "project" { | name | description | resources | |---|---|---| | [alerts.tf](./alerts.tf) | None | google_monitoring_alert_policy | +| [assets.tf](./assets.tf) | None | google_cloud_asset_project_feed | | [bigquery-reservation.tf](./bigquery-reservation.tf) | None | google_bigquery_reservation · google_bigquery_reservation_assignment | | [cmek.tf](./cmek.tf) | Service Agent IAM Bindings for CMEK | google_kms_crypto_key_iam_member | | [iam.tf](./iam.tf) | IAM bindings. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | @@ -2166,29 +2205,30 @@ module "project" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L200) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L237) | Project name and id suffix. | string | ✓ | | | [alerts](variables-observability.tf#L17) | Monitoring alerts. | map(object({…})) | | {} | -| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | -| [bigquery_reservations](variables.tf#L23) | BigQuery reservations and assignments. Assignment specified as {JOB_TYPE = ['projects/PROJECT_ID']}. | map(object({…})) | | {} | -| [billing_account](variables.tf#L60) | Billing account id. | string | | null | -| [compute_metadata](variables.tf#L66) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | -| [contacts](variables.tf#L73) | 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)) | | {} | -| [context](variables.tf#L91) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L111) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_network_tier](variables.tf#L118) | Default compute network tier for the project. | string | | null | -| [default_service_account](variables.tf#L124) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [deletion_policy](variables.tf#L137) | Deletion policy setting for this project. | string | | "DELETE" | -| [descriptive_name](variables.tf#L148) | Descriptive project name. Set when name differs from project id. | string | | null | -| [factories_config](variables.tf#L154) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | +| [auto_create_network](variables.tf#L51) | Whether to create the default network for the project. | bool | | false | +| [bigquery_reservations](variables.tf#L57) | BigQuery reservations and assignments. Assignment specified as {JOB_TYPE = ['projects/PROJECT_ID']}. | map(object({…})) | | {} | +| [billing_account](variables.tf#L94) | Billing account id. | string | | null | +| [compute_metadata](variables.tf#L100) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | +| [contacts](variables.tf#L107) | 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)) | | {} | +| [context](variables.tf#L125) | Context-specific interpolations. | object({…}) | | {} | +| [custom_roles](variables.tf#L148) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [default_network_tier](variables.tf#L155) | Default compute network tier for the project. | string | | null | +| [default_service_account](variables.tf#L161) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [deletion_policy](variables.tf#L174) | Deletion policy setting for this project. | string | | "DELETE" | +| [descriptive_name](variables.tf#L185) | Descriptive project name. Set when name differs from project id. | string | | null | +| [factories_config](variables.tf#L191) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [kms_autokeys](variables.tf#L169) | KMS Autokey key handles. | map(object({…})) | | {} | -| [labels](variables.tf#L187) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L194) | If non-empty, creates a project lien with this description. | string | | null | +| [kms_autokeys](variables.tf#L206) | KMS Autokey key handles. | map(object({…})) | | {} | +| [labels](variables.tf#L224) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L231) | If non-empty, creates a project lien with this description. | string | | null | | [log_scopes](variables-observability.tf#L117) | Log scopes under this project. | map(object({…})) | | {} | | [logging_data_access](variables-observability.tf#L127) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-observability.tf#L138) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | @@ -2197,25 +2237,25 @@ module "project" { | [metric_scopes](variables-observability.tf#L216) | List of projects that will act as metric scopes for this project. | list(string) | | [] | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [notification_channels](variables-observability.tf#L223) | Monitoring notification channels. | map(object({…})) | | {} | -| [org_policies](variables.tf#L205) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L242) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L233) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L247) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L257) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [parent](variables.tf#L270) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L284) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L294) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | | [quotas](variables-quotas.tf#L17) | Service quota configuration. | map(object({…})) | | {} | | [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L277) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L288) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L300) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L307) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L313) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L323) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L360) | Deprecated. Use deletion_policy. | bool | | null | +| [service_agents_config](variables.tf#L314) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L325) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L337) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L344) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L350) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L360) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L397) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L96) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [tags_config](variables-tags.tf#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | -| [universe](variables.tf#L372) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L383) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L409) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L420) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | | [workload_identity_pools](variables-identity-providers.tf#L17) | Workload Identity Federation pools and providers. | map(object({…})) | | {} | ## Outputs diff --git a/modules/project/assets.tf b/modules/project/assets.tf new file mode 100644 index 000000000..1db6a5c70 --- /dev/null +++ b/modules/project/assets.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2026 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. + */ + +resource "google_cloud_asset_project_feed" "default" { + for_each = var.asset_feeds + project = local.project.project_id + billing_project = coalesce(each.value.billing_project, local.project.project_id) + feed_id = each.key + content_type = each.value.content_type + + asset_types = each.value.asset_types + asset_names = each.value.asset_names + + feed_output_config { + pubsub_destination { + topic = lookup( + local.ctx.pubsub_topics, + each.value.feed_output_config.pubsub_destination.topic, + each.value.feed_output_config.pubsub_destination.topic + ) + } + } + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [each.value.condition] + content { + expression = condition.value.expression + title = condition.value.title + description = condition.value.description + location = condition.value.location + } + } +} diff --git a/modules/project/logging.tf b/modules/project/logging.tf index 7c5d0aa8f..fea740125 100644 --- a/modules/project/logging.tf +++ b/modules/project/logging.tf @@ -19,16 +19,38 @@ locals { logging_sinks = { for k, v in var.logging_sinks : - # rewrite destination and type when type="project" - k => merge(v, v.type != "project" ? {} : { - destination = "projects/${v.destination}" - type = "logging" - }) + # expand destination contexts + k => merge(v, + v.type != "bigquery" ? {} : { + destination = lookup( + local.ctx.bigquery_datasets, v.destination, v.destination + ) + }, + v.type != "logging" ? {} : { + destination = lookup( + local.ctx.log_buckets, v.destination, v.destination + ) + }, + v.type != "project" ? {} : { + api = "logging" + destination = "projects/${lookup(local.ctx.project_ids, v.destination, v.destination)}" + }, + v.type != "pubsub" ? {} : { + destination = lookup( + local.ctx.pubsub_topics, v.destination, v.destination + ) + }, + v.type != "storage" ? {} : { + destination = lookup( + local.ctx.storage_buckets, v.destination, v.destination + ) + } + ) } sink_bindings = { for type in ["bigquery", "logging", "project", "pubsub", "storage"] : type => { - for name, sink in var.logging_sinks : + for name, sink in local.logging_sinks : name => sink if sink.iam && sink.type == type } } @@ -66,7 +88,7 @@ resource "google_logging_project_sink" "sink" { name = each.key description = coalesce(each.value.description, "${each.key} (Terraform-managed).") project = local.project.project_id - destination = "${each.value.type}.googleapis.com/${each.value.destination}" + destination = "${lookup(each.value, "api", each.value.type)}.googleapis.com/${each.value.destination}" filter = each.value.filter unique_writer_identity = each.value.unique_writer disabled = each.value.disabled diff --git a/modules/project/variables.tf b/modules/project/variables.tf index e878675e6..098ec8f68 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2025 Google LLC + * Copyright 2026 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,6 +14,40 @@ * limitations under the License. */ + +variable "asset_feeds" { + description = "Cloud Asset Inventory feeds." + type = map(object({ + billing_project = optional(string) + content_type = optional(string) + asset_types = optional(list(string)) + asset_names = optional(list(string)) + feed_output_config = object({ + pubsub_destination = object({ + topic = string + }) + }) + condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.asset_feeds : + v.content_type == null || contains( + ["RESOURCE", "IAM_POLICY", "ORG_POLICY", "ACCESS_POLICY", "OS_INVENTORY", "RELATIONSHIP"], + v.content_type + ) + ]) + error_message = "Content type must be one of RESOURCE, IAM_POLICY, ORG_POLICY, ACCESS_POLICY, OS_INVENTORY, RELATIONSHIP." + } +} + variable "auto_create_network" { description = "Whether to create the default network for the project." type = bool @@ -91,15 +125,18 @@ variable "contacts" { variable "context" { description = "Context-specific interpolations." type = object({ + bigquery_datasets = optional(map(string), {}) condition_vars = optional(map(map(string)), {}) custom_roles = optional(map(string), {}) email_addresses = optional(map(string), {}) folder_ids = optional(map(string), {}) - kms_keys = optional(map(string), {}) iam_principals = optional(map(string), {}) - notification_channels = optional(map(string), {}) + kms_keys = optional(map(string), {}) log_buckets = optional(map(string), {}) + notification_channels = optional(map(string), {}) project_ids = optional(map(string), {}) + pubsub_topics = optional(map(string), {}) + storage_buckets = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) vpc_sc_perimeters = optional(map(string), {}) diff --git a/tests/modules/folder/context.tfvars b/tests/modules/folder/context.tfvars index d6fcf1f17..edb15a6b0 100644 --- a/tests/modules/folder/context.tfvars +++ b/tests/modules/folder/context.tfvars @@ -21,10 +21,23 @@ context = { mysa = "serviceAccount:test@test-project.iam.gserviceaccount.com" myuser = "user:test-user@example.com" } + pubsub_topics = { + test = "projects/test-prod-audit-logs-0/topics/audit-logs" + } tag_values = { "test/one" = "tagValues/1234567890" } } +asset_feeds = { + test = { + billing_project = "test-project" + feed_output_config = { + pubsub_destination = { + topic = "$pubsub_topics:test" + } + } + } +} contacts = { "$email_addresses:default" = ["ALL"] } @@ -82,6 +95,13 @@ logging_data_access = { DATA_READ = {} } } +logging_sinks = { + test-pubsub = { + destination = "$pubsub_topics:test" + filter = "log_id('cloudaudit.googleapis.com/activity')" + type = "pubsub" + } +} pam_entitlements = { net-admins = { max_request_duration = "3600s" diff --git a/tests/modules/folder/context.yaml b/tests/modules/folder/context.yaml index 17aa0b78b..08aec8d75 100644 --- a/tests/modules/folder/context.yaml +++ b/tests/modules/folder/context.yaml @@ -13,16 +13,29 @@ # limitations under the License. values: + google_cloud_asset_folder_feed.default["test"]: + asset_names: null + asset_types: null + billing_project: test-project + condition: [] + content_type: null + feed_id: test + feed_output_config: + - pubsub_destination: + - topic: projects/test-prod-audit-logs-0/topics/audit-logs + timeouts: null google_essential_contacts_contact.contact["$email_addresses:default"]: email: foo@example.com language_tag: en notification_category_subscriptions: - ALL + timeouts: null google_folder.folder[0]: deletion_protection: false display_name: Test Context parent: organizations/1234567890 tags: null + timeouts: null google_folder_iam_audit_config.default["allServices"]: audit_log_config: - exempted_members: @@ -83,6 +96,15 @@ values: condition: [] member: user:test-user@example.com role: organizations/366118655033/roles/myRoleTwo + google_logging_folder_sink.sink["test-pubsub"]: + description: test-pubsub (Terraform-managed). + destination: pubsub.googleapis.com/projects/test-prod-audit-logs-0/topics/audit-logs + disabled: false + exclusions: [] + filter: log_id('cloudaudit.googleapis.com/activity') + include_children: true + intercept_children: false + name: test-pubsub google_privileged_access_manager_entitlement.default["net-admins"]: additional_notification_targets: [] approval_workflow: @@ -114,16 +136,26 @@ values: - not_mandatory: [] unstructured: - {} + timeouts: null + google_pubsub_topic_iam_member.pubsub-sinks-binding["test-pubsub"]: + condition: [] + project: test-prod-audit-logs-0 + role: roles/pubsub.publisher + topic: audit-logs google_tags_tag_binding.binding["foo"]: tag_value: tagValues/1234567890 + timeouts: null counts: + google_cloud_asset_folder_feed: 1 google_essential_contacts_contact: 1 google_folder: 1 google_folder_iam_audit_config: 1 google_folder_iam_binding: 7 google_folder_iam_member: 1 + google_logging_folder_sink: 1 google_privileged_access_manager_entitlement: 1 + google_pubsub_topic_iam_member: 1 google_tags_tag_binding: 1 modules: 0 - resources: 13 + resources: 16 diff --git a/tests/modules/folder/examples/feeds.yaml b/tests/modules/folder/examples/feeds.yaml new file mode 100644 index 000000000..5f5558152 --- /dev/null +++ b/tests/modules/folder/examples/feeds.yaml @@ -0,0 +1,48 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.folder.google_cloud_asset_folder_feed.default["compute-instances"]: + asset_names: null + asset_types: + - compute.googleapis.com/Instance + billing_project: project-id + condition: [] + content_type: RESOURCE + feed_id: compute-instances + feed_output_config: + - pubsub_destination: + - topic: projects/project-id/topics/folder-asset-feed + timeouts: null + module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: folder-asset-feed + project: project-id + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloud_asset_folder_feed: 1 + google_pubsub_topic: 1 + modules: 2 + resources: 3 diff --git a/tests/modules/folder/examples/logging.yaml b/tests/modules/folder/examples/logging.yaml index fa783fbbb..acb508e69 100644 --- a/tests/modules/folder/examples/logging.yaml +++ b/tests/modules/folder/examples/logging.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,33 +29,51 @@ values: default_table_expiration_ms: null delete_contents_on_destroy: false description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + external_catalog_dataset_options: [] + external_dataset_reference: [] friendly_name: null labels: null location: EU max_time_travel_hours: '168' project: project-id + resource_tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null module.destination-project.data.google_logging_project_settings.logging_sa[0]: project: test-dest-prj module.destination-project.google_project.project[0]: auto_create_network: false billing_account: 123456-123456-123456 - deletion_policy: 'DELETE' + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' folder_id: '1122334455' labels: null name: test-dest-prj org_id: null project_id: test-dest-prj + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null module.destination-project.google_project_service.project_services["logging.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false project: test-dest-prj service: logging.googleapis.com + timeouts: null module.folder-sink.google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: condition: [] role: roles/bigquery.dataEditor module.folder-sink.google_folder.folder[0]: + deletion_protection: false display_name: Folder name parent: folders/1122334455 + tags: null + timeouts: null module.folder-sink.google_logging_folder_exclusion.logging-exclusion["no-gce-instances"]: description: no-gce-instances (Terraform-managed). disabled: null @@ -68,6 +86,7 @@ values: exclusions: [] filter: severity=ALERT include_children: true + intercept_children: false name: alert module.folder-sink.google_logging_folder_sink.sink["debug"]: description: debug (Terraform-managed). @@ -79,6 +98,7 @@ values: name: no-compute filter: severity=DEBUG include_children: true + intercept_children: false name: debug module.folder-sink.google_logging_folder_sink.sink["info"]: bigquery_options: @@ -88,6 +108,7 @@ values: exclusions: [] filter: severity=INFO include_children: true + intercept_children: false name: info module.folder-sink.google_logging_folder_sink.sink["notice"]: description: notice (Terraform-managed). @@ -96,6 +117,7 @@ values: exclusions: [] filter: severity=NOTICE include_children: true + intercept_children: false name: notice module.folder-sink.google_logging_folder_sink.sink["warnings"]: description: warnings (Terraform-managed). @@ -104,6 +126,7 @@ values: exclusions: [] filter: severity=WARNING include_children: true + intercept_children: false name: warnings module.folder-sink.google_project_iam_member.bucket-sinks-binding["debug"]: condition: @@ -111,7 +134,7 @@ values: role: roles/logging.bucketWriter module.folder-sink.google_project_iam_member.project-sinks-binding["alert"]: condition: [] - project: test-dest-prj + project: projects/test-dest-prj role: roles/logging.logWriter module.folder-sink.google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: condition: [] @@ -122,14 +145,19 @@ values: bucket: test-gcs_sink condition: [] role: roles/storage.objectCreator + timeouts: null module.gcs.google_storage_bucket.bucket[0]: autoclass: [] cors: [] custom_placement_config: [] default_event_based_hold: null + effective_labels: + goog-terraform-provisioned: 'true' enable_object_retention: null encryption: [] force_destroy: true + hierarchical_namespace: [] + ip_filter: [] labels: null lifecycle_rule: [] location: EU @@ -139,13 +167,25 @@ values: requester_pays: null retention_policy: [] storage_class: STANDARD + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null uniform_bucket_level_access: true module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] kms_key_name: null labels: null message_retention_duration: null + message_transforms: [] name: pubsub_sink project: project-id + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null counts: google_bigquery_dataset: 1 diff --git a/tests/modules/organization/context.tfvars b/tests/modules/organization/context.tfvars index bf12512dc..50af9f7da 100644 --- a/tests/modules/organization/context.tfvars +++ b/tests/modules/organization/context.tfvars @@ -41,6 +41,16 @@ context = { "test/one" = "tagValues/1234567890" } } +asset_feeds = { + test = { + billing_project = "test-project" + feed_output_config = { + pubsub_destination = { + topic = "$pubsub_topics:test" + } + } + } +} contacts = { "$email_addresses:default" = ["ALL"] } diff --git a/tests/modules/organization/context.yaml b/tests/modules/organization/context.yaml index d3790d6b0..e86e7eeaa 100644 --- a/tests/modules/organization/context.yaml +++ b/tests/modules/organization/context.yaml @@ -18,15 +18,29 @@ values: dataset_id: logs project: test-prod-audit-logs-0 role: roles/bigquery.dataEditor + google_cloud_asset_organization_feed.default["test"]: + asset_names: null + asset_types: null + billing_project: test-project + condition: [] + content_type: null + feed_id: test + feed_output_config: + - pubsub_destination: + - topic: projects/test-prod-audit-logs-0/topics/audit-logs + org_id: organizations/1234567890 + timeouts: null google_essential_contacts_contact.contact["$email_addresses:default"]: email: foo@example.com language_tag: en notification_category_subscriptions: - ALL parent: organizations/1234567890 + timeouts: null google_logging_organization_settings.default[0]: organization: '1234567890' storage_location: europe-west8 + timeouts: null google_logging_organization_sink.sink["test-bq"]: bigquery_options: - use_partitioned_tables: false @@ -182,6 +196,7 @@ values: - not_mandatory: [] unstructured: - {} + timeouts: null google_project_iam_member.bucket-sinks-binding["test-logging"]: condition: - expression: resource.name.endsWith('projects/test-prod-audit-logs-0/locations/europe-west8/buckets/audit-logs') @@ -201,9 +216,11 @@ values: bucket: test-prod-logs-audit-0 condition: [] role: roles/storage.objectCreator + timeouts: null google_tags_tag_binding.binding["foo"]: parent: //cloudresourcemanager.googleapis.com/organizations/1234567890 tag_value: tagValues/1234567890 + timeouts: null google_tags_tag_key_iam_binding.bindings["test:tag_user"]: condition: [] members: @@ -241,6 +258,7 @@ values: counts: google_bigquery_dataset_iam_member: 1 + google_cloud_asset_organization_feed: 1 google_essential_contacts_contact: 1 google_logging_organization_settings: 1 google_logging_organization_sink: 5 @@ -257,4 +275,4 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 29 + resources: 30 diff --git a/tests/modules/organization/examples/feeds.yaml b/tests/modules/organization/examples/feeds.yaml new file mode 100644 index 000000000..fc6138d5a --- /dev/null +++ b/tests/modules/organization/examples/feeds.yaml @@ -0,0 +1,50 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_cloud_asset_organization_feed.default["security-monitoring"]: + asset_names: null + asset_types: null + billing_project: project-id + condition: [] + content_type: IAM_POLICY + feed_id: security-monitoring + feed_output_config: + - pubsub_destination: + - topic: projects/project-id/topics/org-asset-feed + org_id: organizations/1122334455 + timeouts: null + module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: org-asset-feed + project: project-id + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloud_asset_organization_feed: 1 + google_pubsub_topic: 1 + modules: 2 + resources: 2 + +outputs: {} diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars index d42243970..476f74134 100644 --- a/tests/modules/project/context.tfvars +++ b/tests/modules/project/context.tfvars @@ -34,6 +34,19 @@ context = { vpc_sc_perimeters = { default = "accessPolicies/888933661165/servicePerimeters/default" } + pubsub_topics = { + test = "projects/test-prod-audit-logs-0/topics/audit-logs" + } +} +asset_feeds = { + test = { + billing_project = "test-project" + feed_output_config = { + pubsub_destination = { + topic = "$pubsub_topics:test" + } + } + } } contacts = { "$email_addresses:default" = ["ALL"] @@ -82,6 +95,13 @@ logging_data_access = { DATA_READ = {} } } +logging_sinks = { + test-pubsub = { + destination = "$pubsub_topics:test" + filter = "log_id('cloudaudit.googleapis.com/activity')" + type = "pubsub" + } +} pam_entitlements = { net-admins = { max_request_duration = "3600s" diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml index 242b4758e..dd888c331 100644 --- a/tests/modules/project/context.yaml +++ b/tests/modules/project/context.yaml @@ -15,20 +15,45 @@ values: google_access_context_manager_service_perimeter_resource.default["$vpc_sc_perimeters:default"]: perimeter_name: accessPolicies/888933661165/servicePerimeters/default + timeouts: null + google_cloud_asset_project_feed.default["test"]: + asset_names: null + asset_types: null + billing_project: test-project + condition: [] + content_type: null + feed_id: test + feed_output_config: + - pubsub_destination: + - topic: projects/test-prod-audit-logs-0/topics/audit-logs + project: my-project + timeouts: null google_compute_shared_vpc_service_project.shared_vpc_service[0]: deletion_policy: null host_project: test-vpc-host service_project: my-project + timeouts: null google_essential_contacts_contact.contact["$email_addresses:default"]: email: foo@example.com language_tag: en notification_category_subscriptions: - ALL parent: projects/my-project + timeouts: null google_kms_crypto_key_iam_member.service_agent_cmek["key-0.compute-system"]: condition: [] crypto_key_id: projects/kms-central-prj/locations/europe-west1/keyRings/my-keyring/cryptoKeys/ew1-compute role: roles/cloudkms.cryptoKeyEncrypterDecrypter + google_logging_project_sink.sink["test-pubsub"]: + custom_writer_identity: null + description: test-pubsub (Terraform-managed). + destination: pubsub.googleapis.com/projects/test-prod-audit-logs-0/topics/audit-logs + disabled: false + exclusions: [] + filter: log_id('cloudaudit.googleapis.com/activity') + name: test-pubsub + project: my-project + unique_writer_identity: true google_privileged_access_manager_entitlement.default["net-admins"]: additional_notification_targets: [] approval_workflow: @@ -62,6 +87,7 @@ values: - not_mandatory: [] unstructured: - {} + timeouts: null google_project.project[0]: auto_create_network: false billing_account: null @@ -76,6 +102,7 @@ values: tags: null terraform_labels: goog-terraform-provisioned: 'true' + timeouts: null google_project_iam_audit_config.default["allServices"]: audit_log_config: - exempted_members: @@ -176,8 +203,15 @@ values: disable_on_destroy: false project: my-project service: compute.googleapis.com + timeouts: null + google_pubsub_topic_iam_member.pubsub-sinks-binding["test-pubsub"]: + condition: [] + project: test-prod-audit-logs-0 + role: roles/pubsub.publisher + topic: audit-logs google_tags_tag_binding.binding["foo"]: tag_value: tagValues/1234567890 + timeouts: null google_tags_tag_key_iam_binding.bindings["test:tag_user"]: condition: [] members: @@ -215,19 +249,22 @@ values: counts: google_access_context_manager_service_perimeter_resource: 1 + google_cloud_asset_project_feed: 1 google_compute_shared_vpc_service_project: 1 google_essential_contacts_contact: 1 google_kms_crypto_key_iam_member: 1 + google_logging_project_sink: 1 google_privileged_access_manager_entitlement: 1 google_project: 1 google_project_iam_audit_config: 1 google_project_iam_binding: 7 google_project_iam_member: 7 google_project_service: 1 + google_pubsub_topic_iam_member: 1 google_tags_tag_binding: 1 google_tags_tag_key_iam_binding: 2 google_tags_tag_key_iam_member: 1 google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 29 + resources: 32 diff --git a/tests/modules/project/examples/feeds.yaml b/tests/modules/project/examples/feeds.yaml new file mode 100644 index 000000000..17f27706f --- /dev/null +++ b/tests/modules/project/examples/feeds.yaml @@ -0,0 +1,49 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_cloud_asset_project_feed.default["compute-instances"]: + asset_names: null + asset_types: + - compute.googleapis.com/Instance + billing_project: test-project + condition: [] + content_type: RESOURCE + feed_id: compute-instances + feed_output_config: + - pubsub_destination: + - topic: projects/project-id/topics/asset-feed + project: test-project + timeouts: null + module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: asset-feed + project: project-id + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloud_asset_project_feed: 1 + google_pubsub_topic: 1 + modules: 2 + resources: 6 diff --git a/tests/modules/project/examples/logging.yaml b/tests/modules/project/examples/logging.yaml index 1e7aacf35..d0f87d281 100644 --- a/tests/modules/project/examples/logging.yaml +++ b/tests/modules/project/examples/logging.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,35 +29,54 @@ values: default_table_expiration_ms: null delete_contents_on_destroy: true description: Terraform managed. + effective_labels: + goog-terraform-provisioned: 'true' + external_catalog_dataset_options: [] + external_dataset_reference: [] friendly_name: null labels: null location: EU max_time_travel_hours: '168' project: project-id + resource_tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null module.destination-project.data.google_logging_project_settings.logging_sa[0]: project: test-dest-prj module.destination-project.google_project.project[0]: auto_create_network: false billing_account: 123456-123456-123456 - deletion_policy: 'DELETE' + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' folder_id: '1122334455' labels: null name: test-dest-prj org_id: null project_id: test-dest-prj + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null module.destination-project.google_project_service.project_services["logging.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false project: test-dest-prj service: logging.googleapis.com + timeouts: null module.gcs.google_storage_bucket.bucket[0]: autoclass: [] cors: [] custom_placement_config: [] default_event_based_hold: null + effective_labels: + goog-terraform-provisioned: 'true' enable_object_retention: null encryption: [] force_destroy: true + hierarchical_namespace: [] + ip_filter: [] labels: null lifecycle_rule: [] location: EU @@ -67,6 +86,9 @@ values: requester_pays: null retention_policy: [] storage_class: STANDARD + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null uniform_bucket_level_access: true module.project-host.data.google_logging_project_settings.logging_sa[0]: project: test-project @@ -136,25 +158,32 @@ values: module.project-host.google_project.project[0]: auto_create_network: false billing_account: 123456-123456-123456 - deletion_policy: 'DELETE' + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' folder_id: '1122334455' labels: null name: test-project org_id: null project_id: test-project + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null module.project-host.google_project_iam_member.bucket-sinks-binding["debug"]: condition: - title: debug bucket writer role: roles/logging.bucketWriter module.project-host.google_project_iam_member.project-sinks-binding["alert"]: condition: [] - project: test-dest-prj + project: projects/test-dest-prj role: roles/logging.logWriter module.project-host.google_project_service.project_services["logging.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false project: test-project service: logging.googleapis.com + timeouts: null module.project-host.google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: condition: [] project: project-id @@ -164,12 +193,22 @@ values: bucket: test-gcs_sink condition: [] role: roles/storage.objectCreator + timeouts: null module.pubsub.google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] kms_key_name: null labels: null message_retention_duration: null + message_transforms: [] name: pubsub_sink project: project-id + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null counts: google_bigquery_dataset: 1 diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml index 8243efa4d..5cedb1415 100644 --- a/tests/modules/project_factory/examples/example.yaml +++ b/tests/modules/project_factory/examples/example.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -213,6 +213,18 @@ values: display_name: Test tags: null timeouts: null + ? module.project-factory.module.folder-4["team-c/apps/test/app-x"].google_cloud_asset_folder_feed.default["compute-instances"] + : asset_names: null + asset_types: + - compute.googleapis.com/Instance + billing_project: $project_ids:feeds-project + condition: [] + content_type: RESOURCE + feed_id: compute-instances + feed_output_config: + - pubsub_destination: + - topic: projects/my-cai-feeds-project/topics/feed + timeouts: null module.project-factory.module.folder-4["team-c/apps/test/app-x"].google_folder.folder[0]: deletion_protection: false display_name: App X @@ -366,6 +378,10 @@ values: condition: [] project: test-pf-dev-ta-app0-be role: roles/container.defaultNodeServiceAgent + module.project-factory.module.projects["dev-ta-app0-be"].google_project_iam_member.service_agents["pubsub"]: + condition: [] + project: test-pf-dev-ta-app0-be + role: roles/pubsub.serviceAgent module.project-factory.module.projects["dev-ta-app0-be"].google_project_service.project_services["compute.googleapis.com"]: disable_dependent_services: false disable_on_destroy: false @@ -378,6 +394,12 @@ values: project: test-pf-dev-ta-app0-be service: container.googleapis.com timeouts: null + module.project-factory.module.projects["dev-ta-app0-be"].google_project_service.project_services["pubsub.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: test-pf-dev-ta-app0-be + service: pubsub.googleapis.com + timeouts: null ? module.project-factory.module.projects["dev-ta-app0-be"].google_project_service.project_services["stackdriver.googleapis.com"] : disable_dependent_services: false disable_on_destroy: false @@ -394,6 +416,10 @@ values: : project: test-pf-dev-ta-app0-be service: container.googleapis.com timeouts: null + module.project-factory.module.projects["dev-ta-app0-be"].google_project_service_identity.default["pubsub.googleapis.com"]: + project: test-pf-dev-ta-app0-be + service: pubsub.googleapis.com + timeouts: null module.project-factory.module.projects["dev-ta-app0-be"].google_tags_tag_binding.binding["context"]: tag_value: tagValues/654321 timeouts: null @@ -560,6 +586,7 @@ values: attribute.repository_owner: assertion.repository_owner attribute.sub: assertion.sub google.subject: assertion.sub + aws: [] description: null disabled: false display_name: GitHub test provider. @@ -568,9 +595,11 @@ values: issuer_uri: https://token.actions.githubusercontent.com jwks_json: null project: test-pf-teams-iac-0 + saml: [] timeouts: null workload_identity_pool_id: test-0 workload_identity_pool_provider_id: github-test + x509: [] module.project-factory.module.projects["teams-iac-0"].google_org_policy_policy.default["compute.disableSerialPortAccess"]: dry_run_spec: [] name: projects/test-pf-teams-iac-0/policies/compute.disableSerialPortAccess @@ -586,6 +615,22 @@ values: parameters: null values: [] timeouts: null + ? module.project-factory.module.projects["teams-iac-0"].google_org_policy_policy.default["gcp.restrictCmekCryptoKeyProjects"] + : dry_run_spec: [] + name: projects/test-pf-teams-iac-0/policies/gcp.restrictCmekCryptoKeyProjects + parent: projects/test-pf-teams-iac-0 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + parameters: null + values: + - denied_values: null + timeouts: null module.project-factory.module.projects["teams-iac-0"].google_project.project[0]: auto_create_network: false billing_account: 012345-67890A-BCDEF0 @@ -635,11 +680,19 @@ values: service: container.googleapis.com timeouts: null module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-a"].google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] kms_key_name: null + labels: null message_retention_duration: null + message_transforms: [] name: app-0-topic-a project: test-pf-dev-ta-app0-be schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' timeouts: null ? module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-a"].google_pubsub_topic_iam_binding.authoritative["roles/pubsub.subscriber"] : condition: [] @@ -648,28 +701,43 @@ values: project: test-pf-dev-ta-app0-be role: roles/pubsub.subscriber topic: app-0-topic-a - module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-b"].google_pubsub_topic.default: - kms_key_name: null - message_retention_duration: null - name: app-0-topic-b - project: test-pf-dev-ta-app0-be - schema_settings: [] - timeouts: null - module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-b"].google_pubsub_subscription.default["app-0-topic-b-sub"]: - bigquery_config: [] + ? module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-b"].google_pubsub_subscription.default["app-0-topic-b-sub"] + : bigquery_config: [] cloud_storage_config: [] dead_letter_policy: [] + effective_labels: + goog-terraform-provisioned: 'true' enable_exactly_once_delivery: false enable_message_ordering: false filter: null + labels: null message_retention_duration: 604800s + message_transforms: [] name: app-0-topic-b-sub project: test-pf-dev-ta-app0-be push_config: [] retain_acked_messages: false retry_policy: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' timeouts: null topic: app-0-topic-b + module.project-factory.module.pubsub["dev-ta-app0-be/app-0-topic-b"].google_pubsub_topic.default: + effective_labels: + goog-terraform-provisioned: 'true' + ingestion_data_source_settings: [] + kms_key_name: null + labels: null + message_retention_duration: null + message_transforms: [] + name: app-0-topic-b + project: test-pf-dev-ta-app0-be + schema_settings: [] + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null ? module.project-factory.module.service-accounts["dev-ta-app0-be/app-0-be"].google_project_iam_member.project-roles["$project_ids:dev-spoke-0-roles/compute.networkUser"] : condition: [] project: $project_ids:dev-spoke-0 @@ -776,6 +844,7 @@ values: counts: google_billing_budget: 1 + google_cloud_asset_folder_feed: 1 google_compute_shared_vpc_host_project: 1 google_compute_shared_vpc_service_project: 1 google_essential_contacts_contact: 4 @@ -808,5 +877,5 @@ counts: google_tags_tag_value: 2 google_tags_tag_value_iam_binding: 1 modules: 29 - resources: 107 + resources: 108 terraform_data: 2 diff --git a/tools/tfdoc.py b/tools/tfdoc.py index fb764195c..5fc60f252 100755 --- a/tools/tfdoc.py +++ b/tools/tfdoc.py @@ -517,34 +517,37 @@ def render_toc(readme, toc): @click.command() -@click.argument('module_path', type=click.Path(exists=True)) +@click.argument('module_paths', type=click.Path(exists=True), nargs=-1) @click.option('--exclude-file', '-x', multiple=True) @click.option('--files/--no-files', default=False) @click.option('--replace/--no-replace', default=True) @click.option('--show-extra/--no-show-extra', default=False) @click.option('--toc-only', is_flag=True, default=False) @click.option('--toc-skip', multiple=True, default=['contents']) -def main(module_path=None, exclude_file=None, files=False, replace=True, +def main(module_paths=None, exclude_file=None, files=False, replace=True, show_extra=True, toc_only=False, toc_skip=['contents']): 'Program entry point.' - if toc_only and module_path.endswith('.md'): - readme_path = module_path - else: - readme_path = os.path.join(module_path, 'README.md') - readme = get_readme(readme_path) - if not toc_only: - doc = create_tfref(module_path, files, show_extra, exclude_file, readme) - readme = render_tfref(readme, doc.content) - toc = create_toc(readme, toc_skip) - readme = render_toc(readme, toc) - if replace: - try: - with open(readme_path, 'w', encoding='utf-8') as f: - f.write(readme) - except (IOError, OSError) as e: - raise SystemExit(f'Error replacing README {readme_path}: {e}') - else: - print(readme) + if not module_paths: + return + for module_path in module_paths: + if toc_only and module_path.endswith('.md'): + readme_path = module_path + else: + readme_path = os.path.join(module_path, 'README.md') + readme = get_readme(readme_path) + if not toc_only: + doc = create_tfref(module_path, files, show_extra, exclude_file, readme) + readme = render_tfref(readme, doc.content) + toc = create_toc(readme, toc_skip) + readme = render_toc(readme, toc) + if replace: + try: + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(readme) + except (IOError, OSError) as e: + raise SystemExit(f'Error replacing README {readme_path}: {e}') + else: + print(readme) if __name__ == '__main__':