diff --git a/README.md b/README.md index 6c2375998..ec08bbbc8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Currently available modules: - **networking** - [DNS](./modules/dns), [DNS Response Policy](./modules/dns-response-policy/), [Cloud Endpoints](./modules/endpoints), [address reservation](./modules/net-address), [NAT](./modules/net-cloudnat), [VLAN Attachment](./modules/net-vlan-attachment/), [External Application LB](./modules/net-lb-app-ext/), [External Passthrough Network LB](./modules/net-lb-ext), [External Regional Application Load Balancer](./modules/net-lb-app-ext-regional/), [Firewall policy](./modules/net-firewall-policy), [Internal Application LB](./modules/net-lb-app-int), [Cross-region Internal Application LB](./modules/net-lb-app-int-cross-region), [Internal Passthrough Network LB](./modules/net-lb-int), [Internal Proxy Network LB](./modules/net-lb-proxy-int), [IPSec over Interconnect](./modules/net-ipsec-over-interconnect), [VPC](./modules/net-vpc), [VPC factory](./modules/net-vpc-factory/README.md), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [VPN static](./modules/net-vpn-static), [Service Directory](./modules/service-directory), [Secure Web Proxy](./modules/net-swp) - **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster-standard), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool), [GCVE private cloud](./modules/gcve-private-cloud) - **data** - [AlloyDB instance](./modules/alloydb), [Analytics Hub](./modules/analytics-hub), [BigQuery dataset](./modules/bigquery-dataset), [Biglake Catalog](./modules/biglake-catalog), [Bigtable instance](./modules/bigtable-instance), [Dataplex](./modules/dataplex), [Dataplex Aspect Types](./modules/dataplex-aspect-types/), [Dataplex DataScan](./modules/dataplex-datascan), [Cloud SQL instance](./modules/cloudsql-instance), [Spanner instance](./modules/spanner-instance), [Firestore](./modules/firestore), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Data Catalog Tag](./modules/data-catalog-tag), [Data Catalog Tag Template](./modules/data-catalog-tag-template), [Datafusion](./modules/datafusion), [Dataproc](./modules/dataproc), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub), [Dataform Repository](./modules/dataform-repository/), [Looker Core](./modules/looker-core) +- **AI** - [Agentspace](./modules/agentspace/README.md) - **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository), [Secure Source Manager instance](./modules/secure-source-manager-instance), [Workstation cluster](./modules/workstation-cluster) - **security** - [Binauthz](./modules/binauthz/), [Certificate Authority Service (CAS)](./modules/certificate-authority-service), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc), [Certificate Manager](./modules/certificate-manager/) - **serverless** - [Cloud Function v1](./modules/cloud-function-v1), [Cloud Function v2](./modules/cloud-function-v2), [Cloud Run](./modules/cloud-run), [Cloud Run v2](./modules/cloud-run-v2) diff --git a/modules/README.md b/modules/README.md index 6851500f0..48105f494 100644 --- a/modules/README.md +++ b/modules/README.md @@ -101,6 +101,10 @@ These modules are used in the examples included in this repository. If you are u - [Pub/Sub](./pubsub) - [Spanner instance](./spanner-instance) +## AI + +- [Agentspace](./agentspace/README.md) + ## Development - [API Gateway](./api-gateway) diff --git a/modules/agentspace/README.md b/modules/agentspace/README.md new file mode 100644 index 000000000..033517b6d --- /dev/null +++ b/modules/agentspace/README.md @@ -0,0 +1,253 @@ +# Agentspace + +This module handles the creation of Agentspace data sources, engines and related configurations. + + +* [Agentspace module](#agentspace) + * [APIs](#apis) + * [Quota Project](#quota-project) + * [Examples](#examples) + * [Chat Engine](#chat-engine) + * [Search Engine](#search-engine) + * [Deploy your service into a region](#deploy-your-service-into-a-region) + * [Reference Existing Data Sources](#reference-existing-data-sources) + * [Using multiple data stores](#using-multiple-data-stores) + * [Set data store schemas](#set-data-store-schemas) + * [Back data stores with websites data](#back-data-stores-with-websites-data) + * [Variables](#variables) + * [Outputs](#outputs) + + +## APIs + +This module uses the API `discoveryengine.googleapis.com` + +## Quota Project + +To run this module you'll need to set a quota project. + +```shell +export GOOGLE_BILLING_PROJECT=your-project-id +export USER_PROJECT_OVERRIDE=true +``` + +## Examples + +### Chat Engine + +This is a minimal example to create a Chat Engine agent. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + my-chat-engine = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + } + } + } +} +# tftest modules=1 resources=2 +``` + +### Search Engine + +This is a minimal example to create a Search Engine agent. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-search-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_SEARCH"] + } + } + engines_configs = { + my-search-engine = { + data_store_ids = ["data-store-1"] + search_engine_config = {} + } + } +} +# tftest modules=1 resources=2 +``` + +### Deploy your service into a region + +By default services are deployed globally. You optionally specify a region where to deploy them. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-chat-app" + project_id = var.project_id + location = var.region + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + my-chat-engine = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + } + } + } +} +# tftest modules=1 resources=2 +``` + +### Reference existing data sources + +You can reference from engines existing data sources created outside this module, by passing their ids. In this case, you'll need to configure in the engine valid `industry_vertical` and `location`. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-search-app" + project_id = var.project_id + engines_configs = { + my-search-engine = { + data_store_ids = [ + "projects/my-project/locations/global/collections/my-collection/dataStores/data-store-1" + ] + industry_vertical = "GENERIC" + search_engine_config = {} + } + } +} +# tftest modules=1 resources=1 +``` + +### Using multiple data stores + +You can create and connect from your engines multiple data stores. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + data-store-2 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + my-chat-engine = { + data_store_ids = [ + "data-store-1", + "data-store-2", + "projects/my-project/locations/global/collections/default_collection/dataStores/data-store-3" + ] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + } + } + } +} +# tftest modules=1 resources=3 +``` + +### Set data store schemas + +You can configure JSON data store schema definitions directly in your data store configuration. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-search-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + json_schema = "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"datetime_detection\":true,\"type\":\"object\",\"geolocation_detection\":true}" + solution_types = ["SOLUTION_TYPE_SEARCH"] + } + } +} +# tftest modules=1 resources=2 +``` + +### Back data stores with websites data + +You can make data stores point to multiple websites and optionally specify their sitemap. + +```hcl +module "agentspace" { + source = "./fabric/modules/agentspace" + name = "my-search-app" + project_id = var.project_id + data_stores_configs = { + website-search-ds = { + solution_types = ["SOLUTION_TYPE_SEARCH"] + sites_search_config = { + sitemap_uri = "https://cloud.google.com/sitemap.xml" + target_sites = { + include-google-docs = { + provided_uri_pattern = "cloud.google.com/docs/*" + } + exclude-one-page = { + exact_match = true + provided_uri_pattern = "https://cloud.google.com/agentspace" + type = "EXCLUDE" + } + } + } + } + } + engines_configs = { + my-search-engine = { + data_store_ids = [ + "website-search-ds" + ] + industry_vertical = "GENERIC" + search_engine_config = {} + } + } +} +# tftest modules=1 resources=5 +``` + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [name](variables.tf#L159) | The name of the resources. | string | ✓ | | +| [project_id](variables.tf#L165) | The ID of the project where the data stores and the agents will be created. | string | ✓ | | +| [data_stores_configs](variables.tf#L17) | The Agentspace datastore configurations. | map(object({…})) | | {} | +| [engines_configs](variables.tf#L112) | The Agentspace engines configurations. | map(object({…})) | | {} | +| [location](variables.tf#L153) | Location where the data stores and agents will be created. | string | | "global" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [chat_engine_ids](outputs.tf#L17) | The ids of the chat engines created. | | +| [chat_engines](outputs.tf#L25) | The chat engines created. | | +| [data_store_ids](outputs.tf#L30) | The ids of the data stores created. | | +| [data_stores](outputs.tf#L38) | The data stores resources created. | | +| [search_engine_ids](outputs.tf#L43) | The ids of the search engines created. | | +| [search_engines](outputs.tf#L51) | The search engines created. | | + diff --git a/modules/agentspace/engines.tf b/modules/agentspace/engines.tf new file mode 100644 index 000000000..4c84f9b79 --- /dev/null +++ b/modules/agentspace/engines.tf @@ -0,0 +1,94 @@ +/** + * Copyright 2025 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_discovery_engine_chat_engine" "default" { + for_each = ({ + for k, v in var.engines_configs + : k => v if v.chat_engine_config != null + }) + engine_id = "${var.name}-${each.key}" + display_name = "${var.name}-${each.key}" + collection_id = each.value.collection_id + project = var.project_id + data_store_ids = [ + for ds in each.value.data_store_ids + : coalesce( + try(google_discovery_engine_data_store.default[ds].data_store_id, null), + ds + ) + ] + industry_vertical = coalesce( + try(each.value.industry_vertical, null), + try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].industry_vertical, null) + ) + location = coalesce( + try(each.value.location, null), + try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), + var.location + ) + + chat_engine_config { + allow_cross_region = each.value.chat_engine_config.allow_cross_region + dialogflow_agent_to_link = each.value.chat_engine_config.dialogflow_agent_to_link + + agent_creation_config { + business = each.value.chat_engine_config.business + default_language_code = each.value.chat_engine_config.default_language_code + time_zone = each.value.chat_engine_config.time_zone + location = coalesce( + try(each.value.location, null), + try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), + var.location + ) + } + } + + common_config { + company_name = each.value.chat_engine_config.company_name + } +} + +resource "google_discovery_engine_search_engine" "default" { + for_each = ({ + for k, v in var.engines_configs + : k => v if v.search_engine_config != null + }) + engine_id = "${var.name}-${each.key}" + display_name = "${var.name}-${each.key}" + collection_id = each.value.collection_id + project = var.project_id + data_store_ids = [ + for ds in each.value.data_store_ids + : coalesce( + try(google_discovery_engine_data_store.default[ds].data_store_id, null), + ds + ) + ] + industry_vertical = coalesce( + try(each.value.industry_vertical, null), + try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].industry_vertical, null) + ) + location = coalesce( + try(each.value.location, null), + try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), + var.location + ) + + search_engine_config { + search_add_ons = each.value.search_engine_config.search_add_ons + search_tier = each.value.search_engine_config.search_tier + } +} diff --git a/modules/agentspace/main.tf b/modules/agentspace/main.tf new file mode 100644 index 000000000..5ae3d1119 --- /dev/null +++ b/modules/agentspace/main.tf @@ -0,0 +1,194 @@ +/** + * Copyright 2025 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. + */ + +locals { + data_stores_target_sites = { + for item in flatten([ + for data_store_id, data_store_config in var.data_stores_configs : + [ + for target_site_key, target_site_config in try( + data_store_config.sites_search_config.target_sites, {}) : + merge({ + key = "${data_store_id}-${target_site_key}" + data_store_id = data_store_id + }, target_site_config) + ] + ]) : item.key => item + } +} + +resource "google_discovery_engine_data_store" "default" { + for_each = var.data_stores_configs + data_store_id = "${var.name}-${each.key}" + project = var.project_id + location = coalesce(each.value.location, var.location) + display_name = each.value.display_name + industry_vertical = each.value.industry_vertical + content_config = each.value.content_config + solution_types = each.value.solution_types + create_advanced_site_search = each.value.create_advanced_site_search + skip_default_schema_creation = each.value.skip_default_schema_creation + + dynamic "advanced_site_search_config" { + for_each = ( + try(each.value.advanced_site_search_config, null) == null + ? [] : [""] + ) + + content { + disable_initial_index = each.value.advanced_site_search_config.disable_initial_index + disable_automatic_refresh = each.value.advanced_site_search_config.disable_automatic_refresh + } + } + + dynamic "document_processing_config" { + for_each = ( + try(each.value.document_processing_config, null) == null + ? [] : [""] + ) + + content { + dynamic "chunking_config" { + for_each = ( + try(each.value.document_processing_config.chunking_config, null) == null + ? [] : [""] + ) + + content { + dynamic "layout_based_chunking_config" { + for_each = ( + try(each.value.document_processing_config.chunking_config.layout_based_chunking_config, null) == null + ? [] : [""] + ) + + content { + chunk_size = each.value.document_processing_config.chunking_config.layout_based_chunking_config.chunk_size + include_ancestor_headings = each.value.document_processing_config.chunking_config.layout_based_chunking_config.include_ancestor_headings + } + } + } + } + + dynamic "default_parsing_config" { + for_each = ( + try(each.value.document_processing_config.default_parsing_config, null) == null + ? [] : [""] + ) + + content { + dynamic "digital_parsing_config" { + for_each = ( + try(each.value.document_processing_config.default_parsing_config.digital_parsing_config, null) == null + ? [] : [""] + ) + + content {} + } + + dynamic "layout_parsing_config" { + for_each = ( + try(each.value.document_processing_config.default_parsing_config.layout_parsing_config, null) == null + ? [] : [""] + ) + + content {} + } + + dynamic "ocr_parsing_config" { + for_each = ( + try(each.value.document_processing_config.default_parsing_config.ocr_parsing_config, null) == null + ? [] : [""] + ) + + content { + use_native_text = each.value.document_processing_config.default_parsing_config.ocr_parsing_config.use_native_text + } + } + } + } + + dynamic "parsing_config_overrides" { + for_each = each.value.document_processing_config.parsing_config_overrides + + content { + file_type = parsing_config_overrides.key + + dynamic "digital_parsing_config" { + for_each = ( + try(parsing_config_overrides.value["digital_parsing_config"], null) == null + ? [] : [""] + ) + + content {} + } + + dynamic "layout_parsing_config" { + for_each = ( + try(parsing_config_overrides.value["layout_parsing_config"], null) == null + ? [] : [""] + ) + + content {} + } + + dynamic "ocr_parsing_config" { + for_each = ( + try(parsing_config_overrides.value["ocr_parsing_config"], null) == null + ? [] : [""] + ) + + content { + use_native_text = parsing_config_overrides.value["ocr_parsing_config"]["use_native_text"] + } + } + } + } + } + } +} + +resource "google_discovery_engine_schema" "default" { + for_each = ({ + for k, v in var.data_stores_configs + : k => v if try(v.json_schema, null) != null + }) + schema_id = "${var.name}-${each.key}" + project = var.project_id + location = google_discovery_engine_data_store.default[each.key].location + data_store_id = google_discovery_engine_data_store.default[each.key].data_store_id + json_schema = each.value.json_schema +} + +resource "google_discovery_engine_sitemap" "default" { + for_each = ({ + for k, v in var.data_stores_configs + : k => v if try(v.sites_search_config.sitemap_uri, null) != null + }) + project = var.project_id + location = google_discovery_engine_data_store.default[each.key].location + data_store_id = google_discovery_engine_data_store.default[each.key].data_store_id + uri = each.value.sites_search_config.sitemap_uri +} + +resource "google_discovery_engine_target_site" "default" { + for_each = local.data_stores_target_sites + project = var.project_id + location = google_discovery_engine_data_store.default[each.value.data_store_id].location + data_store_id = google_discovery_engine_data_store.default[each.value.data_store_id].data_store_id + provided_uri_pattern = each.value.provided_uri_pattern + type = each.value.type + exact_match = each.value.exact_match +} diff --git a/modules/agentspace/outputs.tf b/modules/agentspace/outputs.tf new file mode 100644 index 000000000..10ca868b7 --- /dev/null +++ b/modules/agentspace/outputs.tf @@ -0,0 +1,54 @@ +/** + * Copyright 2024 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. + */ + +output "chat_engine_ids" { + description = "The ids of the chat engines created." + value = { + for k, v in google_discovery_engine_chat_engine.default + : k => v.id + } +} + +output "chat_engines" { + description = "The chat engines created." + value = google_discovery_engine_chat_engine.default +} + +output "data_store_ids" { + description = "The ids of the data stores created." + value = { + for k, v in google_discovery_engine_data_store.default + : k => v.id + } +} + +output "data_stores" { + description = "The data stores resources created." + value = google_discovery_engine_data_store.default +} + +output "search_engine_ids" { + description = "The ids of the search engines created." + value = { + for k, v in google_discovery_engine_search_engine.default + : k => v.id + } +} + +output "search_engines" { + description = "The search engines created." + value = google_discovery_engine_search_engine.default +} diff --git a/modules/agentspace/variables.tf b/modules/agentspace/variables.tf new file mode 100644 index 000000000..8adbd96f1 --- /dev/null +++ b/modules/agentspace/variables.tf @@ -0,0 +1,169 @@ +/** + * Copyright 2025 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. + */ + +variable "data_stores_configs" { + description = "The Agentspace datastore configurations." + type = map(object({ + advanced_site_search_config = optional(object({ + disable_initial_index = optional(bool) + disable_automatic_refresh = optional(bool) + })) + content_config = optional(string, "NO_CONTENT") + create_advanced_site_search = optional(bool) + display_name = optional(string, "Terraform managed.") + document_processing_config = optional(object({ + chunking_config = optional(object({ + layout_based_chunking_config = optional(object({ + chunk_size = optional(number) + include_ancestor_headings = optional(bool) + })) + })) + default_parsing_config = optional(object({ + digital_parsing_config = optional(bool) + layout_parsing_config = optional(bool) + ocr_parsing_config = optional(object({ + use_native_text = optional(bool) + })) + })) + # Accepted keys: docx, html, pdf + parsing_config_overrides = map(object({ + digital_parsing_config = optional(bool) + layout_parsing_config = optional(bool) + ocr_parsing_config = optional(object({ + use_native_text = optional(bool) + })) + })) + })) + industry_vertical = optional(string, "GENERIC") + json_schema = optional(string) + location = optional(string) + skip_default_schema_creation = optional(bool) + solution_types = optional(list(string)) + sites_search_config = optional(object({ + sitemap_uri = optional(string) + target_sites = map(object({ + provided_uri_pattern = string + exact_match = optional(bool, false) + type = optional(string, "INCLUDE") + })) + })) + })) + nullable = false + default = {} + validation { + condition = try(contains( + ["CONTENT_REQUIRED", "NO_CONTENT", "PUBLIC_WEBSITE"], + var.data_stores_configs.content_config + ), true) + error_message = "data_store_configs.content_config must be one or more of [CONTENT_REQUIRED, NO_CONTENT, PUBLIC_WEBSITE]." + } + validation { + condition = try(contains( + ["GENERIC", "HEALTHCARE_FHIR", "MEDIA"], + var.data_stores_configs.industry_vertical + ), true) + error_message = "data_store_configs.industry_vertical must be one or more of [GENERIC, HEALTHCARE_FHIR, MEDIA]." + } + validation { + condition = alltrue([ + for st in try(var.data_stores_configs.solution_types, []) + : contains([ + "SOLUTION_TYPE_CHAT", + "SOLUTION_TYPE_GENERATIVE_CHAT", + "SOLUTION_TYPE_RECOMMENDATION", + "SOLUTION_TYPE_SEARCH" + ], st) + ]) + error_message = "data_store_configs.solution_types must be one or more of [SOLUTION_TYPE_CHAT, SOLUTION_TYPE_GENERATIVE_CHAT, SOLUTION_TYPE_RECOMMENDATION, SOLUTION_TYPE_SEARCH]." + } + validation { + condition = alltrue([ + for k, _ in try(var.data_stores_configs.document_processing_config.parsing_config_overrides, {}) + : contains([ + "docx", + "html", + "pdf" + ], k) + ]) + error_message = "keys in var.data_stores_configs.document_processing_config.parsing_config_overrides must be one of [docx, html, pdf]." + } + validation { + condition = try(contains( + ["EXCLUDE", "INCLUDE"], + var.data_stores_configs.target_site_config + ), true) + error_message = "data_store_configs.target_site_config must be one or more of [EXCLUDE, INCLUDE]." + } +} + +variable "engines_configs" { + description = "The Agentspace engines configurations." + type = map(object({ + data_store_ids = list(string) + collection_id = optional(string, "default_collection") + chat_engine_config = optional(object({ + allow_cross_region = optional(bool) + business = optional(string) + company_name = optional(string) + default_language_code = optional(string) + dialogflow_agent_to_link = optional(string) + time_zone = optional(string) + })) + # If industry_vertical and location are not given, + # they are derived from the first datastore attached + # to the engines + industry_vertical = optional(string) + location = optional(string) + search_engine_config = optional(object({ + search_add_ons = optional(list(string), []) + search_tier = optional(string) + })) + })) + nullable = false + default = {} + validation { + condition = alltrue([ + for ao in try(var.engines_configs.search_engine_config.search_add_ons, []) + : contains(["SEARCH_ADD_ON_LLM"], ao) + ]) + error_message = "Elements in engines_configs.search_engine_config.search_add_ons must be one or more of [SEARCH_ADD_ON_LLM]." + } + validation { + condition = try(contains( + ["SEARCH_TIER_ENTERPRISE", "SEARCH_TIER_STANDARD"], + var.engines_configs.search_engine_config.search_tier + ), true) + error_message = "engines_configs.search_engine_config.search_tier must be one of [SEARCH_TIER_ENTERPRISE, SEARCH_TIER_STANDARD]." + } +} + +variable "location" { + description = "Location where the data stores and agents will be created." + type = string + default = "global" +} + +variable "name" { + description = "The name of the resources." + type = string + nullable = false +} + +variable "project_id" { + description = "The ID of the project where the data stores and the agents will be created." + type = string + nullable = false +} diff --git a/modules/agentspace/versions.tf b/modules/agentspace/versions.tf new file mode 100644 index 000000000..00cfef9d6 --- /dev/null +++ b/modules/agentspace/versions.tf @@ -0,0 +1,35 @@ +# Copyright 2025 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 +# +# https://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. + +# Fabric release: 40.1.0 + +terraform { + required_version = ">= 1.11.4" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.33.0, < 7.0.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.33.0, < 7.0.0" # tftest + } + } + provider_meta "google" { + module_name = "google-pso-tool/cloud-foundation-fabric/modules/alloydb:40.1.0-tf" + } + provider_meta "google-beta" { + module_name = "google-pso-tool/cloud-foundation-fabric/modules/alloydb:40.1.0-tf" + } +} diff --git a/modules/agentspace/versions.tofu b/modules/agentspace/versions.tofu new file mode 100644 index 000000000..b87d6d775 --- /dev/null +++ b/modules/agentspace/versions.tofu @@ -0,0 +1,35 @@ +# Copyright 2025 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 +# +# https://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. + +# Fabric release: 40.1.0 + +terraform { + required_version = ">= 1.9.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.33.0, < 7.0.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.33.0, < 7.0.0" # tftest + } + } + provider_meta "google" { + module_name = "google-pso-tool/cloud-foundation-fabric/modules/alloydb:40.1.0-tofu" + } + provider_meta "google-beta" { + module_name = "google-pso-tool/cloud-foundation-fabric/modules/alloydb:40.1.0-tofu" + } +} diff --git a/modules/apigee/README.md b/modules/apigee/README.md index 45229c5bc..d4a1d30fb 100644 --- a/modules/apigee/README.md +++ b/modules/apigee/README.md @@ -17,6 +17,7 @@ This module simplifies the creation of a Apigee resources (organization, environ - [New instance (Non VPC Peering Provisioning Mode)](#new-instance-non-vpc-peering-provisioning-mode) - [New endpoint attachment](#new-endpoint-attachment) - [Apigee add-ons](#apigee-add-ons) +- [New DNS ZONE](#new-dns-zone) - [IAM](#iam) - [Recipes](#recipes) - [Variables](#variables) @@ -306,6 +307,24 @@ module "apigee" { # tftest modules=1 resources=1 ``` +## New DNS ZONE + +``` +module "apigee" { + source = "./fabric/modules/apigee" + project_id = "my-project" + dns_zones = { + test = { + domain = "mydomain.com" + description = "Zone for mydomain.com" + target_project_id = "my-other-project" + target_network_id = "projects/my-other-projects/global/networks/vpc" + } + } +} +# tftest modules=1 resources=1 +``` + ### IAM ```hcl @@ -364,13 +383,14 @@ module "apigee" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L132) | Project ID. | string | ✓ | | +| [project_id](variables.tf#L144) | Project ID. | string | ✓ | | | [addons_config](variables.tf#L17) | Addons configuration. | object({…}) | | null | -| [endpoint_attachments](variables.tf#L29) | Endpoint attachments. | map(object({…})) | | {} | -| [envgroups](variables.tf#L39) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | {} | -| [environments](variables.tf#L46) | Environments. | map(object({…})) | | {} | -| [instances](variables.tf#L74) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | -| [organization](variables.tf#L100) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +| [dns_zones](variables.tf#L29) | DNS zones. | map(object({…})) | | {} | +| [endpoint_attachments](variables.tf#L41) | Endpoint attachments. | map(object({…})) | | {} | +| [envgroups](variables.tf#L51) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | {} | +| [environments](variables.tf#L58) | Environments. | map(object({…})) | | {} | +| [instances](variables.tf#L86) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | +| [organization](variables.tf#L112) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | ## Outputs diff --git a/modules/apigee/main.tf b/modules/apigee/main.tf index 0c8f6bcfa..130314797 100644 --- a/modules/apigee/main.tf +++ b/modules/apigee/main.tf @@ -173,3 +173,15 @@ resource "google_apigee_addons_config" "addons_config" { } } } + +resource "google_apigee_dns_zone" "dns_zones" { + for_each = var.dns_zones + org_id = google_apigee_organization.organization[0].id + dns_zone_id = each.key + domain = each.value.domain + description = each.value.description + peering_config { + target_project_id = each.value.target_project_id + target_network_id = each.value.target_network_id + } +} diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf index b7f9aa6d8..b450b47f5 100644 --- a/modules/apigee/variables.tf +++ b/modules/apigee/variables.tf @@ -26,6 +26,18 @@ variable "addons_config" { default = null } +variable "dns_zones" { + description = "DNS zones." + type = map(object({ + domain = string + description = string + target_project_id = string + target_network_id = string + })) + default = {} + nullable = false +} + variable "endpoint_attachments" { description = "Endpoint attachments." type = map(object({ diff --git a/modules/cloud-run-v2/README.md b/modules/cloud-run-v2/README.md index b237175b3..5c577f528 100644 --- a/modules/cloud-run-v2/README.md +++ b/modules/cloud-run-v2/README.md @@ -19,6 +19,7 @@ Cloud Run Services and Jobs, with support for IAM roles and Eventarc trigger cre - [Cloud Run Service Account](#cloud-run-service-account) - [Creating Cloud Run Jobs](#creating-cloud-run-jobs) - [Tag bindings](#tag-bindings) +- [IAP Configuration](#iap-configuration) - [Variables](#variables) - [Outputs](#outputs) - [Fixtures](#fixtures) @@ -808,14 +809,42 @@ module "cloud_run" { } # tftest modules=2 resources=7 ``` + +## IAP Configuration + +IAP is only supported for service. Refer to the [Configure IAP directly on cloud run](https://cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run) documentation for details on usage. + +```hcl +module "cloud_run" { + source = "./fabric/modules/cloud-run-v2" + project_id = var.project_id + name = "hello" + region = var.region + containers = { + hello = { + image = "us-docker.pkg.dev/cloudrun/container/hello" + env = { + VAR1 = "VALUE1" + VAR2 = "VALUE2" + } + } + } + + iap_config = { + iam = ["group:abc@domain.com"] + } + +} +# tftest modules=1 resources=2 +``` ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L181) | Name used for Cloud Run service. | string | ✓ | | -| [project_id](variables.tf#L196) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L201) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L206) | Name used for Cloud Run service. | string | ✓ | | +| [project_id](variables.tf#L221) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L226) | Region used for all resources. | string | ✓ | | | [containers](variables.tf#L17) | Containers in name => attributes format. | map(object({…})) | | {} | | [create_job](variables.tf#L80) | Create Cloud Run Job instead of Service. | bool | | false | | [custom_audiences](variables.tf#L86) | Custom audiences for service. | list(string) | | null | @@ -823,17 +852,18 @@ module "cloud_run" { | [encryption_key](variables.tf#L98) | The full resource name of the Cloud KMS CryptoKey. | string | | null | | [eventarc_triggers](variables.tf#L104) | Event arc triggers for different sources. | object({…}) | | {} | | [iam](variables.tf#L122) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [ingress](variables.tf#L128) | Ingress settings. | string | | null | -| [invoker_iam_disabled](variables.tf#L145) | Disables IAM permission check for run.routes.invoke for callers of this service. | bool | | false | -| [labels](variables.tf#L151) | Resource labels. | map(string) | | {} | -| [launch_stage](variables.tf#L157) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | -| [managed_revision](variables.tf#L174) | Whether the Terraform module should control the deployment of revisions. | bool | | true | -| [prefix](variables.tf#L186) | Optional prefix used for resource names. | string | | null | -| [revision](variables.tf#L206) | Revision template configurations. | object({…}) | | {} | -| [service_account](variables.tf#L245) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L251) | Auto-create service account. | bool | | false | -| [tag_bindings](variables.tf#L257) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | -| [volumes](variables.tf#L264) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | +| [iap_config](variables.tf#L128) | If present, turns on Identity-Aware Proxy (IAP) for the Cloud Run service. | object({…}) | | null | +| [ingress](variables.tf#L153) | Ingress settings. | string | | null | +| [invoker_iam_disabled](variables.tf#L170) | Disables IAM permission check for run.routes.invoke for callers of this service. | bool | | false | +| [labels](variables.tf#L176) | Resource labels. | map(string) | | {} | +| [launch_stage](variables.tf#L182) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | +| [managed_revision](variables.tf#L199) | Whether the Terraform module should control the deployment of revisions. | bool | | true | +| [prefix](variables.tf#L211) | Optional prefix used for resource names. | string | | null | +| [revision](variables.tf#L231) | Revision template configurations. | object({…}) | | {} | +| [service_account](variables.tf#L270) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L276) | Auto-create service account. | bool | | false | +| [tag_bindings](variables.tf#L282) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | +| [volumes](variables.tf#L289) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | | [vpc_connector_create](variables-vpcconnector.tf#L17) | Populate this to create a Serverless VPC Access connector. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-run-v2/main.tf b/modules/cloud-run-v2/main.tf index 4c0a0243b..84212ba07 100644 --- a/modules/cloud-run-v2/main.tf +++ b/modules/cloud-run-v2/main.tf @@ -54,6 +54,8 @@ locals { var.eventarc_triggers.service_account_email, null ) + + iap_enabled = var.iap_config != null } resource "google_cloud_run_v2_service_iam_member" "default" { diff --git a/modules/cloud-run-v2/service.tf b/modules/cloud-run-v2/service.tf index a9b1e0fea..d493b927d 100644 --- a/modules/cloud-run-v2/service.tf +++ b/modules/cloud-run-v2/service.tf @@ -30,6 +30,7 @@ resource "google_cloud_run_v2_service" "service" { launch_stage = var.launch_stage custom_audiences = var.custom_audiences deletion_protection = var.deletion_protection + iap_enabled = local.iap_enabled template { labels = var.revision.labels @@ -274,6 +275,7 @@ resource "google_cloud_run_v2_service" "service_unmanaged" { launch_stage = var.launch_stage custom_audiences = var.custom_audiences deletion_protection = var.deletion_protection + iap_enabled = local.iap_enabled template { labels = var.revision.labels @@ -522,3 +524,28 @@ resource "google_cloud_run_v2_service_iam_binding" "binding" { ) ) } + +locals { + + iap_iam_additive = local.iap_enabled ? var.iap_config.iam_additive : [] + iap_iam = local.iap_enabled ? var.iap_config.iam : [] + +} + +resource "google_iap_web_cloud_run_service_iam_member" "member" { + for_each = toset(local.iap_iam_additive) + project = local.service.project + location = local.service.location + cloud_run_service_name = local.service.name + role = "roles/iap.httpsResourceAccessor" + member = each.key +} + +resource "google_iap_web_cloud_run_service_iam_binding" "binding" { + for_each = length(local.iap_iam) == 0 ? {} : { 1 = 1 } + project = local.service.project + location = local.service.location + cloud_run_service_name = local.service.name + role = "roles/iap.httpsResourceAccessor" + members = local.iap_iam +} \ No newline at end of file diff --git a/modules/cloud-run-v2/variables.tf b/modules/cloud-run-v2/variables.tf index 61a9ab95c..1de08de08 100644 --- a/modules/cloud-run-v2/variables.tf +++ b/modules/cloud-run-v2/variables.tf @@ -125,6 +125,31 @@ variable "iam" { default = {} } +variable "iap_config" { + description = "If present, turns on Identity-Aware Proxy (IAP) for the Cloud Run service." + type = object({ + iam = optional(list(string), []) + iam_additive = optional(list(string), []) + }) + default = null + + validation { + condition = !(length(try(var.iap_config.iam, [])) > 0 && length(try(var.iap_config.iam_additive, [])) > 0) + + error_message = "Providing both 'iam' and 'iam_additive' in iap_config is not supported." + } + + validation { + condition = var.iap_config == null || !var.create_job + error_message = "IAP is only supported for Cloud Run services, not Cloud Run jobs. Set create_job to false when using iap_config." + } + + validation { + condition = var.iap_config == null || var.launch_stage != "GA" + error_message = "iap is currently not supported in GA. Set launch_stage to 'BETA' or lower." + } +} + variable "ingress" { description = "Ingress settings." type = string diff --git a/modules/net-lb-app-ext-regional/variables-backend-service.tf b/modules/net-lb-app-ext-regional/variables-backend-service.tf index 78146d431..3cd2933fd 100644 --- a/modules/net-lb-app-ext-regional/variables-backend-service.tf +++ b/modules/net-lb-app-ext-regional/variables-backend-service.tf @@ -125,7 +125,9 @@ variable "backend_service_configs" { for backend_service in values(var.backend_service_configs) : contains( [ "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION", - "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO" + "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO", + "GENERATED_COOKIE", "HEADER_FIELD", "HTTP_COOKIE", + "STRONG_COOKIE_AFFINITY" ], coalesce(backend_service.session_affinity, "NONE") ) diff --git a/modules/net-lb-app-ext/variables-backend-service.tf b/modules/net-lb-app-ext/variables-backend-service.tf index 63d445a9d..19e88cb49 100644 --- a/modules/net-lb-app-ext/variables-backend-service.tf +++ b/modules/net-lb-app-ext/variables-backend-service.tf @@ -148,7 +148,9 @@ variable "backend_service_configs" { for backend_service in values(var.backend_service_configs) : contains( [ "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION", - "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO" + "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO", + "GENERATED_COOKIE", "HEADER_FIELD", "HTTP_COOKIE", + "STRONG_COOKIE_AFFINITY" ], coalesce(backend_service.session_affinity, "NONE") )