From abb2b209eae4275a687da425bafa2b85e0af2628 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 10 Feb 2026 10:23:47 +0100 Subject: [PATCH 1/6] Update billing-0.yaml in gcd dataset (#3719) --- .../datasets/classic-gcd/projects/core/billing-0.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml index 29070bbc2..0d877293a 100644 --- a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml +++ b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml @@ -22,7 +22,7 @@ iam_by_principals: - roles/owner services: - bigquery.googleapis.com - - bigquerydatatransfer.googleapis.com + # - bigquerydatatransfer.googleapis.com - storage.googleapis.com datasets: billing_export: From fd0badd870c7fd7396795ff4c8b3194590058180 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 10 Feb 2026 10:32:12 +0100 Subject: [PATCH 2/6] Revert "Update billing-0.yaml in gcd dataset (#3719)" (#3721) This reverts commit abb2b209eae4275a687da425bafa2b85e0af2628. --- .../datasets/classic-gcd/projects/core/billing-0.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml index 0d877293a..29070bbc2 100644 --- a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml +++ b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml @@ -22,7 +22,7 @@ iam_by_principals: - roles/owner services: - bigquery.googleapis.com - # - bigquerydatatransfer.googleapis.com + - bigquerydatatransfer.googleapis.com - storage.googleapis.com datasets: billing_export: From 496e2791c1be852e76739d4ee1b62b62d75a00cd Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 10 Feb 2026 11:56:42 +0100 Subject: [PATCH 3/6] Update README-GCD.md --- fast/stages/0-org-setup/README-GCD.md | 28 ++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/fast/stages/0-org-setup/README-GCD.md b/fast/stages/0-org-setup/README-GCD.md index e511a2838..b7af7fc11 100644 --- a/fast/stages/0-org-setup/README-GCD.md +++ b/fast/stages/0-org-setup/README-GCD.md @@ -1,6 +1,6 @@ # FAST Installation on Google Cloud Dedicated (GCD) -This document serves as an extension to the main **[FAST Organization Setup README](../README.md)**, detailing the specific configurations and steps required to deploy the Fabric FAST landing zone on **Google Cloud Dedicated (GCD)**. +This document serves as an extension to the main **[FAST Organization Setup README](./README.md)**, detailing the specific configurations and steps required to deploy the Fabric FAST landing zone on **Google Cloud Dedicated (GCD)**. It assumes familiarity with the standard FAST bootstrap flow but highlights the critical divergences required for the Google Cloud Dedicated (GCD) environment. @@ -31,7 +31,7 @@ The core stages are: ## 2. Prerequisites -In addition to the [standard FAST prerequisites](../README.md#prerequisites), ensure the following GCD-specific requirements are met. +In addition to the [standard FAST prerequisites](./README.md#prerequisites), ensure the following GCD-specific requirements are met. ### Identity Provider @@ -84,7 +84,7 @@ gcloud auth application-default login \ ## 3. Bootstrap: Manual Temporary Project -*This step replaces the standard [Default project](../README.md#default-project) creation flow.* +*This step replaces the standard [Default project](./README.md#default-project) creation flow.* GCD requires a manual bootstrap project because organization policy services are not automatically available at the organization root during the initial setup. @@ -113,7 +113,7 @@ GCD requires a manual bootstrap project because organization policy services are ## 4. Terraform Configuration Updates -*This section details specific modifications to the [Configure defaults](../README.md#configure-defaults) step.* +*This section details specific modifications to the [Configure defaults](./README.md#configure-defaults) step.* ### Provider Configuration @@ -132,9 +132,18 @@ provider "google-beta" { ### Defaults Configuration (`defaults.yaml`) -Update your `defaults.yaml` file to include a `universe` block within the `overrides` section. This configures the correct API domains and disables service identities that are not available in GCD. +Update your `defaults.yaml` file to include a `universe` block within the `overrides` section. This configures the correct API domains and disables service identities that are not available in GCD. + +Additionally, you must provide valid values for the following fields in the context section: +* `context.email_addresses.gcp-organization-admins`: used to set the [essential contact]([url](https://docs.cloud.google.com/resource-manager/docs/manage-essential-contacts)) for the core projects +* `context.iam_principals.gcp-organization-admins`: Used to grant administrative permissions to the administrators. + + **Note on Principals:** If you use a group for the admin principal, ensure your user identity is a member of that group. Otherwise, set this field to your own user identity (e.g., `principal://iam.googleapis.com/locations/global/workforcePools/...`) instead of a group. For further details, refer to the [Configure defaults](./README.md#configure-defaults) section in the standard README. + +Your `defaults.yaml should` contain sections that look like this: ```yaml +# ... existing configuration ... projects: defaults: # customize prefix as per usual FAST instructions @@ -154,6 +163,15 @@ projects: - dns.googleapis.com - monitoring.googleapis.com - networksecurity.googleapis.com +context: + email_addresses: + gcp-organization-admins: gcp-organization-admins@example.com + iam_principals: + gcp-organization-admins: group:gcp-organization-admins@example.com + locations: + # Replace with values from the Configuration Reference table + primary: +# ... existing configuration ... ``` ### Switch to GCD Dataset From 4e7b44e184c8c1134cd50286f6a554144873cf07 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 10 Feb 2026 14:42:21 +0100 Subject: [PATCH 4/6] Retry #3719 and fix broken link (#3723) * Fix url and remove unavailable service * Add inline depedencies to `tools/check_links.py` --- fast/stages/0-org-setup/README-GCD.md | 7 +++---- .../datasets/classic-gcd/projects/core/billing-0.yaml | 4 ++-- tools/check_links.py | 11 ++++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/fast/stages/0-org-setup/README-GCD.md b/fast/stages/0-org-setup/README-GCD.md index b7af7fc11..0dfbc3e5e 100644 --- a/fast/stages/0-org-setup/README-GCD.md +++ b/fast/stages/0-org-setup/README-GCD.md @@ -132,12 +132,12 @@ provider "google-beta" { ### Defaults Configuration (`defaults.yaml`) -Update your `defaults.yaml` file to include a `universe` block within the `overrides` section. This configures the correct API domains and disables service identities that are not available in GCD. +Update your `defaults.yaml` file to include a `universe` block within the `overrides` section. This configures the correct API domains and disables service identities that are not available in GCD. Additionally, you must provide valid values for the following fields in the context section: -* `context.email_addresses.gcp-organization-admins`: used to set the [essential contact]([url](https://docs.cloud.google.com/resource-manager/docs/manage-essential-contacts)) for the core projects +* `context.email_addresses.gcp-organization-admins`: used to set the [essential contact](https://docs.cloud.google.com/resource-manager/docs/manage-essential-contacts) for the core projects * `context.iam_principals.gcp-organization-admins`: Used to grant administrative permissions to the administrators. - + **Note on Principals:** If you use a group for the admin principal, ensure your user identity is a member of that group. Otherwise, set this field to your own user identity (e.g., `principal://iam.googleapis.com/locations/global/workforcePools/...`) instead of a group. For further details, refer to the [Configure defaults](./README.md#configure-defaults) section in the standard README. Your `defaults.yaml should` contain sections that look like this: @@ -277,4 +277,3 @@ Once the **Organization Setup** stage is fully deployed: ``` 2. **Proceed to Next Stages:** Continue with the subsequent FAST stages (VPC-SC, Security, Networking, Project Factory). The universe configuration established here is automatically propagated to these stages via the FAST cross-stage output mechanism. - diff --git a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml index 29070bbc2..65db76843 100644 --- a/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.yaml +++ b/fast/stages/0-org-setup/datasets/classic-gcd/projects/core/billing-0.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. @@ -22,7 +22,7 @@ iam_by_principals: - roles/owner services: - bigquery.googleapis.com - - bigquerydatatransfer.googleapis.com + # - bigquerydatatransfer.googleapis.com - storage.googleapis.com datasets: billing_export: diff --git a/tools/check_links.py b/tools/check_links.py index a6ce48a30..d9624a5b3 100755 --- a/tools/check_links.py +++ b/tools/check_links.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023 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. @@ -13,6 +13,15 @@ # 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. +# +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "click", +# "marko", +# "requests", +# ] +# /// '''Recursively check link destination validity in Markdown files. This tool recursively checks that local links in Markdown files point to valid From ddab73d03fcbbf11263c67011661a92de1feb774 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Tue, 10 Feb 2026 15:35:13 +0100 Subject: [PATCH 5/6] Add basic observability example to classic dataset (#3697) * Add basic observability example to classic dataset * Add boilerplate and observability schema * Lint fix * Add observability schema to project-factory module * Extend duplicate-diff * Fix tests * Remove unused schemas --- .../observability/iac-0/impersonation.yaml | 63 +++ .../datasets/classic/projects/core/iac-0.yaml | 8 +- .../schemas/observability.schema.json | 514 ++++++++++++++++++ .../schemas/observability.schema.md | 166 ++++++ modules/project/alerts.tf | 2 +- modules/project/logging-metrics.tf | 4 +- modules/project/notification-channels.tf | 14 +- tests/fast/stages/s0_org_setup/simple.yaml | 107 +++- tests/modules/project/context.tfvars | 41 ++ tests/modules/project/context.yaml | 70 ++- tools/duplicate-diff.py | 4 + 11 files changed, 981 insertions(+), 12 deletions(-) create mode 100644 fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml create mode 100644 fast/stages/0-org-setup/schemas/observability.schema.json create mode 100644 fast/stages/0-org-setup/schemas/observability.schema.md diff --git a/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml b/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml new file mode 100644 index 000000000..632be9ef2 --- /dev/null +++ b/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml @@ -0,0 +1,63 @@ +# 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. + +# yaml-language-server: $schema=../../../../schemas/observability.schema.json + +notification_channels: + email-security: + type: email + display_name: Security Team Email + labels: + email_address: $email_addresses:gcp-organization-admins + enabled: true + +logging_metrics: + sa-impersonation: + filter: | + protoPayload.serviceName="iamcredentials.googleapis.com" + (protoPayload.methodName="GenerateAccessToken" OR protoPayload.methodName="GenerateIdToken") + label_extractors: + email_id: EXTRACT(resource.labels.email_id) + metric_descriptor: + metric_kind: DELTA + value_type: INT64 + unit: "1" + display_name: Service Account Impersonation + labels: + - key: email_id + value_type: STRING + +alerts: + sa-impersonation-alert: + display_name: Service Account Impersonation Alert + combiner: OR + conditions: + - display_name: Impersonation Detected + condition_threshold: + filter: | + metric.type="logging.googleapis.com/user/sa-impersonation" AND + resource.type="global" + comparison: COMPARISON_GT + threshold_value: 0 + duration: 60s + trigger: + count: 1 + aggregations: + alignment_period: 60s + per_series_aligner: ALIGN_COUNT + cross_series_reducer: REDUCE_SUM + group_by_fields: ["metric.label.email_id"] + notification_channels: + - email-security + enabled: true diff --git a/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml b/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml index 0b234f28f..cb1ba257e 100644 --- a/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml +++ b/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.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. @@ -83,6 +83,8 @@ org_policies: - https://token.actions.githubusercontent.com - https://gitlab.com - https://app.terraform.io +factories_config: + observability: datasets/classic/observability/iac-0 data_access_logs: storage.googleapis.com: DATA_READ: {} @@ -90,6 +92,10 @@ data_access_logs: sts.googleapis.com: DATA_READ: {} DATA_WRITE: {} + # required to capture service account impersonation events + iam.googleapis.com: + DATA_READ: {} + DATA_WRITE: {} buckets: # Terraform state bucket for this stage iac-org-state: diff --git a/fast/stages/0-org-setup/schemas/observability.schema.json b/fast/stages/0-org-setup/schemas/observability.schema.json new file mode 100644 index 000000000..cf3eb2f0a --- /dev/null +++ b/fast/stages/0-org-setup/schemas/observability.schema.json @@ -0,0 +1,514 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Observability Schema", + "type": "object", + "additionalProperties": false, + "properties": { + "alerts": { + "$ref": "#/$defs/alerts" + }, + "logging_metrics": { + "$ref": "#/$defs/logging_metrics" + }, + "notification_channels": { + "$ref": "#/$defs/notification_channels" + } + }, + "$defs": { + "alerts": { + "title": "Alerts", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "combiner": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "notification_channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "severity": { + "type": "string" + }, + "user_labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "alert_strategy": { + "type": "object", + "additionalProperties": false, + "properties": { + "auto_close": { + "type": "string" + }, + "notification_prompts": { + "type": "string" + }, + "notification_rate_limit": { + "type": "object", + "additionalProperties": false, + "properties": { + "period": { + "type": "string" + } + } + }, + "notification_channel_strategy": { + "type": "object", + "additionalProperties": false, + "properties": { + "notification_channel_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "renotify_interval": { + "type": "string" + } + } + } + } + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/$defs/condition" + } + }, + "documentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "content": { + "type": "string" + }, + "mime_type": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } + } + } + }, + "required": [ + "combiner" + ] + } + } + }, + "logging_metrics": { + "title": "Logging Metrics", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "label_extractors": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "value_extractor": { + "type": "string" + }, + "bucket_options": { + "type": "object", + "additionalProperties": false, + "properties": { + "explicit_buckets": { + "type": "object", + "additionalProperties": false, + "properties": { + "bounds": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "exponential_buckets": { + "type": "object", + "additionalProperties": false, + "properties": { + "num_finite_buckets": { + "type": "number" + }, + "growth_factor": { + "type": "number" + }, + "scale": { + "type": "number" + } + } + }, + "linear_buckets": { + "type": "object", + "additionalProperties": false, + "properties": { + "num_finite_buckets": { + "type": "number" + }, + "width": { + "type": "number" + }, + "offset": { + "type": "number" + } + } + } + } + }, + "metric_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "metric_kind": { + "type": "string" + }, + "value_type": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value_type": { + "type": "string" + } + }, + "required": [ + "key" + ] + } + } + }, + "required": [ + "metric_kind", + "value_type" + ] + } + }, + "required": [ + "filter" + ] + } + } + }, + "notification_channels": { + "title": "Notification Channels", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "user_labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sensitive_labels": { + "type": "object", + "additionalProperties": false, + "properties": { + "auth_token": { + "type": "string" + }, + "password": { + "type": "string" + }, + "service_key": { + "type": "string" + } + } + } + }, + "required": [ + "type" + ] + } + } + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "string" + }, + "condition_absent": { + "$ref": "#/$defs/absent_condition" + }, + "condition_matched_log": { + "$ref": "#/$defs/matched_log_condition" + }, + "condition_monitoring_query_language": { + "$ref": "#/$defs/monitoring_query_condition" + }, + "condition_prometheus_query_language": { + "$ref": "#/$defs/prometheus_query_condition" + }, + "condition_threshold": { + "$ref": "#/$defs/threshold_condition" + } + }, + "required": [ + "display_name" + ] + }, + "absent_condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "duration": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "aggregations": { + "$ref": "#/$defs/aggregations" + }, + "trigger": { + "$ref": "#/$defs/trigger" + } + }, + "required": [ + "duration" + ] + }, + "matched_log_condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string" + }, + "label_extractors": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "filter" + ] + }, + "monitoring_query_condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "duration": { + "type": "string" + }, + "query": { + "type": "string" + }, + "evaluation_missing_data": { + "type": "string" + }, + "trigger": { + "$ref": "#/$defs/trigger" + } + }, + "required": [ + "duration", + "query" + ] + }, + "prometheus_query_condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + }, + "alert_rule": { + "type": "string" + }, + "disable_metric_validation": { + "type": "boolean" + }, + "duration": { + "type": "string" + }, + "evaluation_interval": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "rule_group": { + "type": "string" + } + }, + "required": [ + "query" + ] + }, + "threshold_condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "comparison": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "denominator_filter": { + "type": "string" + }, + "evaluation_missing_data": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "threshold_value": { + "type": "number" + }, + "aggregations": { + "$ref": "#/$defs/aggregations" + }, + "denominator_aggregations": { + "$ref": "#/$defs/aggregations" + }, + "forecast_options": { + "type": "object", + "additionalProperties": false, + "properties": { + "forecast_horizon": { + "type": "string" + } + } + }, + "trigger": { + "$ref": "#/$defs/trigger" + } + }, + "required": [ + "comparison", + "duration" + ] + }, + "aggregations": { + "type": "object", + "additionalProperties": false, + "properties": { + "per_series_aligner": { + "type": "string" + }, + "group_by_fields": { + "type": "array", + "items": { + "type": "string" + } + }, + "cross_series_reducer": { + "type": "string" + }, + "alignment_period": { + "type": "string" + } + } + }, + "trigger": { + "type": "object", + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "percent": { + "type": "number" + } + } + } + } +} diff --git a/fast/stages/0-org-setup/schemas/observability.schema.md b/fast/stages/0-org-setup/schemas/observability.schema.md new file mode 100644 index 000000000..e3e411782 --- /dev/null +++ b/fast/stages/0-org-setup/schemas/observability.schema.md @@ -0,0 +1,166 @@ +# Observability Schema + + + +## Properties + +*additional properties: false* + +- **alerts**: *reference([alerts](#refs-alerts))* +- **logging_metrics**: *reference([logging_metrics](#refs-logging_metrics))* +- **notification_channels**: *reference([notification_channels](#refs-notification_channels))* + +## Definitions + +- **alerts**: *object* +
*additional properties: false* + - **`^[a-zA-Z0-9-]+$`**: *object* +
*additional properties: false* + - ⁺**combiner**: *string* + - **display_name**: *string* + - **enabled**: *boolean* + - **notification_channels**: *array* + - items: *string* + - **severity**: *string* + - **user_labels**: *object* +
*additional properties: string* + - **alert_strategy**: *object* +
*additional properties: false* + - **auto_close**: *string* + - **notification_prompts**: *string* + - **notification_rate_limit**: *object* +
*additional properties: false* + - **period**: *string* + - **notification_channel_strategy**: *object* +
*additional properties: false* + - **notification_channel_names**: *array* + - items: *string* + - **renotify_interval**: *string* + - **conditions**: *array* + - items: *reference([condition](#refs-condition))* + - **documentation**: *object* +
*additional properties: false* + - **content**: *string* + - **mime_type**: *string* + - **subject**: *string* + - **links**: *array* + - items: *object* +
*additional properties: false* + - **display_name**: *string* + - **url**: *string* +- **logging_metrics**: *object* +
*additional properties: false* + - **`^[a-zA-Z0-9-]+$`**: *object* +
*additional properties: false* + - ⁺**filter**: *string* + - **bucket_name**: *string* + - **description**: *string* + - **disabled**: *boolean* + - **label_extractors**: *object* +
*additional properties: string* + - **value_extractor**: *string* + - **bucket_options**: *object* +
*additional properties: false* + - **explicit_buckets**: *object* +
*additional properties: false* + - **bounds**: *array* + - items: *number* + - **exponential_buckets**: *object* +
*additional properties: false* + - **num_finite_buckets**: *number* + - **growth_factor**: *number* + - **scale**: *number* + - **linear_buckets**: *object* +
*additional properties: false* + - **num_finite_buckets**: *number* + - **width**: *number* + - **offset**: *number* + - **metric_descriptor**: *object* +
*additional properties: false* + - ⁺**metric_kind**: *string* + - ⁺**value_type**: *string* + - **display_name**: *string* + - **unit**: *string* + - **labels**: *array* + - items: *object* +
*additional properties: false* + - ⁺**key**: *string* + - **description**: *string* + - **value_type**: *string* +- **notification_channels**: *object* +
*additional properties: false* + - **`^[a-zA-Z0-9-]+$`**: *object* +
*additional properties: false* + - ⁺**type**: *string* + - **description**: *string* + - **display_name**: *string* + - **enabled**: *boolean* + - **labels**: *object* +
*additional properties: string* + - **user_labels**: *object* +
*additional properties: string* + - **sensitive_labels**: *object* +
*additional properties: false* + - **auth_token**: *string* + - **password**: *string* + - **service_key**: *string* +- **condition**: *object* +
*additional properties: false* + - ⁺**display_name**: *string* + - **condition_absent**: *reference([absent_condition](#refs-absent_condition))* + - **condition_matched_log**: *reference([matched_log_condition](#refs-matched_log_condition))* + - **condition_monitoring_query_language**: *reference([monitoring_query_condition](#refs-monitoring_query_condition))* + - **condition_prometheus_query_language**: *reference([prometheus_query_condition](#refs-prometheus_query_condition))* + - **condition_threshold**: *reference([threshold_condition](#refs-threshold_condition))* +- **absent_condition**: *object* +
*additional properties: false* + - ⁺**duration**: *string* + - **filter**: *string* + - **aggregations**: *reference([aggregations](#refs-aggregations))* + - **trigger**: *reference([trigger](#refs-trigger))* +- **matched_log_condition**: *object* +
*additional properties: false* + - ⁺**filter**: *string* + - **label_extractors**: *object* +
*additional properties: string* +- **monitoring_query_condition**: *object* +
*additional properties: false* + - ⁺**duration**: *string* + - ⁺**query**: *string* + - **evaluation_missing_data**: *string* + - **trigger**: *reference([trigger](#refs-trigger))* +- **prometheus_query_condition**: *object* +
*additional properties: false* + - ⁺**query**: *string* + - **alert_rule**: *string* + - **disable_metric_validation**: *boolean* + - **duration**: *string* + - **evaluation_interval**: *string* + - **labels**: *object* +
*additional properties: string* + - **rule_group**: *string* +- **threshold_condition**: *object* +
*additional properties: false* + - ⁺**comparison**: *string* + - ⁺**duration**: *string* + - **denominator_filter**: *string* + - **evaluation_missing_data**: *string* + - **filter**: *string* + - **threshold_value**: *number* + - **aggregations**: *reference([aggregations](#refs-aggregations))* + - **denominator_aggregations**: *reference([aggregations](#refs-aggregations))* + - **forecast_options**: *object* +
*additional properties: false* + - **forecast_horizon**: *string* + - **trigger**: *reference([trigger](#refs-trigger))* +- **aggregations**: *object* +
*additional properties: false* + - **per_series_aligner**: *string* + - **group_by_fields**: *array* + - items: *string* + - **cross_series_reducer**: *string* + - **alignment_period**: *string* +- **trigger**: *object* +
*additional properties: false* + - **count**: *number* + - **percent**: *number* diff --git a/modules/project/alerts.tf b/modules/project/alerts.tf index ef2139c06..a28a67dc4 100644 --- a/modules/project/alerts.tf +++ b/modules/project/alerts.tf @@ -135,7 +135,7 @@ resource "google_monitoring_alert_policy" "alerts" { # first try to get a channel created by this module google_monitoring_notification_channel.channels[x].name, # otherwise check the context - var.context.notification_channels[x], + local.ctx.notification_channels[x], # if nothing else, use the provided channel as is x ) diff --git a/modules/project/logging-metrics.tf b/modules/project/logging-metrics.tf index 59dc7088e..42ba6f29d 100644 --- a/modules/project/logging-metrics.tf +++ b/modules/project/logging-metrics.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. @@ -71,7 +71,7 @@ resource "google_logging_metric" "metrics" { disabled = each.value.disabled bucket_name = try( # first try to check the context - var.context.log_buckets[each.value.bucket_name], + local.ctx.log_buckets[each.value.bucket_name], # if nothing else, use the provided channel as is each.value.bucket_name ) diff --git a/modules/project/notification-channels.tf b/modules/project/notification-channels.tf index 45d1678c6..1827b07f9 100644 --- a/modules/project/notification-channels.tf +++ b/modules/project/notification-channels.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. @@ -45,8 +45,16 @@ resource "google_monitoring_notification_channel" "channels" { description = each.value.description display_name = each.value.display_name enabled = each.value.enabled - labels = each.value.labels - user_labels = each.value.user_labels + labels = each.value.labels == null ? null : { + for k, v in each.value.labels : + # allow interpolation of email addresses and pubsub topics + k => try( + local.ctx.email_addresses[v], + local.ctx.pubsub_topics[v], + v + ) + } + user_labels = each.value.user_labels dynamic "sensitive_labels" { for_each = each.value.sensitive_labels[*] content { diff --git a/tests/fast/stages/s0_org_setup/simple.yaml b/tests/fast/stages/s0_org_setup/simple.yaml index c9c855a3a..16395beb7 100644 --- a/tests/fast/stages/s0_org_setup/simple.yaml +++ b/tests/fast/stages/s0_org_setup/simple.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. @@ -34,6 +34,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -65,6 +66,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -97,6 +99,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -129,6 +132,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -161,6 +165,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -193,6 +198,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -211,6 +217,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -228,6 +235,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -245,6 +253,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -357,6 +366,7 @@ values: content_disposition: null content_encoding: null content_language: null + contexts: [] customer_encryption: [] deletion_policy: null detect_md5hash: null @@ -1112,6 +1122,14 @@ values: condition: [] project: ft0-prod-billing-exp-0 role: roles/viewer + module.factory.module.projects-iam["iac-0"].google_project_iam_audit_config.default["iam.googleapis.com"]: + audit_log_config: + - exempted_members: [] + log_type: DATA_READ + - exempted_members: [] + log_type: DATA_WRITE + project: ft0-prod-iac-core-0 + service: iam.googleapis.com module.factory.module.projects-iam["iac-0"].google_project_iam_audit_config.default["storage.googleapis.com"]: audit_log_config: - exempted_members: [] @@ -1241,6 +1259,82 @@ values: module.factory.module.projects["iac-0"].data.google_storage_project_service_account.gcs_sa[0]: project: ft0-prod-iac-core-0 user_project: null + module.factory.module.projects["iac-0"].google_logging_metric.metrics["sa-impersonation"]: + bucket_name: null + bucket_options: [] + description: null + disabled: null + filter: 'protoPayload.serviceName="iamcredentials.googleapis.com" + + (protoPayload.methodName="GenerateAccessToken" OR protoPayload.methodName="GenerateIdToken") + + ' + label_extractors: + email_id: EXTRACT(resource.labels.email_id) + metric_descriptor: + - display_name: Service Account Impersonation + labels: + - description: '' + key: email_id + value_type: STRING + metric_kind: DELTA + unit: '1' + value_type: INT64 + name: sa-impersonation + project: ft0-prod-iac-core-0 + timeouts: null + value_extractor: null + module.factory.module.projects["iac-0"].google_monitoring_alert_policy.alerts["sa-impersonation-alert"]: + alert_strategy: [] + combiner: OR + conditions: + - condition_absent: [] + condition_matched_log: [] + condition_monitoring_query_language: [] + condition_prometheus_query_language: [] + condition_sql: [] + condition_threshold: + - aggregations: + - alignment_period: 60s + cross_series_reducer: REDUCE_SUM + group_by_fields: + - metric.label.email_id + per_series_aligner: ALIGN_COUNT + comparison: COMPARISON_GT + denominator_aggregations: [] + denominator_filter: null + duration: 60s + evaluation_missing_data: null + filter: 'metric.type="logging.googleapis.com/user/sa-impersonation" AND + + resource.type="global" + + ' + forecast_options: [] + threshold_value: 0 + trigger: + - count: 1 + percent: null + display_name: Impersonation Detected + display_name: Service Account Impersonation Alert + documentation: [] + enabled: true + project: ft0-prod-iac-core-0 + severity: null + timeouts: null + user_labels: null + module.factory.module.projects["iac-0"].google_monitoring_notification_channel.channels["email-security"]: + description: null + display_name: Security Team Email + enabled: true + force_delete: false + labels: + email_address: $email_addresses:gcp-organization-admins + project: ft0-prod-iac-core-0 + sensitive_labels: [] + timeouts: null + type: email + user_labels: null module.factory.module.projects["iac-0"].google_org_policy_policy.default["iam.workloadIdentityPoolProviders"]: dry_run_spec: [] name: projects/ft0-prod-iac-core-0/policies/iam.workloadIdentityPoolProviders @@ -2862,6 +2956,7 @@ values: stage: GA title: Custom role tagViewer module.organization[0].google_tags_tag_key.default["context"]: + allowed_values_regex: null description: Organization-level contexts. parent: organizations/1234567890 purpose: null @@ -2869,6 +2964,7 @@ values: short_name: context timeouts: null module.organization[0].google_tags_tag_key.default["environment"]: + allowed_values_regex: null description: Organization-level environments. parent: organizations/1234567890 purpose: null @@ -2876,6 +2972,7 @@ values: short_name: environment timeouts: null module.organization[0].google_tags_tag_key.default["org-policies"]: + allowed_values_regex: null description: Organization policy condition tags. parent: organizations/1234567890 purpose: null @@ -2910,7 +3007,6 @@ values: input: null output: null triggers_replace: null - counts: google_bigquery_dataset: 1 google_bigquery_default_service_account: 2 @@ -2918,17 +3014,20 @@ counts: google_essential_contacts_contact: 1 google_folder: 10 google_folder_iam_binding: 44 + google_logging_metric: 1 google_logging_organization_settings: 1 google_logging_organization_sink: 3 google_logging_project_bucket_config: 3 google_logging_project_settings: 2 + google_monitoring_alert_policy: 1 + google_monitoring_notification_channel: 1 google_org_policy_custom_constraint: 1 google_org_policy_policy: 37 google_organization_iam_audit_config: 1 google_organization_iam_binding: 37 google_organization_iam_custom_role: 9 google_project: 3 - google_project_iam_audit_config: 2 + google_project_iam_audit_config: 3 google_project_iam_binding: 17 google_project_iam_member: 15 google_project_service: 33 @@ -2948,5 +3047,5 @@ counts: google_tags_tag_value_iam_binding: 4 local_file: 9 modules: 50 - resources: 324 + resources: 328 terraform_data: 4 diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars index 476f74134..db7277832 100644 --- a/tests/modules/project/context.tfvars +++ b/tests/modules/project/context.tfvars @@ -31,6 +31,12 @@ context = { tag_values = { "test/one" = "tagValues/1234567890" } + log_buckets = { + audit = "logging.googleapis.com/projects/my-project/locations/global/buckets/audit-bucket" + } + notification_channels = { + email = "projects/my-project/notificationChannels/12345" + } vpc_sc_perimeters = { default = "accessPolicies/888933661165/servicePerimeters/default" } @@ -38,6 +44,41 @@ context = { test = "projects/test-prod-audit-logs-0/topics/audit-logs" } } +alerts = { + test-alert = { + combiner = "OR" + display_name = "Test Alert" + conditions = [{ + display_name = "test-condition" + condition_threshold = { + comparison = "COMPARISON_GT" + duration = "60s" + filter = "resource.type=\"gce_instance\" AND metric.type=\"compute.googleapis.com/instance/cpu/utilization\"" + } + }] + notification_channels = ["$notification_channels:email"] + } +} +logging_metrics = { + test-metric = { + filter = "resource.type=\"gce_instance\"" + bucket_name = "$log_buckets:audit" + } +} +notification_channels = { + new-email = { + type = "email" + labels = { + email_address = "$email_addresses:default" + } + } + new-pubsub = { + type = "pubsub" + labels = { + topic = "$pubsub_topics:test" + } + } +} asset_feeds = { test = { billing_project = "test-project" diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml index dd888c331..b45ea1424 100644 --- a/tests/modules/project/context.yaml +++ b/tests/modules/project/context.yaml @@ -44,6 +44,17 @@ values: condition: [] crypto_key_id: projects/kms-central-prj/locations/europe-west1/keyRings/my-keyring/cryptoKeys/ew1-compute role: roles/cloudkms.cryptoKeyEncrypterDecrypter + google_logging_metric.metrics["test-metric"]: + bucket_name: logging.googleapis.com/projects/my-project/locations/global/buckets/audit-bucket + bucket_options: [] + description: null + disabled: null + filter: resource.type="gce_instance" + label_extractors: null + name: test-metric + project: my-project + timeouts: null + value_extractor: null google_logging_project_sink.sink["test-pubsub"]: custom_writer_identity: null description: test-pubsub (Terraform-managed). @@ -54,6 +65,60 @@ values: name: test-pubsub project: my-project unique_writer_identity: true + google_monitoring_alert_policy.alerts["test-alert"]: + alert_strategy: [] + combiner: OR + conditions: + - condition_absent: [] + condition_matched_log: [] + condition_monitoring_query_language: [] + condition_prometheus_query_language: [] + condition_sql: [] + condition_threshold: + - aggregations: [] + comparison: COMPARISON_GT + denominator_aggregations: [] + denominator_filter: null + duration: 60s + evaluation_missing_data: null + filter: resource.type="gce_instance" AND metric.type="compute.googleapis.com/instance/cpu/utilization" + forecast_options: [] + threshold_value: null + trigger: [] + display_name: test-condition + display_name: Test Alert + documentation: [] + enabled: true + notification_channels: + - projects/my-project/notificationChannels/12345 + project: my-project + severity: null + timeouts: null + user_labels: null + google_monitoring_notification_channel.channels["new-email"]: + description: null + display_name: null + enabled: true + force_delete: false + labels: + email_address: foo@example.com + project: my-project + sensitive_labels: [] + timeouts: null + type: email + user_labels: null + google_monitoring_notification_channel.channels["new-pubsub"]: + description: null + display_name: null + enabled: true + force_delete: false + labels: + topic: projects/test-prod-audit-logs-0/topics/audit-logs + project: my-project + sensitive_labels: [] + timeouts: null + type: pubsub + user_labels: null google_privileged_access_manager_entitlement.default["net-admins"]: additional_notification_targets: [] approval_workflow: @@ -253,7 +318,10 @@ counts: google_compute_shared_vpc_service_project: 1 google_essential_contacts_contact: 1 google_kms_crypto_key_iam_member: 1 + google_logging_metric: 1 google_logging_project_sink: 1 + google_monitoring_alert_policy: 1 + google_monitoring_notification_channel: 2 google_privileged_access_manager_entitlement: 1 google_project: 1 google_project_iam_audit_config: 1 @@ -267,4 +335,4 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 32 + resources: 36 diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py index 479b75540..d014d9f21 100755 --- a/tools/duplicate-diff.py +++ b/tools/duplicate-diff.py @@ -60,6 +60,10 @@ duplicates = [ "fast/stages/2-security/schemas/folder.schema.json", "modules/project-factory/schemas/folder.schema.json", ], + [ + "fast/stages/0-org-setup/schemas/observability.schema.json", + "modules/project/schemas/observability.schema.json", + ], [ "fast/stages/1-vpcsc/schemas/ingress-policy.schema.json", "modules/vpc-sc/schemas/ingress-policy.schema.json", From 2a37612cf3ce1245ebca597e0169b0ebd1653b71 Mon Sep 17 00:00:00 2001 From: Antonio Lopez <94461129+ajlopezn@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:22:05 +0100 Subject: [PATCH 6/6] Looker PSC support (#3724) * added PSC support * added PSC support * added PSC support * Update README * Add inventory --------- Co-authored-by: Julio Castillo --- modules/looker-core/README.md | 37 ++++++++++++--- modules/looker-core/main.tf | 8 ++++ modules/looker-core/variables.tf | 13 ++++-- tests/modules/looker_core/examples/psc.yaml | 51 +++++++++++++++++++++ 4 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 tests/modules/looker_core/examples/psc.yaml diff --git a/modules/looker-core/README.md b/modules/looker-core/README.md index 5a3c48ee2..8e959d3a8 100644 --- a/modules/looker-core/README.md +++ b/modules/looker-core/README.md @@ -19,6 +19,7 @@ is no terraform support for these resources. - [Examples](#examples) - [Simple example](#simple-example) - [Looker Core private instance with PSA](#looker-core-private-instance-with-psa) + - [Looker Core with PSC](#looker-core-with-psc) - [Looker Core full example](#looker-core-full-example) - [Variables](#variables) - [Outputs](#outputs) @@ -90,6 +91,29 @@ module "looker" { # tftest modules=3 resources=17 inventory=psa.yaml ``` + +### Looker Core with PSC + +```hcl +module "looker" { + source = "./fabric/modules/looker-core" + project_id = var.project_id + region = var.region + name = "looker-psc" + network_config = { + psc_config = { + allowed_vpcs = ["projects/test-project/global/networks/test"] + } + } + oauth_config = { + client_id = "xxxxxxxxx" + client_secret = "xxxxxxxx" + } + platform_edition = "LOOKER_CORE_ENTERPRISE_ANNUAL" +} +# tftest inventory=psc.yaml +``` + ### Looker Core full example ```hcl @@ -160,23 +184,22 @@ module "looker" { } # tftest modules=4 resources=23 inventory=full.yaml ``` - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [name](variables.tf#L91) | Name of the looker core instance. | string | ✓ | | -| [network_config](variables.tf#L96) | Network configuration for cluster and instance. Only one between psa_config and psc_config can be used. | object({…}) | ✓ | | -| [oauth_config](variables.tf#L114) | Looker Core Oauth config. Either client ID and secret (existing oauth client) or support email (temporary internal oauth client setup) must be specified. | object({…}) | ✓ | | -| [project_id](variables.tf#L147) | The ID of the project where this instances will be created. | string | ✓ | | -| [region](variables.tf#L152) | Region for the Looker core instance. | string | ✓ | | +| [network_config](variables.tf#L96) | Network configuration for cluster and instance. Only one between psa_config, psc_config and public can be used. | object({…}) | ✓ | | +| [oauth_config](variables.tf#L121) | Looker Core Oauth config. Either client ID and secret (existing oauth client) or support email (temporary internal oauth client setup) must be specified. | object({…}) | ✓ | | +| [project_id](variables.tf#L154) | The ID of the project where this instances will be created. | string | ✓ | | +| [region](variables.tf#L159) | Region for the Looker core instance. | string | ✓ | | | [admin_settings](variables.tf#L17) | Looker Core admins settings. | object({…}) | | null | | [custom_domain](variables.tf#L26) | Looker core instance custom domain. | string | | null | | [encryption_config](variables.tf#L32) | Set encryption configuration. KMS name format: 'projects/[PROJECT]/locations/[REGION]/keyRings/[RING]/cryptoKeys/[KEY_NAME]'. | object({…}) | | null | | [maintenance_config](variables.tf#L41) | Set maintenance window configuration and maintenance deny period (up to 90 days). Date format: 'yyyy-mm-dd'. | object({…}) | | {} | -| [platform_edition](variables.tf#L127) | Platform editions for a Looker instance. Each edition maps to a set of instance features, like its size. | string | | "LOOKER_CORE_TRIAL" | -| [prefix](variables.tf#L137) | Optional prefix used to generate instance names. | string | | null | +| [platform_edition](variables.tf#L134) | Platform editions for a Looker instance. Each edition maps to a set of instance features, like its size. | string | | "LOOKER_CORE_TRIAL" | +| [prefix](variables.tf#L144) | Optional prefix used to generate instance names. | string | | null | ## Outputs diff --git a/modules/looker-core/main.tf b/modules/looker-core/main.tf index 8278fec6b..4cf8f338b 100644 --- a/modules/looker-core/main.tf +++ b/modules/looker-core/main.tf @@ -29,6 +29,7 @@ resource "google_looker_instance" "looker" { platform_edition = var.platform_edition private_ip_enabled = try(var.network_config.psa_config.enable_private_ip, null) public_ip_enabled = coalesce(var.network_config.public, false) || try(var.network_config.psa_config.enable_public_ip, false) + psc_enabled = var.network_config.psc_config != null region = var.region reserved_range = try(var.network_config.psa_config.allocated_ip_range, null) @@ -37,6 +38,13 @@ resource "google_looker_instance" "looker" { client_secret = local.oauth_client_secret } + dynamic "psc_config" { + for_each = var.network_config.psc_config != null ? [""] : [] + content { + allowed_vpcs = var.network_config.psc_config.allowed_vpcs + } + } + dynamic "admin_settings" { for_each = var.admin_settings != null ? [""] : [] content { diff --git a/modules/looker-core/variables.tf b/modules/looker-core/variables.tf index bc28c71d1..e604c0e8c 100644 --- a/modules/looker-core/variables.tf +++ b/modules/looker-core/variables.tf @@ -94,7 +94,7 @@ variable "name" { } variable "network_config" { - description = "Network configuration for cluster and instance. Only one between psa_config and psc_config can be used." + description = "Network configuration for cluster and instance. Only one between psa_config, psc_config and public can be used." type = object({ psa_config = optional(object({ network = string @@ -102,12 +102,19 @@ variable "network_config" { enable_public_ip = optional(bool, false) enable_private_ip = optional(bool, true) })) + psc_config = optional(object({ + allowed_vpcs = optional(list(string), []) + })) public = optional(bool, false) }) nullable = false validation { - condition = (coalesce(var.network_config.public, false)) == (var.network_config.psa_config == null) - error_message = "Please specify either psa_config or public to true." + condition = ( + (coalesce(var.network_config.public, false) ? 1 : 0) + + (var.network_config.psa_config != null ? 1 : 0) + + (var.network_config.psc_config != null ? 1 : 0) + ) == 1 + error_message = "Please specify exactly one of psa_config, psc_config or public." } } diff --git a/tests/modules/looker_core/examples/psc.yaml b/tests/modules/looker_core/examples/psc.yaml new file mode 100644 index 000000000..a82e96b59 --- /dev/null +++ b/tests/modules/looker_core/examples/psc.yaml @@ -0,0 +1,51 @@ +# 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.looker.google_looker_instance.looker: + admin_settings: [] + consumer_network: null + controlled_egress_config: [] + controlled_egress_enabled: null + custom_domain: [] + deletion_policy: DEFAULT + deny_maintenance_period: [] + fips_enabled: null + gemini_enabled: null + maintenance_window: [] + name: looker-psc + oauth_config: + - client_id: xxxxxxxxx + client_secret: xxxxxxxx + periodic_export_config: [] + platform_edition: LOOKER_CORE_ENTERPRISE_ANNUAL + private_ip_enabled: false + project: project-id + psc_config: + - allowed_vpcs: + - projects/test-project/global/networks/test + service_attachments: [] + psc_enabled: true + public_ip_enabled: false + region: europe-west8 + reserved_range: null + timeouts: null + user_metadata: [] + +counts: + google_looker_instance: 1 + modules: 1 + resources: 1 + +outputs: {}